Several Apps in one server
Serve several independently configured Apps from one process, each with its own connections and grants, optionally behind one sign-in.
{ "port": 4880, "apps": [ { "id": "lender", "directory": "./lender", "config": "./lender-connections.json", "mount": "/", "environment": { "BETTER_AUTH_SECRET": "LENDER_AUTH_SECRET", "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", "DATABASE_URL": "DEALER_DATABASE_URL" } } ]}npx tablewalk suite suite.json # http://127.0.0.1:4880/What you’ll see
Section titled “What you’ll see”- Each App at its
mount, answering as it does when served alone: its own grants, row policy, commands, jobs and receipts. - With a suite
identity, one account and one sign-in across Apps, and a chooser at/_tablewalk/appslisting only the Apps the account is granted. - After sign-in, the suite’s address takes each account to its App (below); one with several Apps and no rule picks from the chooser.
- A refusal at startup, naming the App and the reason, for anything a suite cannot serve safely (the table below).
When to use it
Section titled “When to use it”- Use a suite on one machine for a family of Apps that share a host: a desk app, a partner portal and a public site.
- Use one server per App instead when each scales or deploys on its own (build and deploy).
- Use
--apptwice on one server instead when the Apps share one config and neither signs people in.
Options
Section titled “Options”| Key | Does | Default |
|---|---|---|
apps[].id, directory |
The App, relative to the suite file; id must match the App’s. A build is served as built |
required |
apps[].config |
A file holding only connections, in the usual format |
required |
apps[].mount |
Where it is served; one App may take / |
required |
apps[].environment |
This App’s variable → the shell variable it is read from; captured at start | none |
apps[].commands |
Its commands module, as serve --commands; for a build, its own server/commands.mjs |
commands.ts beside the App |
apps[].policy, jobs |
The row or tenant policy (required when auth.rows is policy or tenant), and a jobs module |
none |
identity |
One account and sign-in across Apps (below) | each App signs in alone |
port, basePath |
The listener, on 127.0.0.1 |
4111, none |
The contract behind each key, and what every kind of App keeps in a suite, is
in docs/suites.md.
One account across Apps
Section titled “One account across Apps”Add a top-level identity, and set SUITE_AUTH_SECRET before starting:
"identity": { "id": "keystone", "name": "Keystone", "store": "./runtime/identity.db", "grants": "./runtime/app-grants.db", "environment": { "BETTER_AUTH_SECRET": "SUITE_AUTH_SECRET" }}To keep nothing on the suite’s disk, make store and grants both
postgres:// URLs (a ${NAME} is read from the environment), two separate
databases:
"store": "${SUITE_IDENTITY_URL}", "grants": "${SUITE_GRANTS_URL}"Provision the identity database once with migratePostgresAuthStore(url)
from tablewalk/commands, as for an App’s auth.store; the grants database
is set up at the first start. Every App in such a suite must use PostgreSQL
connections too.
It also takes providers (default ["email"]), signUp (default "open"),
services, the oidc and saml connections those providers name
(identity providers), and landing (below). An
App’s own auth.providers must be among the suite’s.
A new account has no App access. Its account ID shows in the chooser; grant the first administrator of each App from the command line, passing the current revision so an old command never overwrites a newer grant:
tablewalk suite grant suite.json --app lender --subject ACCOUNT_ID --roles admin --revision 0tablewalk suite revoke suite.json --app lender --subject ACCOUNT_ID --revision 1The subject is an account ID, never an email. The same account can be an admin in one App and a reader in another; neither gives anything in a third. After that, an App’s administrator grants and invites within that App.
Where people land
Section titled “Where people land”landing sends a signed-in account from the suite’s address to the first App
whose rule it meets: a role it holds there, or ("member": true) a membership
in one of a tenant App’s tenants. A rule with neither is the default, last.
"landing": [ { "app": "lender", "roles": ["underwriter", "funder"] }, { "app": "leads", "member": true }, { "app": "customer" }]An account’s own saved default App comes first. With no rule met: the root App, if the account has it; the only App it has; else the chooser, which says so when it has none, beside Sign out of the suite.
A link into an App survives sign-in at that App’s door. A page of an App the signed-in account was not granted opens the chooser instead, naming that App; signing in there as an account that has it opens the page. Only a page of a suite App is followed, never another site, an API or the chooser itself. Signing out of any App signs out of all of them.
Flags and Console
Section titled “Flags and Console”Preview. The Console (targeting, rollouts, the Operations and Analytics rooms and Governance) and feature flags work as documented and may change in any 1.x release. See status.
With shared identity, add a flag control store and the Console:
"flags": { "store": "postgres://localhost:5432/tablewalk_flags", "environment": "production", "deploymentId": "keystone-production" },"console": { "enabled": true }tablewalk flags provision --suite suite.json # once, before servingtablewalk migrate stores up --suite suite.json # after an upgradetablewalk suite grant suite.json --app tablewalk-console --subject ACCOUNT_ID --roles admitted --revision 0The control store is its own database; serving only attests it. Open
http://127.0.0.1:4880/_tablewalk/console. admitted opens the shell only:
the account also needs a role with scoped flags capabilities in the target
App (feature flags). The Console reads, sets,
kills and reverts flags, with a reason and the current revision on every
change, and runs experiments. Edits to a flag are staged
and reviewed as a list of changes before they are applied. ⌘K (Ctrl K) jumps
to a flag, a job, an App or a room; j and k move through a list.
Operations and Analytics rooms
Section titled “Operations and Analytics rooms”Console also has an Operations room and an Analytics room. Each answers
App by App from that App’s own operations grant, never from Console admission:
operator: { read: true, operations: ['jobs', 'deliveries', 'workflows', 'stores', 'analytics'] },'jobs'and'deliveries': one list of work, opened on what needs attention. A row shows its attempts, and dead work can be redriven.'workflows': workflow instances and their steps, with cancel and retry.'stores': each store’s layout version, and any upgrade pending. Databases are named, never addressed.'analytics': events by day and simple breakdowns, each event on its own, and the App’s declared funnels as steps. It is read-only and sends counts only: no email, IP, field value or visitor id.
Every change records the signed-in account as its operator, and a grant revoked mid-session refuses the next change. A grant in one App never reaches another App’s queues. Tenant Apps are not listed yet.
Governance room
Section titled “Governance room”Governance opens for an account holding the App’s own admin role, and
changes nothing:
- Audit log: every command from the App’s journal — who, when, which record (linked) and which fields changed, never their values — and every role change. Search by email or account id, command or resource, and dates.
- Authority: the App’s authority.lock as sentences (“Role
agentcan updateincident.state”), grouped by role, and what changed since the App last started with a different one. With shared identity, the suite keeps each start’s authority beside the identity store. - Accounts: who holds which role, and every change to it. Roles are changed on the suite’s access page, which it links to.
A tenant App opens here for an account its own tenant administration grants,
and only to Accounts: the tenants that grant names and, for one, the
members whose roles it reaches. Members are changed in the App’s own tenant
administration, and Support sessions (on the platform context, for a platform
role with operations: ['support']) is a screen in the App; the room links to both.
What a suite refuses
Section titled “What a suite refuses”| Refused at startup | Why |
|---|---|
A tenant App at / with shared identity |
It would answer the suite’s sign-in and chooser routes |
api with shared identity, publicOrigin or hosts, or on a tenant App |
API tokens live in an App’s own sign-in store, and a suite serves the API on loopback only |
An App’s own oidc or saml service, or a provider the suite does not list |
The suite’s sign-in is the only one |
| Two Apps naming different caches | One budget and one lockout per person |
| App analytics beside suite analytics, or on a tenant App | One analytics owner per App |
| Jobs on a row-policy App | Its row policy owns the App’s commands |
Common mistakes
Section titled “Common mistakes”environment variable LENDER_AUTH_SECRET is not set→ the right side ofenvironmentnames a shell variable; export it before starting.- A signed-in account sees no Apps → a new account has no access; grant it
with
tablewalk suite grant. - A
landingrule never matches → the account needs a current grant in that App; a rule’srolesare that App’s role names, checked at startup. tablewalk suite grantrefuses a stale revision → pass the revision the last change printed.- The suite refuses to start on a command journal or flag store → provision stores first; serving only attests them.
Related
Section titled “Related”- Build and deploy: one App per server, behind a proxy.
- Identity providers:
oidcandsamlsign-in and roles from claims. - Experiment analytics: evidence the Console reads.
- Test an App:
startTestSuitefor a disposable suite.