Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Symbolic application source and exact lock

The normal author-owned input is riffdb.application.json. New project-shaped repositories use riffdb.application-source/v7; existing V1 through V6 documents retain their exact writers and decoders. The source names paths, operations, roles, and output targets. It never asks an author or agent to discover, copy, or maintain a compiler-derived identity.

The compiler writes riffdb.application.lock.json using the lock generation corresponding to the source schema (V1 through V8 today). That exact lock covers the normalized symbolic source; contract source, bundle, plan-root, lineage, version, and compiler formats; query module, source, and plan identities; tenant-unbound role definitions and their exact operation authority; and every generated artifact hash. It contains no clock, host path, credential, active pointer, tenant value, or runtime-selected identity.

author source: riffdb.application-source/v1
compiler lock: riffdb.application-lock/v1

Source and lock V1 are permanent compatibility formats. They contain Rust, TypeScript, and MCP generation targets and are never upgraded implicitly. A Python application uses the explicit V2 successor, which adds exactly one required generation member:

{
  "generation": {
    "mcp": "generated/ticketdesk.mcp.json",
    "python": "src/ticketdesk/generated.py",
    "rust": "src/generated/riffdb.rs",
    "typescript": "src/generated/riffdb.ts"
  },
  "schema": "riffdb.application-source/v2"
}

Preview migration locally with riffdb application migrate --to v2. The --write form atomically changes only riffdb.application.json; it does not write a lock, generate code, deploy, bind a role, seed data, or contact a server. Run riffdb application lock --write separately after reviewing the canonical source change. Lock V2 then covers the exact generated Python path, bytes, and digest alongside the existing three artifacts.

Application Source V3 adds an exact migrations array while retaining the V2 Python target. Each entry names one .riffm source and one retained canonical parent bundle. Lock V4 pins those sources, parent identities, generated migration bundles, and the one canonical successor bundle. Source V3 is for contract-data migration; application migrate --to v2 remains only the source-format V1-to-V2 helper and does not create V3.

{
  "schema": "riffdb.application-source/v3",
  "migrations": [
    {
      "parent_bundle": "retained/ticketdesk-v1.riffdb.contract.bundle",
      "source": "riffdb/migrations/ticketdesk-v1-to-v2.riffm"
    }
  ]
}

See Contract Migrations for the complete identity model, supported Gate A changes, and read-only planning workflow.

Application Source V4 adds declared .riffr reactive modules and three closed role allowlists: event_streams, watch_queries, and agent_subscriptions. The compiler emits exact Application Manifest V2 and Lock V5 artifacts. Lock V5 pins separate reactive source, operation, and module hashes and retains the V4 migration and canonical contract-bundle closure.

{
  "schema": "riffdb.application-source/v4",
  "reactive_modules": [
    {
      "name": "TicketActivity",
      "source": "riffdb/reactive/ticket_activity.riffr",
      "version": 1
    }
  ],
  "roles": [
    {
      "agent_subscriptions": ["TicketAgent"],
      "commands": [],
      "environment": "development",
      "event_streams": ["TicketEvents"],
      "name": "TicketDeskAgent",
      "queries": [],
      "tenant_scope": "tenant",
      "watch_queries": ["TicketWatch"]
    }
  ]
}

These members are required arrays in V4, including when a particular role has no operations of that kind. Consume authority never implies stream seek authority. See Reactive Modules for grammar and fixed bounds.

Application Source V5 adds one exact Go generation target. It emits Application Manifest V3 and Lock V6; the lock pins Go, Rust, TypeScript, Python, MCP, contract-bundle, migration, and reactive artifacts in one closure. The reactive_modules member remains required but may be empty—an application with named reads and commands does not invent a dummy stream.

{
  "schema": "riffdb.application-source/v5",
  "generation": {
    "go": "generated/go/client.go",
    "mcp": "generated/mcp/tools.json",
    "python": "generated/python/client.py",
    "rust": "generated/rust/client.rs",
    "typescript": "generated/typescript/client.ts"
  },
  "migrations": [],
  "reactive_modules": []
}

riffdb new --language rust|go|typescript|python uses this same V5/V3/V6 identity chain for every language. Selecting a language changes the runnable starter, not the compiled application identity or operation schemas.

Application Source V6 adds a required symbolic row_policies allowlist to every role. It emits exact Application Manifest V4 and Lock V7. The lock’s V3 role-definition receipt names each selected policy, protected entity, and operation class; executable policy bytecode remains covered by the contract bundle hash and is never copied into an application request or generated client.

{
  "schema": "riffdb.application-source/v6",
  "roles": [
    {
      "agent_subscriptions": [],
      "commands": [],
      "environment": "development",
      "event_streams": [],
      "name": "DocumentReader",
      "queries": ["GetDocument"],
      "row_policies": ["DocumentAccess"],
      "tenant_scope": "tenant",
      "watch_queries": []
    }
  ]
}

The manifest schema exposes this structure for compiler review and exact-lock generation. Protected queries, commands, projections, live views, event delivery, and contextual reactions now use the shared transaction-current evaluator. A database-wide reactive wakeup remains unavailable to protected roles because it has no compiler-owned stream and partition identity; use the protected consumer’s bounded next/long-poll operation instead. Application middleware is not an accepted substitute. See Compiled Row Policies.

Application Source V7 keeps V6 command, query, reactive, migration, role, and row-policy semantics, but replaces the complete generation object with one nonempty closed subset of rust, go, typescript, python, and mcp. It emits Application Manifest V5 and Lock V8. Each present member is generated in memory and hashed into the exact lock; an absent member is not generated, hashed, checked, materialized, or used to discover a language toolchain.

{
  "generation": {
    "go": "generated/go/client.go"
  },
  "schema": "riffdb.application-source/v7"
}

The five target names and their artifact kinds come from one compiler-owned closed registry. Paths must be unique. Empty maps, aliases, unknown targets, unsafe paths, missing declared artifacts, and extra undeclared SDK or MCP lock artifacts fail closed. Removing a target changes source, manifest, and lock identity; RiffDB leaves any old undeclared file in place as unowned content until the user removes it explicitly.

riffdb.toml may request only targets declared by V7. It may materialize a subset in one checkout, but it cannot add a target to the lock or change the authoritative identity. Omitting mcp suppresses only the generated catalog file; it does not disable or change the hosted MCP service, authorization, or operation visibility. A local generated driver still needs a closed dispatch catalog. riffdb dev --run recompiles that catalog once from the exact source and lock into its private temporary directory, verifies every operation against the locked role, and passes its compiler-domain hash through the local handshake. The temporary runtime catalog is not an application artifact, does not appear in Manifest V5 or Lock V8, and is removed with the disposable development process.

The source document is closed JSON with exactly these top-level members:

{
  "application": "ticketdesk",
  "contract": {
    "lineage": "TicketDesk",
    "source": "riffdb/contract.riff",
    "version": 1
  },
  "generation": {
    "mcp": "generated/ticketdesk.mcp.json",
    "rust": "src/generated/riffdb.rs",
    "typescript": "src/generated/riffdb.ts"
  },
  "query_modules": [
    {
      "name": "ticketdesk",
      "queries": [
        {
          "name": "TicketPage",
          "source": "riffdb/queries/ticket_page.riffq"
        }
      ],
      "version": 1
    }
  ],
  "roles": [
    {
      "commands": ["CreateTicket"],
      "environment": "development",
      "name": "TicketDeskAgent",
      "queries": ["TicketPage"],
      "tenant_scope": "tenant"
    }
  ],
  "schema": "riffdb.application-source/v1",
  "seed_inputs": ["riffdb/seed/dev.jsonl"]
}

All paths are normalized, workspace-relative paths. Absolute paths, parent components, empty components, duplicate paths, duplicate names, undeclared query references, and unknown members are rejected. Versions are positive. Names and collections are bounded; the source document may not exceed one MiB. No hash field is valid in author source.

The manifest format also permits the domain-empty bootstrap emitted by riffdb init: it contains one named structural query module whose queries array is empty and an empty roles array. The module still receives a canonical identity for generation and deployment, but it exposes no query, command, tool, or authority. Authors add explicit queries and roles as the domain grows; no placeholder operation is required.

Each role declares an exact environment and either global scope or tenant scope. A tenant-scoped role must receive one concrete tenant at binding time; a global role rejects a tenant argument. The role author names only commands and named queries. Field visibility, stable IDs, indexes, partitions, result shape, and scan ceilings are compiler-private consequences of those names. If a selected .riffq named query declares an exact secret returned leaf, the compiler also derives its query/entity/field authority atom; application source has no field-authority list or wildcard.

RiffDB sorts every set-like collection, serializes the validated source as compact canonical JSON with a final line feed, and hashes those bytes under the source version’s domain. The compiler exposes byte spans for source-defined symbols and paths so diagnostics can point to the reviewed input. Reformatting or reordering set-like members does not change identity; changing a semantic value does.

Lock compilation resolves all symbolic names against exact compiled inputs and privately derives role authority. Locked generation byte-compares the recompiled lock before writing. It never silently selects an ambient active version, trusts an author-provided hash, widens a role, or regenerates against a different identity.

The older riffdb.application-manifest/v1 exact document remains a supported compatibility artifact for deployments and stable V1 tooling. It includes bundle and module hashes, but is now emitted under generated/riffdb.application.exact.json; application authors do not edit it. The checked-in TicketDesk compatibility fixture is fixtures/application-manifests/ticketdesk-v1.json in the repository root.

Run source, lock, generation, and drift checks with:

riffdb application check
riffdb application lock --write
riffdb application lock --check
riffdb application generate --locked
./scripts/check-application-bindings

Generated Rust and TypeScript facades are present in V1; V2 also requires the generated Python facade. V3 and V4 retain all four generation targets. V5 adds the generated Go facade and requires all five targets; V6 retains that complete set. V7 declares any nonempty subset and binds exactly that subset in Manifest V5 and Lock V8. The language facades own parameter serialization, response decoding, exact identity checks, opaque cursors, read-after-commit fences, typed command outcomes, and retry-safe uncertainty recovery. Generated MCP schemas come from the same operation registry but omit secret-output queries. Application code uses the generated facades for those authorized reads; it does not turn the query into a reveal-shaped command. text; numeric IDs, field masks, protobuf records, and raw RPC wrappers remain generated or internal implementation details.

Check, inspect, bind, and revoke roles symbolically:

riffdb role check riffdb.application.json --role TicketDeskAgent --tenant acme
riffdb role describe riffdb.application.json --role TicketDeskAgent --tenant acme
riffdb role bind riffdb.application.json \
  --role TicketDeskAgent \
  --tenant acme \
  --principal app:ticketdesk \
  --actor-kind service \
  --audience ticketdesk \
  --credential-output .riffdb/ticketdesk.credential
riffdb role revoke <capability-uuidv7> --reason replaced

If the role selects a row policy with declared principal facts, add one operator-owned --principal-facts <JSON_OBJECT_PATH> document. Its object keys must exactly match the symbolic principal_fact_schemas reported by riffdb role describe; values use the natural JSON forms documented in Row policies. Generated application clients never receive the document, policy bytecode, or a policy-bypass parameter.

tenant_scope is exact author intent. A role declaring "tenant" requires --tenant <tenant-id> on check, describe, bind, development binding, and provisioning. A role declaring "global" requires that --tenant be omitted. The CLI reports application_role_tenant_required or application_role_tenant_forbidden with that precise recovery action before role compilation; it never silently widens a tenant role or invents a tenant.

The compiled role has a domain-separated identity covering the manifest, contract, immutable query modules, environment, tenant binding, named operations, compiler-derived visibility, and resource ceiling. That identity is retained as a non-authorizing capability marker for audit and substitution detection. The marker cannot execute any operation.

One identity chain at deployment

riffdb application deploy is the normal installed path for contract, query module, application role, generated client/MCP configuration, and seed data. It binds these steps into one checked chain:

application lock
  -> pinned canonical contract bundle
  -> exact parent version + bundle hash
  -> contract bundle hash
  -> query module version + hash
  -> reactive module version + hash + exact query dependencies
  -> compiled role identity
  -> capability ID + private credential

The private deployment journal records each exact identity after remote verification. Query-module publication uses the observed active module hash as its compare-and-swap precondition. A concurrent change reports the selected database and the lock, locked-module, expected-active, and actual-active hashes instead of degrading to invalid input. A process interruption before local publication is recovered by rerunning the same deploy; retained request and capability identities make replay deterministic.

For a V4 application, deployment then publishes each .riffr module against that exact contract and the canonical set of locked query-module identities. The server recompiles source and the CLI accepts the result only when the returned module name, version, content hash, and contract identity equal the lock. Immutable replay is accepted; a same-name/same-version content change, missing contract, or missing query-module dependency fails closed. The private deployment journal records reactive module identities separately, so a process interruption after remote publication resumes by verifying and replaying the same immutable module rather than inventing a new identity.

A changed lock is deliberately not resumed under an old role. If a role is already retained, deploy requires --provision-role <name> together with --replace-role-credential. The replacement is a resumable rotation: RiffDB retains the predecessor, creates and proves the exact successor, atomically switches the protected generated client and MCP configuration, and only then revokes the predecessor. A retry resumes from the retained phase; it never revokes the only proven credential. Application code and scripts should read identity from the generated client or lock and should never hardcode a contract version or module hash.

Role compilation for lock V3 always consumes generated/riffdb.contract.bundle and the exact locked query modules. Both application bind-dev-role and standalone role check|describe|bind discover that lock from either riffdb.application.json or the generated exact manifest. A successor is never reinterpreted as genesis during role compilation. The legacy --replace-expired-credential spelling remains an alias for --replace-role-credential.

Lock V3 makes the contract bundle part of this same chain. Genesis application lock --write remains local. For a successor, the command performs an authorized, read-only server preview against the active parent and writes generated/riffdb.contract.bundle; it does not deploy. Offline application lock --check and application generate --locked decode that pinned bundle instead of recompiling the successor as genesis.

When a V3 lock already pins byte-identical contract source, application lock --write refreshes query, role, and generated-client artifacts locally from that checked bundle. This is the supported path after installing a corrected code generator: it rewrites compiler-owned artifact hashes without treating the already-active contract as another successor. Any contract-source change still requires the authorized server preview against the active parent, and a damaged or substituted pinned bundle fails closed instead of falling back to the network path.

Lock V4 retains the V3 contract-bundle pin and adds exact direct-parent migration artifacts. riffdb migration plan --application riffdb.application.json validates and prints that local plan. This command is inspection only; it cannot mutate or activate a database.

Before deployment, the server compares the lock’s expected parent version and bundle hash and its candidate bundle hash. A disagreement returns the lock, expected-parent, actual-active, and server-compiled candidate identities and changes no catalog, module, role, seed, or deployment-journal state. If the exact candidate is already active, retry is an idempotent success.