@dna-codes/dna-api
v0.3.0
Published
DNA-derived GraphQL API server. Loads an OperationalDNA at startup, generates a GraphQL schema from its noun primitives + relationships + operations, and serves runtime data through @dna-codes/dna-adapters/integration/neo4j.
Readme
@dna-codes/dna-api
Registry-native GraphQL API server. Tenants get a runtime-configurable
type system — ResourceType and RelationshipType records are the
schema. Admins author types through the API; the GraphQL schema
regenerates and hot-swaps to reflect them. The DNA file is a seed for
the foundational types on first boot; after that, storage is the source
of truth.
┌────────────────┐ first boot only ┌────────────────────────┐
│ OperationalDNA │ ──────────────────▶│ ResourceTypes (seeded) │
└────────────────┘ └───────┬────────────────┘
│
admin API mutations
│
▼
┌──────────────────────┐
│ GraphQL API │
└────────┬─────────────┘
│
DnaDataStore
│
▼
┌────────┐
│ Neo4j │
└────────┘Built on top of @dna-codes/dna-adapters/integration/neo4j, which
persists everything (ResourceType, RelationshipType, Instance,
Link) using a single storage layer with versioned history.
Quick start — single-org Docker
From the repo root:
# Pick any DNA — it just seeds the foundational types on first boot:
cp examples/lending/operational.json packages/api/dna.json
docker compose -f packages/api/docker-compose.yml upThen in another terminal:
# Health check
curl -s http://localhost:4000/healthz
# Schema introspection — see the registry-native CRUD surface
curl -s http://localhost:4000/graphql \
-H 'Content-Type: application/json' \
-d '{"query":"{ resourceTypes { id name category currentVersion isSeed } }"}'
# Author a new ResourceType (the tenant adds their own domain type)
curl -s http://localhost:4000/graphql \
-H 'Content-Type: application/json' \
-d '{"query":"mutation { createResourceType(input: { name: \"Account\", category: RESOURCE, attributeSchema: [{ name: \"balance\", type: NUMBER, required: true }] }) { id name currentVersion } }"}'
# Now the schema has an Account type. Create an instance:
curl -s http://localhost:4000/graphql \
-H 'Content-Type: application/json' \
-d '{"query":"mutation { createAccount(input: { balance: 5000 }) { id balance _schemaVersion } }"}'The Neo4j Browser at http://localhost:7474 (neo4j / devpassword)
shows the storage shape — :ResourceType nodes, :ResourceTypeVersion
history, and per-typename Instance nodes (:Loan, :Borrower, …).
Quick start — multi-example-org Docker
Three example DNAs running as fully isolated per-org stacks:
docker compose -f packages/api/docker-compose.examples.yml up| Org | API endpoint | Neo4j HTTP | Neo4j Bolt | Schema seeded from |
|---|---|---|---|---|
| lending | http://localhost:4001/graphql | :7475 | :7688 | examples/lending |
| registry | http://localhost:4002/graphql | :7476 | :7689 | examples/registry |
| mass-tort | http://localhost:4003/graphql | :7477 | :7690 | examples/mass-tort |
Docker setup is local-dev only. No TLS, no secret rotation, no production hardening. See "Out of scope" below.
Quick start — CLI without Docker
NEO4J_URI=bolt://localhost:7687 \
NEO4J_USERNAME=neo4j \
NEO4J_PASSWORD=devpassword \
node packages/api/bin/dna-api.js serve --dna ./examples/lending/operational.json --port 4000How the type system works
First-boot seeding
On startup, the CLI checks dataStore.hasBeenSeeded(). If false, it
calls dataStore.seedFromDna(dna), which writes:
- Four foundational
ResourceTyperecords —Person,Role,Group,Resource. These are the noun-categories tenant-defined types specialize into. All markedis_seed: true. - One
ResourceTypeperdna.domain.{persons,roles,groups,resources}entry. The DNA'sLoanandBorrowerbecome tenant-domainResourceTyperecords. - One
RelationshipTypeperdna.relationships[]entry. Sameis_seed: trueflag. - A
:SeedMarkersentinel node so subsequent boots skip seeding.
After first boot, the DNA file is no longer load-bearing. The CLI
continues to require --dna (or DNA_FILE) at every startup so it
can compute a drift warning if the file has changed since the seed —
but no re-seed runs.
Versioning
Each ResourceType.attribute_schema change creates a new immutable
ResourceTypeVersion record and bumps the live record's
current_version. Every Resource write stamps _schemaVersion =
current_version. Reads return the stamped version unchanged.
v1 does NOT retroactively migrate or revalidate existing data when a
schema changes. The version stamp is the contract — tenants migrate
data explicitly via update<Type> mutations.
Stability
Each ResourceType / RelationshipType also carries a stability
marker — experimental / beta / stable / deprecated (Kubernetes
API-maturity model) — describing how settled the concept is. It is
orthogonal to current_version: current_version tracks schema
shape, stability tracks concept maturity. A type can be
experimental at version 1 or stable at version 3.
- The four foundational types (
Person,Role,Group,Resource) seed asstable; every other seeded type defaults toexperimentalunless the authored DNA definition declares astability. stabilityis queryable on both type kinds and on their version history (each version snapshot records the stability in effect when it was written), and is accepted on the create/update inputs.setResourceTypeStability(id, stability)andsetRelationshipTypeStability(id, stability)transition the marker without bumpingcurrent_versionor appending a version record — the dedicated orthogonal path. (No schema rebuild: stability does not affect the generated GraphQL shape.)
curl -s localhost:4000/graphql -H 'content-type: application/json' \
-d '{"query":"mutation { setResourceTypeStability(id: \"<id>\", stability: STABLE) { name stability currentVersion } }"}'Schema hot-reload
createResourceType / updateResourceType / deleteResourceType (and
the same for RelationshipType) trigger a SchemaManager.rebuild() in
the resolver. The Apollo Server instance is recreated with the fresh
schema; in-flight requests started before the swap complete against
the prior schema. Expect a sub-second window of 5xx during the swap on
the rare schema-mutation event.
Foundational types are deletable (with cascade)
is_seed: true records can be deleted — but delete<Type>(id) without
cascade: true returns an error if the type is seeded. Pass cascade:
true to confirm; the resolver cascades into Instance deletion before
removing the type and its versions.
v1 limitations
All documented, all queued as follow-on proposals:
- Authentication / authorization — wide-open admin CRUD in v1.
- Rule enforcement (
dna.rules[]) — not consulted by resolvers. - GraphQL subscriptions / federation — not in v1.
- Retroactive schema migration — existing Resources keep their
write-time
_schemaVersionafter a type update. No automatic backfill. - DNA hot-reload — schema changes happen through the API, not by editing the DNA file.
- Multi-tenancy in a single process — one DNA seed per process; multi-org via multiple processes / containers.
- Naive pluralization —
Person → personsis wired; other irregulars need a one-line override map addition. - Dates as
String— custom scalars deferred to v2. - DataLoader batching — queue after a benchmark.
- DNA
Operationcodegen — dropped from v1. Operations may return as a fifth top-level type in a future proposal.
Out of scope (production hardening)
The Docker assets are local-dev only. They explicitly do NOT provide TLS termination, secret management beyond env vars, readiness gating, structured observability, Neo4j backup/restore, or multi-replica scaling. These belong in a deployment story owned by the operator.
Environment variables
| Variable | Required | Description |
|---|---|---|
| DNA_FILE | yes (or --dna) | Path to a seed OperationalDNA JSON document. Used for first-boot seeding; drift-checked on subsequent boots. |
| NEO4J_URI | yes | Bolt URI, e.g. bolt://localhost:7687 |
| NEO4J_USERNAME | yes | Basic-auth username |
| NEO4J_PASSWORD | yes | Basic-auth password |
| NEO4J_DATABASE | no | Database name (driver default if unset) |
| PORT | no | HTTP port (default 4000) |
Migrating from v0.1.0 → v0.2.0
If you ran the prior @dna-codes/[email protected], run the migration
script ONCE per Neo4j instance before deploying v0.2.0:
NEO4J_URI=bolt://localhost:7687 \
NEO4J_USERNAME=neo4j \
NEO4J_PASSWORD=devpassword \
npx ts-node packages/api/scripts/migrate-to-registry.tsThe script:
- Renames
:TypeDefinition→:ResourceType(stampscurrent_version: 1,is_seed: false). - Same for
:RelationshipDef→:RelationshipType. - Writes initial
:ResourceTypeVersion/:RelationshipTypeVersionhistory records. - Stamps
_schemaVersion: 1on every Instance node and:LINKedge. - Writes a
:SeedMarkerso the next API boot skips re-seeding.
Idempotent — re-running after success is a no-op.
Releasing
See the root README.md. Tag-driven release
publishes @dna-codes/dna-core, @dna-codes/dna-adapters, and this
package together.
