Skip to content

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.

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:

Terminal window
tablewalk suite grant suite.json --app lender --subject ACCOUNT_ID --roles admin --revision 0
tablewalk suite grant suite.json --app dealer --subject ACCOUNT_ID --roles reader --revision 0

Use 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:

Terminal window
tablewalk suite revoke suite.json --app dealer --subject ACCOUNT_ID --revision 1

The 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.

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:

Terminal window
tablewalk flags provision --suite suite.json

For an existing verified version-1 flag store, use the explicit upgrade instead:

Terminal window
tablewalk flags upgrade-v1-to-v2 --suite suite.json

Serving 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:

Terminal window
tablewalk suite grant suite.json --app tablewalk-console --subject ACCOUNT_ID --roles admitted --revision 0

Open 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:

Terminal window
tablewalk suite grant suite.json --app lender --subject ACCOUNT_ID --roles flagOperator --revision 0

For 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.

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.