AGENTS.md: guide for coding agents
A web UI for managing Neo4j Aura, built as a GRANDstack setup. Neo4j is a cache of Aura’s state, served by grandstack-server (MODEL=aura); the Aura REST API is used to sync and to apply confirmed changes. The compose files are in the grandstack repository (compose.aura.yaml; the real Aura API is the default, compose.aura-mock.yaml adds a fake one for tests), and the gateway image is grandstack-federation’s. The repositories must sit side by side. Read docs/architecture.md first, then docs/recipes.md for the change you are making. PLAN.md holds the design decisions and the build history, and is kept true.
Commands
npm ci
npm run typecheck # all workspaces
npm run docs:check # relative links, anchors, and every diagram's alt text and legend
npm test # all workspaces; the integration tests need NEO4J_TEST_URI and ../grandstack-server built
npm run generate:api # aura/src/generated/aura-api.d.ts from docs/aura_api_spec_v1-2.yaml
npm run compose:supergraph # recompose gateway/supergraph.graphql from the RUNNING subgraphs (stack up first)
npm run codegen -w web # typed queries for the web app, read from the RUNNING gateway; commit the output
BASE_URL=http://localhost:3000 npm run e2e -w web # real-browser checks; needs Chrome and a FRESH stack WITH THE FAKE AURA, see web/e2e/README.md
# the stack, from the grandstack folder (see README.md "Run it"; needs SECRET_KEY in grandstack/.env):
# docker compose -f compose.yaml -f compose.aura.yaml up --build # the REAL Aura; add -f compose.aura-mock.yaml for the fake (tests), -f compose.aura-edge.yaml for TLS and a sign-in
Before you finish a change: npm run typecheck && npm test && npm run docs:check, and the integration tests (with a Neo4j and the sibling server built) if you touched aura/ or models/. If you changed a subgraph’s schema or models/aura/schema.graphql, recompose and commit the supergraph.
Layout
models/aura/the graph model thatgrandstack-serverloads withMODEL=aura:schema.graphql(every type and field described),setup.cypher(idempotent, run at every start),model.json. There is no server code in this repository.aura/the Aura subgraph.src/client.tsthe REST client and token manager,src/sync.tsAura into the store,src/service.tsconnect, disconnect, sync-now, reveal and check,src/crypto.tsandsrc/secrets.tsthe sealed secrets,src/schema.tsits GraphQL,src/storeClient.tsthe writable server.src/generated/is generated, never edit it.test/integration/starts two realgrandstack-serverprocesses (read-only and writable) from the sibling repository, with a real Neo4j and mock-aura.aura/src/operations/plan, confirm, run and recover a change (state.tsthe state machine,diff.tsthe review,plan.tsthe checks,run.tsthe reconciler,service.tsthe entry points,store.tsthe store calls).mock-aura/a fake Aura API.test/contract.test.tsvalidates its responses against the real spec.gateway/supergraph.graphqlis GENERATED bynpm run compose:supergraphand committed;gateway.config.tsandmesh.config.tsare the config. They are mounted intograndstack-federation’s gateway image bycompose.aura.yaml; there is no Dockerfile here.test/supergraph.test.tsguards what it may expose.web/the React app (Vite, react-router, urql).src/graphql/documents.tsholds every query;src/graphql/generated/is generated from the gateway and committed.src/lib/is pure logic with unit tests;src/pages/andsrc/ui/are the screens;default.conf.templateis its nginx config (it also forwards/graphql);e2e/run.mjsdrives Chrome.edge/the optional nginx in front (TLS, sign-in, rate limits, headers);check.mjschecks what it promises.scripts/init-env.shwritesSECRET_KEYinto../grandstack/.env, and with--edgemakes the certificate and sign-in.docs/aura_api_spec_v1-2.yamlthe real spec. It is the source of truth for the REST API.
Rules that are easy to get wrong
- Build on the blocks; do not make a silo. A new repository or setup must be made from what GRANDstack already provides (the
grandstack-serverimage and its models, thegrandstack-federationgateway,grandstack-auth,grandstack-agents, compose overlays in thegrandstackrepository), and improve a block when it falls short rather than copying or working around it. Read building on GRANDstack before creating a repository, a compose file, a server, a gateway or a docs structure. Providing building blocks that people shape into their own solution is a goal of the project. - Never call a real Aura account from code, tests or scripts you run, unless the person has given credentials for it and said to. Then read first (token, tenants, instances) and ask before any create, change or delete, which cost money or cannot be undone. Pass credentials by environment only, never write them to a file, a commit or a log, and tear the stack down with
-vafterwards so the sealed secret does not stay in a volume. Otherwise use mock-aura, throughcompose.aura-mock.yaml, which fixes the Aura address to the fake so that a realAURA_BASE_URLin.envcannot reach a test. The stack now talks to the real Aura by default, so check which overlay a stack was started with before running anything that changes data. - Secrets are never GraphQL fields. The instance password and the Aura client secret live in
InstanceCredentialandAuraSecretnodes that are not inschema.graphql.aura/test/model.test.tsandaura/test/integration/model.test.tsfail if a password, secret, cipher or nonce field appears. Do not add one. - The read-only server is
READ_ONLY=true; the writable one is for the Aura subgraph only. The server adds@mutation(operations: [])to every@nodetype itself. The writable server has no token (the stock server has none): what keeps others out is that it is on the privatestorenetwork incompose.aura.yaml, publishes no port, and is not in the gateway’s config. Keep it that way, and never put it on a network the gateway or web server share. MODELholds one name, so one model runs per server. The Movies model is the default and the Aura model is mounted frommodels/. Two models need two servers and two databases. The Aura overlay uses its own Neo4j volume for that reason.- Timestamps are ISO 8601 UTC strings, not
DateTime. The library formatsDateTimewith the APOC plugin, which a plain Neo4j image lacks. - A
@cypherfield must always return a row. UseOPTIONAL MATCH. A field that returns no row removes the whole parent from the results (this hid instances once). - A secret is sealed before it is stored, and only ever read for the call that needs it.
SecretBoxbinds each secret to its owner, so a copied value will not open. Credentials are checked with Aura before anything is stored. Do not log, return or put a secret in an error, anOperationor adesiredvalue. Tests scan the whole database and every real service’s log for the secret. - The sync only connects a relationship on create or on a real change. Connecting one that already exists adds a second link in this library. Disconnect the old tag, then connect the new one, as two statements.
- The store’s filters have no
neq(v7). Useeqon the value you mean, orNOT. - Timestamps carry milliseconds (
2026-10-28T09:03:00.123Z). Without them two runs in one second tie and “latest” can be the wrong run. Aura’s owncreated_atis stored as Aura sends it. - This setup uses the standard GRANDstack ports (3000, 4000, 7474, 7687), as the other setups do, so it replaces them. When you test beside a running Movies stack, use a project name of your own (
-p gsaura) and an override that moves the ports, and never runcompose down -vunder the default project name: it deletes the Movies data volume. Check a response really came from the stack you mean before trusting a test. - Look at the app, do not only test it. Checks passing did not show the status chips stretched across every card or the phone top bar wrapping onto two lines; screenshots (
OUT=dir npm run e2e -w web) did. Check the phone width (390 px) and dark mode for any visual change. - The web app never holds a secret longer than a request. The client secret field is cleared after every attempt, success or not, and nothing is written to storage except the theme choice. The e2e run checks the page, storage and URL for the secret.
- Tag counts must use the “not removed” filter, or they count instances Aura no longer lists. Each facet’s count leaves out its own filter (
buildWhere(filters, facet)). - Integration test files share one database and wipe it, so they run one at a time (
fileParallelism: false). Do not add a second file that expects data from another. - Every type and field in
schema.graphqlneeds a"""description""". A test checks it. - Types need
@node(v7). v7 has no@unique: uniqueness comes frommodels/aura/setup.cypher. graphql16 ships CommonJS and ESM builds. Plain Node uses one; Vitest would use both and graphql then rejects schemas, or masks aGraphQLError, as coming from “another module”.aura/vitest.config.tsaliasesgraphqlto the CommonJS file, andaura/test/model.test.tsloads it withcreateRequire. If you see “Cannot use GraphQLSchema … from another module” or an error that is wrongly masked, this is why.- Keep
@apollo/subgraphat 2.14.4 (rootoverrides); newer breaks@neo4j/graphql7.6.4. Pin exact dependency versions. - The gateway watches
gateway/supergraph.graphqland stops if it reads the file half written. Composing over the mounted file while the gateway runs can crash it (CI did exactly that, and the app then answered 502). Compose to a temporary file to check (mesh-compose -c mesh.config.ts -o /tmp/x.graphql, thendiff), and afternpm run compose:supergraphrecreate the gateway container. - The mock must not drift from the spec. If you add an endpoint to mock-aura, add it to
contract.test.ts, and check its responses validate. - New code files need the copyright header (two lines:
Copyright (c) "Neo4j"andNeo4j Sweden AB [http://neo4j.com], in the file’s comment syntax). Generated files are exempt. - Base images in Dockerfiles and compose files are pinned to an exact version and a digest (
image:1.2.3@sha256:...), never a floating tag. To pin a new image, look up the digest of the multi-arch index. - Commit at the end of each phase (the phases are in PLAN.md section 11), once typecheck and tests pass and PLAN.md and README.md say what was built. One commit per phase, with a message that lists what was added and any design changes. Do not push or bump versions unless asked. Releases use the release-cut workflow, not hand-made tags (see
grandstack/docs/releasing.md): it sets the version in everypackage.json(root and workspaces must agree), tags, publishes four images and tells the website, which builds this repository’s docs from its entry ingrand-stack.github.io/docs-manifest.json. Add a page to that entry when you add a doc. - Nothing reaches Aura without a confirm. A plan only writes an
Operation(PLANNED) and returns its id.confirmOperationmoves PLANNED to CONFIRMED with an atomic compare-and-set; every state change goes throughmoveOperation, which throws on a move the state machine does not allow. Do not add a mutation that sends to Aura any other way. The supergraph test lists the exact mutations. - A change to Aura is never retried after a 5xx or a lost connection. It may have been applied. Reads are retried. Completion needs a stable status and the values that were asked for, not either alone.
- The stale check compares fields, not
syncedAt. A sync that changed nothing must not invalidate a plan; a changed name, size or switch must. - A page that reads a second query waits for it in the e2e run. The size options arrive after the form shows. Use
waitForbefore checking or clicking what depends on them. - mock-aura faults are for tests only (
POST /__faults,DELETE /__faults).reset()clears them. Clear them after a run that sets one. - Aura shows an instance password once, so store it before anything else can fail.
sendCreateseals and saves it the moment the create answer arrives, then records the instance, then marks steps. Do not reorder these. revealInstancePasswordis the only way a password leaves the store, and it records before it returns.RevealedPasswordmay be returned by no other field. The supergraph test checks this. Never log a revealed password, never put one in an operation, and keep it out of web storage and URLs.- A delete is finished by a 404, not by “destroying”. Waiting for the status would mark it done while Aura still lists the instance. Confirming a delete checks the typed name on the server.
- A create is never sent twice. It is not retried after a 5xx or lost answer, and recovery fails a possibly-sent create instead of asking again.
- The phone top bar has room for four icons, not five. A fifth made the page scroll sideways and made taps miss their targets on a touch viewport only. Put new actions in the page and run the e2e on the phone viewport.
- mock-aura test controls:
GET /__passwords/:idshows the password of a created instance, so a test can compare it with what the app shows. - Everything is published on loopback only, and the writable server not at all.
compose.aura.yaml(in thegrandstackrepository, whose other overlays publish on all interfaces) binds every port to127.0.0.1, because this app can delete databases.compose.aura-edge.yamlremoves them all and publishes the edge on 8443. Do not bind anything to0.0.0.0by default; opening it to a network is the user’s choice (EDGE_BIND). - The edge refuses to start without a hashed sign-in list and a certificate. Do not add a way around that. Secrets live in
edge/secrets/(git-ignored, mode 700);scripts/init-env.shmakes them and never replaces an existing one. - The app is one origin. The browser calls
/graphqlon its own origin; the web server (or the edge in front of it) forwards it to the gateway. Do not add a cross-origin call, an inline script or style, or a third-party resource: the content security policy would block it, and the browser run would fail on the console error. - nginx caches addresses at start unless told not to. The edge uses a resolver and variables in
proxy_passso a restarted gateway is found again. Keep that.add_headerin a location replaces the server’s headers, so the security headers are included again where one is added. - A busybox
greptreats\$and\|in basic expressions differently from GNU grep. Usegrep -Ein the edge’s start-up script. - A new
.github/workflowsaction is pinned by commit SHA (resolve it withgh api repos/OWNER/REPO/git/ref/tags/TAG), with the version in a comment. - Real Aura differs from its OpenAPI file.
graph_nodesandgraph_relationshipsare numbers, not strings. mock-aura follows the file (its contract test checks it), so it cannot catch this kind of mismatch: when something touches a new field, look at a real response. The store’s text fields takeString(value). - A real tenant offers about 3,000 option combinations. Never read every tenant’s options in one query. Read the chosen tenant’s, and keep reads compressed at the edge.
Keep the docs true
When you change behaviour, update the matching doc in the same change: routes, folder map and security model in docs/architecture.md; how a feature works in docs/features.md; settings and names in docs/customising.md; a new way of changing things in docs/recipes.md; test layers and gotchas in docs/testing.md; deployment in docs/deploying.md. PLAN.md records decisions and history. Diagrams are SVG files in docs/diagrams/ with a numbered legend under each figure: edit the SVG and its legend together. npm run docs:check fails on a broken relative link or anchor, a missing alt text, unsafe SVG content (script, style, foreignObject, external links), and legend numbers that do not match the callouts drawn (the blue numbered circles, fill #0a6190).
Source: grandstack-web-aura/AGENTS.md (this page is a copy; edit it in the repository)