Local App suites
tablewalk suite suite.json serves several Apps in one process. Each App has
its own connection registry, grants and row policy. One App can live at /,
with others mounted beside it. By default each App has its own sign-in store.
An explicit suite identity enables one account and sign-in across Apps, with
access granted separately for each App.
{ "port": 4880, "apps": [ { "id": "lender", "directory": "./lender", "config": "./lender-connections.json", "mount": "/", "environment": { "BETTER_AUTH_SECRET": "LENDER_AUTH_SECRET", "XDG_CONFIG_HOME": "LENDER_RUNTIME_HOME", "DATABASE_URL": "LENDER_DATABASE_URL" } }, { "id": "dealer", "directory": "./dealer", "config": "./dealer-connections.json", "mount": "/dealer", "policy": "./dealer/policy.ts", "environment": { "BETTER_AUTH_SECRET": "DEALER_AUTH_SECRET", "XDG_CONFIG_HOME": "DEALER_RUNTIME_HOME", "DATABASE_URL": "DEALER_DATABASE_URL" } } ]}Paths are relative to the suite file. Each id must match the loaded App’s id.
A policy path is required exactly for Apps whose auth.rows is policy or tenant.
Each connection configuration contains only a connections array, using the
ordinary connection format. For example, its URL can be ${DATABASE_URL}.
The environment map names variables already set by the operator: the left
side is the name used by this App’s runtime, and the right side is the name to
read from the shell environment. Values are captured at startup. Missing
variables fail startup, and no App changes the process environment. This map
controls runtime credentials; it does not grant App definitions access to
environment variables.
Without a suite identity, authentication needs an explicit auth.store or an XDG_CONFIG_HOME mapping.
SQL URL references and HTTP URL/header references use the App’s captured
environment. OS keychain, credentials-file and database-driver defaults still
use their existing machine configuration; use explicit connection credentials
when deploying Apps with different accounts.
One suite account, explicit App access
Section titled “One suite account, explicit App access”For a new suite, add this top-level block to suite.json:
"identity": { "id": "keystone", "name": "Keystone", "store": "./runtime/identity.db", "grants": "./runtime/app-grants.db", "environment": { "BETTER_AUTH_SECRET": "SUITE_AUTH_SECRET" }}Set SUITE_AUTH_SECRET in the operator environment before starting. The two
SQLite paths are relative to the suite file and must be separate from each
other and every business database, including file aliases and SQLite sidecars.
The suite provisions a new empty grants file and refuses an unrecognized
existing layout. Keep these runtime files across restarts. Apps keep their own
role declarations, policies and tenant memberships, but use the suite identity
store instead of opening their individual auth.store. Shared identity currently
supports email/password and a SQLite identity/grant store; business databases
can still use the supported engines. Existing standalone App auth is unchanged.
Open http://127.0.0.1:4880/_tablewalk/apps (include basePath before
/_tablewalk/apps when configured). Create one account or sign in. A new account
has no App access. Its stable account ID appears in the chooser so an operator
can explicitly bootstrap the first administrator of each App:
tablewalk suite grant suite.json --app lender --subject ACCOUNT_ID --roles admin --revision 0tablewalk suite grant suite.json --app dealer --subject ACCOUNT_ID --roles reader --revision 0Use roles declared by the target App, or its explicit admin role. Each change
prints its new revision and roles. Later changes must supply that current
revision, preventing an old operator command from overwriting a newer grant:
tablewalk suite revoke suite.json --app dealer --subject ACCOUNT_ID --revision 1The subject is an existing suite account ID, never an email address. The same account can be an administrator in Lender and a reader in Dealer; neither role gives any access to Leads. Grants are read again on requests, and an App grant revocation invalidates that App’s existing tenant contexts. A tenant App still requires its own tenant membership after the App grant is established.
An administrator can grant or revoke access only within their own App through
POST <app-mount>/api/suite/grants, using their suite cookie, same-origin request
and x-tablewalk-account header. The body is
{ "subjectId": "ACCOUNT_ID", "roles": ["reader"], "expectedRevision": 0 };
empty roles revokes. The target must already have a suite account. Grant and
audit row commit together, and a stale target or administrator revision refuses
the change. This endpoint does not administer the suite’s native account store
or any other App.
To require the recipient’s acceptance, an App administrator can instead use
POST <app-mount>/api/suite/invitations/issue with the same headers and a body
containing subjectId, roles, expectedRevision and expiresAt (epoch
milliseconds, at most seven days ahead). The recipient must already have a
suite account; use its account ID. The response returns a one-time token and
acceptUrl. Share that link with the recipient, who signs in and explicitly
accepts in the chooser. Only the bound account can accept. Issuing an invitation
does not grant access, and neither a matching email nor possession by another
account does so. The token is stored only as a hash and travels in the link’s
fragment rather than a server query string.
An invitation refuses if expired, consumed or revoked, if the target grant
changed, or if its issuing administrator’s grant changed. Acceptance commits
the App grant, consumption and audit entries together. Revoke a pending
invitation with POST <app-mount>/api/suite/invitations/revoke and
{ "id": "INVITATION_ID" }, under that App’s administrator grant. Account
creation and sign-in remain suite-owned; tenant invitations separately manage
membership within the App. A grant-management screen and email delivery are
not yet provided.
The chooser and GET <basePath>/api/suite/apps list only currently granted Apps.
Signing out from any App or the chooser ends the one suite session everywhere.
An ungranted App still refuses protected reads and writes. There is no automatic
conversion or email-based linking of existing per-App accounts.
Flags and Console preview
Section titled “Flags and Console preview”With the explicit suite identity above, add these top-level fields to
suite.json (use your PostgreSQL control database URL):
"flags": { "store": "postgres://localhost:5432/tablewalk_flags", "environment": "production", "deploymentId": "keystone-production"},"console": { "enabled": true }The control store is separate from business and identity storage. Provision an empty store explicitly before serving:
tablewalk flags provision --suite suite.jsonFor an existing verified version-1 flag store, use the explicit upgrade instead:
tablewalk flags upgrade-v1-to-v2 --suite suite.jsonServing opens and attests the configured store; it never provisions or migrates
it. Keep its suite/deployment binding stable across restarts. Declare the
App flags and scoped role capabilities, review
the authority lock, then start with tablewalk suite suite.json.
For an existing suite account, grant Console admission separately:
tablewalk suite grant suite.json --app tablewalk-console --subject ACCOUNT_ID --roles admitted --revision 0Open http://127.0.0.1:4880/_tablewalk/console for the port used above; prepend
the configured basePath. Console is a private built-in App using the public
sidebar DSL and Sage preset. Its admitted role opens the shell only. The
account also needs a current grant in the target App with its own scoped
flags capabilities; for the DSL example, a first target grant is:
tablewalk suite grant suite.json --app lender --subject ACCOUNT_ID --roles flagOperator --revision 0For an existing grant, supply its current revision and the complete roles to retain. Console admission and another App’s administrator role confer no target flag powers. Tenant targets additionally require current membership in the selected tenant or platform realm.
Choose App, check the displayed environment, then choose a scope. Global means App-wide in that environment, not suite-wide. Tenant scopes are offered only through the target App’s membership owner. The GUI supports reading flag controls, setting a fixed value, killing, viewing history and reverting to a historical revision. Every change needs a reason and the current revision (compare-and-swap); stale changes are refused. Revert writes a new audited revision rather than deleting history. Refetch after a change or conflict before submitting again. The displayed control value is not a per-person evaluation of targeting rules.
- View only: read values and history, with no mutation controls.
- Manage only: set/revert using the allowed type/variants and revision;
current values, owner details and history remain private without
view. - Kill only: disable to the declared safe value; no value/history access, arbitrary value setting or re-enable permission.
Console supports bounded targeting, percentage rollout, history and revert. Configured experiment evidence adds activation, pause/resume, mature-cohort results and explicit winner decisions. See experiment analytics for separate evidence storage, source provisioning and supported identity boundaries.
Serving boundaries
Section titled “Serving boundaries”This initial command binds to 127.0.0.1, using port 4111 unless configured.
It supports ordinary Apps, email/password authentication, row-policy Apps and
registered transactional commands. Commands load from the App’s commands.ts,
commands.mjs or commands.js, using its authority lock and existing command
journal. Provision that journal explicitly before starting; suite startup
attests it and does not migrate it. Each App retains its own command grants,
row-policy checks and receipts, even when commands share a name.
Ordinary Apps can declare a jobs module path beside config. Scheduled jobs
use that App’s registered commands, service-principal grants and captured
connection environment. Provision their command journal and job store before
startup. Workers start only after every App is ready and the suite is listening;
shutdown cancels and drains accepted handlers before closing App stores.
Tenant Apps can be mounted at a sub-App path. They require a tenant
policy and already provisioned tenant storage; sign-in, membership selection
and row contexts stay with that App’s dedicated tenant runtime. Scoped writes
and registered transactional commands use its own grants, policy and command
journal. Writable resources and commands require a writable connection, and
each tenant command needs its policy’s resultReferences extractor. Provision
the journal before startup; a missing journal fails before auth opens. Business and
platform-source locations use the App’s captured environment. Tenant Apps at
the suite root remain refused. Public intake runs only under that tenant App’s enrolled
intake: { address } policy, including saved steps and resume; the address
is resolved on every submission. The suite does not adopt it as an ordinary
App form.
Tenant administration, invitations and audited support use the same App-owned
policy and storage as standalone tenant deployments. Provision their journals,
invitation store and support audit before startup. Map TABLEWALK_AUDIT_KEY
explicitly in each supporting App’s environment; the key stays with that App
through startup, request handling and audit writes. Support still needs a recent
native sign-in and a matching platform membership; an App grant alone cannot
open a support session or administer a tenant.
With shared identity, an invitee first needs a suite account and explicit access to that App. Tenant invitations then grant their own scoped membership. The App projection cannot create global accounts through an invitation claim. Tenant API declarations may coexist with this browser deployment, but the suite does not mount a bearer-token API transport.
Tenant Apps can also declare a jobs module for event-driven jobs. Their
commands may emit the events those jobs consume; ordinary App events remain
unsupported. Provision the tenant event, delivery and job stores explicitly.
The existing tenant job rules still apply: active tenant/platform anchors,
service-principal command grants, no scheduled tenant jobs, and no event-emitting
or lookup commands as job handlers. Jobs start after the shared listener and
all Apps are ready, then cancel and drain before their tenant stores close.
Ordinary App public forms (public: true) are supported when they have no
keyed inputs. Row-policy Apps also support public unkeyed forms that are not
saved on every step, when the policy explicitly enrolls the form under
publicForms with a fixed row scope. Private forms, keyed form inputs, saved
forms on ordinary/row-policy Apps, and tenant forms without public intake
enrollment remain refused. Tenant Apps can serve public unkeyed forms,
including saved forms, through their own intake owner. Row-policy jobs, machine
timers, ordinary command events, all command effects, API tokens, notifications
and other background services are refused at startup. Keystone’s
examples/apps/keystone/start-suite.mjs launcher demonstrates all six Apps with
shared identity, independent grants and tenant-owned intake/support.
Definition changes require a restart; client source changes
retain the shared Vite development transport. Ctrl+C stops the listener, drains
accepted requests and releases every App’s resources.