Console
Sign in
Docs for Repository maintainer
On this page
  1. Architecture
  2. Code layout
  3. Domains and import rules
  4. Request flow
  5. Storage
  6. Catalog, model and events
  7. Development
  8. Test suites
  9. Release gates
  10. Scripts
  11. Release acceptance
  12. Capacity
  13. Results
  14. Statement shape at scale
  15. Deployment declaration
  16. Compatibility
  17. Integration baseline
  18. Harness
  19. Access extension
  20. Signature fixtures
  21. Platform release 1.0.0
  22. Release readiness
  23. Releasing
  24. Security
  25. Review
  26. Open findings

Internals

Maintainer documentation — you don't need this to use or operate the platform.

How Data is built, tested, measured and released, how it proves compatibility with the gateway and Access, and what its security review leaves open. This page is for maintainers of packages/data/.

Read bySecurity engineerRepository maintainer

Architecture#

Data is a headless modular monolith with an optional separate worker process and no user interface; splitting it into more services is not required. Studios, websites, scripts and services are all API clients, and none has a privileged database connection. Data never reads Access tables, never mints an identity (it holds only the gateway's public key), and never creates a session or runs a login flow (spec §1.2).

Code layout#

Path in packages/data/Owns
src/main.tsThe process entry point.
src/app/Hono routes, HTTP parsing, context establishment, response and error mapping.
src/domains/<domain>/A domain's policies, use cases, ports, public entry point and tests.
src/domains/<domain>/adapters/Concrete port implementations, assembled by the runtime.
src/platform/bun/The listener, transport primitives and process APIs.
src/platform/postgres/Connection lifecycle, scoped transactions, statement execution and migration mechanics.
src/runtime/Configuration, composition root, subscribers, readiness, signals and drain.
src/observability/Structured logs and metrics interfaces.
src/protocol/Errors, clock and envelopes shared by domains.
packages/client/The typed client (Client).
manifests/This deployment's manifest and policy bundle.
migrations/The reviewed SQL migrations.
test/Contract, compatibility, database, load and architecture suites.

Only src/runtime/config.ts reads environment variable names; everything else receives typed settings (Configuration).

Domains and import rules#

The required domains are gateway-context, workspaces, schema, authorization, catalog, records, audit and events, plus model for the workspace model.

DomainMay import
gateway-context, workspaces, schema, audit, eventsnothing
authorizationgateway-context, workspaces, schema
catalogschema, authorization, workspaces, model
modelschema
recordsgateway-context, workspaces, schema, authorization, audit, events
  1. A domain's core imports no Hono, no Bun API, no application handler, no runtime and no concrete adapter.
  2. The application imports domains' public entry points, never their internals.
  3. Only the composition root constructs concrete adapters.
  4. Worker orchestration lives in the runtime and calls the domains' public use cases.
  5. Audit and events accept primitive-typed envelopes and injected persistence ports, and do not import records.
  6. A unit-of-work port lets records coordinate its repositories, audit sink and outbox without importing the concrete SQL adapters.
  7. No cycle is allowed, including a type-only cycle, and no catch-all directory of shared implementations.

bun run architecture enforces the matrix and these rules, and test/architecture checks that the matrix names every domain.

Request flow#

  1. The gateway signs the caller's identity into x-gateway-signature. Data rebuilds the canonical payload from method, path, query, host and headers and verifies it with the route's public key; it never trusts a transmitted payload.
  2. One statement reads the workspace and its catalog (collections and policy bundle).
  3. Access answers the principal's permission keys for subject, tenant, workspace and route, or a refusal, outside the business-data transaction.
  4. The scoped transaction projects only granted fields, authorizes a write by one complete policy, and commits record, audit entry, domain event and receipt together (spec §10).

Storage#

  1. Deployments run PostgreSQL 15 or later; development, tests and CI use 18 (decision 32).
  2. Business tables carry tenant_id and workspace_id under row-level security, scoped per transaction with set_config(…, true).
  3. There are five roles: migration owner, API runtime, registry reader, delivery and maintenance. The API never holds the credentials of the other four (Operations).
  4. Tables come only from reviewed migrations. A declared history table mirrors its collection table and is written by the change statement (spec §11.6).

Catalog, model and events#

  1. The catalog (registered collections and policy bundle) is a logical registry over the global physical schema (spec §7.5). It is read uncached, so a published bundle applies on the next request.
  2. The model overlays the manifest in realtime (spec §7.6). Its domain owns the model store, table column reader and workspace collection registrar ports.
  3. Subscribers registered in the composition root receive each committed event at least once, possibly out of order. Workers claim deliveries with leases and act with a fresh context for their service principal; the originating user is only an attribution.

The process roles (api, worker, all) are described in Operations.

Development#

Installation and the platform-wide commands are in platform development.

Test suites#

SuiteCommandCovers
Domain rulesbun test test/unitPredicates, field rules, write checks, schema validation, context expiry, identity, secrets, metrics.
API contractbun test test/contractPaths, envelopes, limits, error codes, ETags, receipts, aggregates, search, catalog, attack cases.
Database and workerbun test test/integrationRow-level security, transactions, worker claims, replicas, differential tests, search plan.
Boundariesbun test test/architectureImports, cycles, private adapter construction.
Suite strengthbun run test:mutationBreaks code on purpose; the suites must notice.
Clientbun test packages/client/testThe client against a mocked API.
Platformbun run test:compatReal gateway, Access and Data.

The differential tests (test/integration/filter.property.test.ts and write-guard.test.ts) compare generated predicates and write policies evaluated in memory and in the database. Fuzzing (test/unit/fuzz.test.ts) proves caller input never reaches a statement as an identifier. Integration tests start their own PostgreSQL 18 server (decision 16).

Release gates#

bash
bun run typecheck
bun run lint
bun run format:check
bun run architecture
bun run dead
bun test test/unit test/contract test/integration test/architecture packages/client/test
bun run test:mutation
bun run test:compat

The operational gates come on top: the backup rehearsal, the capacity measurement, the declared rate and latency objective, and CI's dependency audit. The mutation gate is textual, so a refactor that moves a mutation's text must update the mutation, or the gate fails it as stale.

Scripts#

Operator commands run on the deployment machine. They are never HTTP routes and never bypass Access. Their credentials are listed in Configuration.

DirectoryCommands
scripts/cli/provision.ts (roles and database), migrate.ts (migrations as schema owner), workspace.ts and collection.ts (create/register, enable, disable, list), policy.ts (publish, list, show), delivery.ts (list, requeue, backlog), retention.ts.
scripts/ops/backup.sh, restore.sh, rehearse-backup.sh (run with bash on a dedicated server), capacity.ts, scale.ts.
scripts/compat/compatibility.ts (harness), check-compatibility.ts (baseline drift).
scripts/gates/architecture.ts, mutation.ts, check-client-package.ts (packed client installed outside the repository).
scripts/dev/generate-client.ts (types from the manifest), generate-fixtures.ts (signature fixtures).

bun run dead finds unreachable modules, unread exported names and unused dependencies. bun run release:tag tags a release whose version increased.

To add a collection to this deployment, follow Modeling.

Release acceptance#

No requirement is complete until an automated check demonstrates its behavior.

SuiteRequired evidence
DomainPredicate evaluation, field rules, write check composition, schema validation, context expiry and event envelope rules.
HTTP contractPaths, envelopes, query limits, error codes, canonical ETags, create receipts, and the absence of deferred endpoints.
Gateway compatibilityThe workspace's unmodified gateway forwards real ES256-signed requests that Data accepts with the public key; forged signatures, tampered identities and workspace mismatches fail; a current-format route refuses the legacy format.
Client packagingThe client tarball installs outside the repository, imports by its public name, and resolves its own files without the service's dependencies.
Baseline driftThe baseline manifest, the spec's baseline block and the workflows agree.
Access compatibilityThe real context implementation returns the complete user and group key set and preserves the existing decisions contract.
PostgreSQL integrationRow-level security, composite reference constraints, refusal of a missing context, pool reuse, concurrent version checks, migration drift and role restrictions.
Transaction failureA failure injected at every mutation stage leaves record, audit, event and receipt all committed or all absent.
Version historyA version image is written by the change statement; a change the guard refuses writes no image; a version the caller may not read is not found; the deployment's policies authorize a rollback.
Subscriber integrationMulti-worker claims, lease expiry, duplicate delivery, retry exhaustion, missing handlers, service principal revocation and causation depth bounds.
ArchitectureAllowed imports, public entry points, no cycles and private adapter construction.
Dead codeModules no root reaches, exported names nobody reads, and unused dependencies.
Operations and loadDeadlines cancel SQL, resources stay bounded, dependency failures fail closed, the drain completes, and the restore is rehearsed.

Mandatory adversarial cases:

  1. A grant for workspace A cannot read, write, count, reference or expand workspace B's data.
  2. An Access grant for one tenant does not make another tenant's registered workspace accessible.
  3. A forged tenant, forged route, e-mail address match or stale role claim cannot influence authorization.
  4. Group removal, grant revocation and expiry, a disabled principal and a disabled tenant take effect within the documented freshness bounds.
  5. Filtering and sorting by a hidden field are refused, and counts and relation expansions cannot bypass row policies.
  6. Two policies with complementary field sets cannot authorize a synthetic write neither permits.
  7. An update that moves a row outside its check is refused atomically.
  8. Concurrent updates with one ETag yield one successful version change and one precondition refusal.
  9. A version hidden by the deployment's policies is not found in the history, and a rollback they forbid is refused: history is not a path around authorization.
  10. A POST retried after a committed but lost response creates one record and returns its original receipt.
  11. Reusing a create key with another body cannot create another record within the retention window.
  12. An expired or malformed Access context, wrong context identifiers and incomplete capability sets never allow a data operation.
  13. A pooled connection never keeps a previous tenant, workspace or principal scope.
  14. Runtime credentials cannot disable row-level security, alter the schema, edit audit history, or query Access's tables.
  15. A subscriber cannot turn an actor's attribution into current authorization.
  16. All backend functionality and API tests run without any Studio process or package.

Capacity#

bun run scripts/ops/capacity.ts starts PostgreSQL, a fixture Access, Data and concurrent clients that sign like the pinned gateway, then runs the scenarios of spec §15.5. Tune it with CAPACITY_CLIENTS (default 16) and CAPACITY_DURATION_MS (default 10000 per scenario). The numbers describe one machine at one moment; a deployment measures its own with the same command.

MachineValue
Hardware16 cores, x86_64, 31 GiB, Linux 7.2.9, Bun 1.4.3
DatabasePostgreSQL 18.6 on the same machine, fsync=off and synchronous_commit=off
DeploymentOne api process, DATA_DB_POOL_MAX=20, registry and delivery pools of 5
Context cacheDATA_CONTEXT_CACHE_TTL_MS=0: every request asks Access
CatalogThe workspace registered nothing; the deployment catalog is read with the registry in one statement
Data1000 articles in one workspace, one policy per action, pages of 50

Results#

Release candidate 1.0.0, guarded write path:

ScenarioRequestsRatep50p95p99Failures
Read heavy, a page of 50151021510/s10.4 ms13.4 ms15.5 ms0
Mixed, half creations214912149/s7.3 ms9.4 ms10.7 ms0
Concurrent updates of one record6011601/s26.6 ms30.9 ms33.8 ms0 (379 successes, 5632 precondition failures)
Access down626176262/s2.5 ms3.2 ms4.6 ms62617 answers 503
  1. Reads hold up with a context fetch per request: the fetch precedes the statement and is the cheap part.
  2. No update is lost: an update without the current validator gets 412 PRECONDITION_FAILED, and its latency is the row lock's.
  3. With Access down, requests fail closed in about two milliseconds (503 AUTHORIZATION_UNAVAILABLE), with no queue.
  4. Version 1.0.0 on PostgreSQL 15.19 measured 991/s, 1251/s, 313/s and 4280/s (p50 15.8, 11.6, 50.8 and 3.6 ms). Runs on different database versions are not comparable.
  5. Three consecutive read-heavy runs gave 1510/s, 1571/s and 1371/s, a spread of about a fifth.

Statement shape at scale#

bun run scripts/ops/scale.ts writes a million-row collection, reads a page statement's plan, walks every page and compares the rows with those of a single statement. Pages are selected first and projected afterwards (decision 2), in an index order (decision 3).

CollectionPlan beforePlan nowPage time
200 000 rowsSequential scan, sort of the whole scope, projection of every rowIndex scan of the page, 51 lookups97 ms → 0.9 ms
1 000 000 rowsThe same, three times the rowsThe same plan0.9 ms

The walk over 200 000 rows read 4 000 pages, every row, in database order. Sorting or filtering on a field needs that field's index in the collection's reviewed migration.

Not measured:

  1. Several replicas: reads scale with replicas, writes with the database.
  2. Worker capacity: integration tests cover claims, leases and retries; watch data_outbox_backlog and data_outbox_oldest_age_seconds.
  3. Maximum bounds (a 200-record page, a 50-predicate filter or a 1000-row expansion): contract tests check them and refuse an excess with 422 RESULT_TOO_LARGE.

Deployment declaration#

A deployment declares these items, measures them on its own hardware and publishes the result.

ItemDeclaresReference deployment
Request rateRequests per second1 000 per second, context fetched for every request
Latency objectivePercentile and valuep95 under 25 ms for a page of 50, under 50 ms for a write
Data shapeRecords, page size, filter predicates1 000 records in one workspace, pages of 50, ten-predicate filters
Policy complexityPolicies per collection, fields per policyOne policy per action, five fields each
PoolDATA_DB_POOL_MAX, database sizeDATA_DB_POOL_MAX=20, registry and delivery pools of five
ReplicasAPI processes and workersOne of each, on the measurement machine
AvailabilityCache lifetime and revocation boundDATA_CONTEXT_CACHE_TTL_MS=0: revocation shows on the next request; an Access outage answers 503

The reference objectives add a 50% margin to the measurement (1 510 and 2 149 requests per second at a p95 of 13.4 and 9.4 ms). A deployment that declares more than its machine holds answers 503 when full.

Compatibility#

The harness runs this repository's real gateway, Access and Data against a real PostgreSQL server.

Integration baseline#

compatibility.json at the repository root names the three service workspaces and the revision they are tested against (spec §1.1).

json
{
  "version": 2,
  "repository": "https://github.com/ggpwnkthx/platform.git",
  "workspaces": {
    "gateway": {
      "path": "packages/gateway",
      "source": {
        "repository": "https://github.com/ggpwnkthx/platform.git",
        "commit": "dc3d421a5f490594fe012d04eaaef21733736563"
      }
    },
    "access": {
      "path": "packages/access",
      "source": {
        "repository": "https://github.com/ggpwnkthx/platform.git",
        "commit": "dc3d421a5f490594fe012d04eaaef21733736563"
      }
    },
    "data": {
      "path": "packages/data"
    }
  }
}

References, candidates and overrides work as described in platform compatibility: here the gateway and Access are references and Data is the candidate. bun run check:compatibility proves the spec's baseline block agrees with the manifest, and refuses a CI workflow that pins its own revision.

Harness#

bash
bun run --cwd packages/data test:compat   # Data harness
bun run compatibility                     # Access contract and platform acceptance suites
GATEWAY_REPOSITORY=/srv/mirrors/gateway ACCESS_REPOSITORY=/srv/mirrors/access \
  bun run --cwd packages/data test:compat

Through the gateway, a client discovers a collection, creates a record under its own identity, reads the fields it may read, updates with a current validator, observes the audit metadata, and proves another tenant cannot reach the data.

The harness takes each reference's source from the first of these that is available:

  1. GATEWAY_REPOSITORY and ACCESS_REPOSITORY: a git URL or a local path, cloned without network,
  2. the COMPAT_MIRROR directory of bare gateway.git and access.git mirrors,
  3. the local repository,
  4. the manifest URL.

A URL with or without .git names the same mirror. GATEWAY_COMMIT and ACCESS_COMMIT must be full revisions; anything else is an error, with no fallback. COMPAT_KEEP keeps the run's temporary directory.

Access extension#

Without Access answering /v1/access-context, Data fails closed: the extension is a required dependency, and it lives upstream (decision 15). To change it, land the change in the Access workspace (keeping the gateway decision contract), then pass the harness against the new revision.

Signature fixtures#

bash
GATEWAY_CHECKOUT=/path/to/platform/gateway \
  bun run --cwd packages/data scripts/dev/generate-fixtures.ts

Gateway code at the repository revision produces the canonical payload and signature, and the fixture records that revision. Legacy v4 is included; since the gateway stopped signing it in 1.0.0, only its key derivation is kept locally.

Platform release 1.0.0#

Platform release 1.0.0 is a major change to the gateway and Access contracts. The Data contract (v1 routes, v5 identity, access context 1), the route table format, the decision protocol and the identity headers are unchanged. Impact on Data:

ChangeImpact
The gateway writes the caller subject as is (an unwritable one gets 500).Subjects are Access UUIDs. Data never trims or normalizes the header, and the unit suite proves a subject the gateway did not write is refused.
The gateway decides the host before the method.Bindings are per signed host; a deployment relying on a catch-all naming GET must move the hosts it names into the catch-all.
Gateway: GATEWAY_TRUSTED_PROXY_HOPS (default one), ranges only from GATEWAY_TRUSTED_PROXIES, password-free Redis user rules (passwords set after start).Gateway deployment only.
Access: tenant-scoped gateway credentials (deployment bearer token deprecated), platform-owned shared_client and is_default flags, limiter budget spent by failing calls, provider calls to the checked address, browser headers, uncached token answers.None: Data calls no decision endpoint or provider, holds no tenant, reads only the access context body, and its credential is scoped to the tenants it may ask about.

Release readiness#

A gate is done when it is a repository fact (a running check, a published measurement, a written decision), and outside when the repository cannot hold it. 1.0.0 is published but stays pre-production until both outside gates are met. The suites make requirements checked, not proved: the author wrote every check.

GateStateProof
Release gatesDoneEvery command passes.
Fuzzing and both differential testsDoneEight seeds each.
Client outside the repository, with bun and nodeDonebun run check:client-package
Service image built and startedDoneImage job in CI
Baseline driftDonebun run check:compatibility
Backup and restore rehearsalDonebash scripts/ops/rehearse-backup.sh; Operations
Capacity measurementDoneCapacity
Dependency auditDonebun audit in CI
A deployment with a real studio and tenantOutsideReal traffic and an environment: a machine, a domain, a tenant and a person who answers the phone.
A review by someone other than the authorOutsideThe security review, item by item.

Releasing#

Follow the platform release steps. The tag's workflows publish the image, binaries for seven platforms, the client and checksums. For Data:

  1. Update README.md, docs/data/README.md, CHANGELOG.md and docs/data/internals.md: retire the pre-production wording, and write "proved" only where someone else's check proves it.
  2. bun run release:tag compares versions numerically (0.10.0 is greater than 0.9.0) and falls back to the workflow's release identity when the checkout has no committer identity.
  3. The changelog's "Unreleased" section lists client-visible changes: the database evaluates a write's predicate, legacy identity is refused by default, the client is on a registry and runs on node, and backup works with the deployment's roles.

Security#

The rules are spec §2.3 and §6.4. This section reviews them and everything around them.

Review#

Reviewed with no finding:

#AreaWhere
1, 3–5Identity and routing: detached JWS rebuilt from the request, empty payload segment, ES256 with the gateway's public key only. The binding, never a header, picks version, algorithm and key, with no fallback. Exact host, one matching binding, ambiguity refused at load; encoded separators, backslashes and dots are not workspaces; the path's workspace must equal the signature's.src/domains/gateway-context/ (verify.ts, bindings.ts, index.ts), src/runtime/identity-keys.ts
6, 8Context validated, fresh per mutation, deadline rechecked before commit, 503 when absent. The registry reader sees five columns of one table (id, tenant, state, slug, name), by exact id.src/domains/authorization/access-context.ts, src/domains/authorization/adapters/access-context.ts, src/platform/postgres/adapters.ts
7, 18Forced row-level security, transaction-local scope. The API role cannot alter the schema, read deliveries or update the audit (insert and read only; no values).migrations/0002_platform.sql, migrations/0004_isolation.sql, src/platform/postgres/scoped.ts
9, 13–17Records: one in-statement projection for reads and writes; one complete policy, checked before and after a change; references readable in scope, refused generically; validators, receipts and lock-bounded key claims; signed cursors bound to caller, scope, query, versions and permissions; atomic mutations, with failure injected at every stage.src/domains/authorization/rules.ts, src/domains/records/service.ts, src/domains/records/canonical.ts, test/integration/transactions.test.ts
19–21Worker with its own principal, route and fresh context, and bounded chain depth; at-least-once delivery, recovering leases, deployment-registered subscribers; no undeclared tables; bundles published only by operator command.src/runtime/worker.ts, src/runtime/subscribers.ts, src/platform/postgres/adapters.ts, src/domains/catalog/index.ts, scripts/cli/policy.ts
22–25Every work bound, plus the authorization deadline before commit; secrets from files, never in image or log, redacted by the logger; metrics need their token (404 without one); non-root image without shell or build tools, digest-pinned, built in CI.src/runtime/config.ts, src/domains/records/types.ts, src/observability/logger.ts, src/app/app.ts, Dockerfile, .github/workflows/ci.yml
35Version images (the first business values outside the record) are read under the record's policies; rollback is an authorized write; the API role cannot update or delete images, and retention belongs to the maintenance role.migrations/0008_record_versions.sql, src/domains/records/service.ts, src/domains/records/write-guard.ts

Fixed:

#Finding and fixWhere
1bSubjects are read as is, never trimmed or normalized.src/domains/gateway-context/index.ts, test/unit/gateway-context.test.ts
2CI fuzzing: malformed, altered and payload-carrying signatures are refused without throwing.test/unit/fuzz.test.ts
3bSymmetric v4 can sign: refused unless DATA_LEGACY_IDENTITY allows it; removed in 2.0.0.src/domains/gateway-context/bindings.ts, src/runtime/config.ts
10Memory compared numeric-looking text as numbers and strings by UTF-16 units; now by field type.src/domains/authorization/filter/compile.ts, src/domains/authorization/filter/evaluate.ts, test/integration/filter.property.test.ts
10bThe write statement evaluates the predicates; memory is the oracle.src/domains/records/write-guard.ts, test/integration/write-guard.test.ts
10cReads no longer evaluate predicates in memory.src/domains/records/service.ts
11Fuzzing proves no caller value or identifier reaches statement text.src/domains/authorization/filter/parse.ts, src/domains/authorization/filter/compile.ts, test/unit/fuzz.test.ts
12JSON lists are bound as text, not list literals.src/domains/authorization/filter/parse.ts
26CI fails on a known dependency vulnerability..github/workflows/ci.yml
27The metrics token comparison leaked prefix length through timing; values are hashed first.src/app/secrets.ts
28Database failures answered 500; SQLSTATE now maps to contract codes.src/platform/postgres/errors.ts
29A tampered cursor answered 500; it now answers 400 INVALID_CURSOR.src/domains/records/canonical.ts
30Failed statements leaked pool connections until every request got 503.src/platform/postgres/scoped.ts
31The response budget was checked after building; now it is spent while building.src/domains/records/budget.ts, src/domains/records/service.ts
32The context cache was unbounded.src/domains/authorization/index.ts
33No per-tenant bound; now 429 TOO_MANY_REQUESTS.src/runtime/limits.ts
34The mutation gate found five unproven behaviors, now covered.scripts/gates/mutation.ts

Open findings#

#FindingSeverityState and plan
1A write's predicate was evaluated twice (SQL for reads, memory for writes) and diverged three times.MediumClosed by decision 5; the generated guard check compares both answers.
2Service availability is Access's: with no cache, an outage answers 503 to every call.MediumOpen, with a decision. A deployment preferring availability sets DATA_CONTEXT_CACHE_TTL_MS and declares the revocation bound (cache lifetime plus running request). The reference deployment declares no cache (Capacity).
3The driver reads timestamps in milliseconds while statements compare microseconds, so a sub-millisecond value compares differently on the two paths; likewise a JSON field holding JSON null.LowOpen, outside the contract. API writes normalize to the millisecond; only rows written directly to the database, against the migration contract, are affected.
4The service has never run outside its own harness; an author's check is not an assurance.MediumOpen. 1.0.0 is marked pre-production. The first real deployment is the review the harness cannot replace.
5Suites and code share assumptions; the mutation gate covers only imagined mutations.MediumOpen. The gate runs in CI and grows with the code: each reviewed behavior has a mutation naming the suite that catches it, fourteen trust-boundary mutations so far.