A small firm's client vault. Staff sign in; sensitive fields (bank, tax id, private notes) are sealed in the browser before they ever reach the database, and revealed only for a user whose id holds a quorum-granted role.
It is tideless: no second login, no separate account, no doken. The signed-in user id is the subject, and a role a quorum granted it is the gate.
| The cohort responding, live, during a seal | ![]() |
| Reveal a field | ![]() |
| The audit trail — including denials | ![]() |
| What the database actually holds | ![]() |
- Encrypted at rest, for real. Every sensitive field is ciphertext the cohort sealed. The Raw
store view is exactly what a
pg_dumpor a stolen database key returns: no plaintext, and no key anywhere to make it readable. - Reads gated by a quorum-granted role, not by the app. Revealing a field asks the server for a
decrypt voucher, issued only if the signed-in user's id holds
vault-reader— a role a quorum granted. The app cannot grant itself one. - The threshold model, made visible. Every seal and reveal shows the cohort responding in real time — one dot per node, lit as each answers ("Sealed · 17 of 20 nodes signed"). No node holds the key, and you can watch that be true.
- An audit trail. Every seal, reveal, denial and access request is logged to an Activity feed — the denials are the proof the gate bites.
- Request access in-app. A user without the role clicks Request access; the app only records the request (it never touches governance), and an administrator grants it through the quorum. It takes effect on the next request.
- Reveals that re-lock. A revealed value offers a copy button and re-hides after 15 seconds and when the drawer closes. Records can be edited (fields are re-encrypted) and deleted.
- No second login. All of it runs off the app's own session; the browser holds no extra credential.
The Activity feed and access requests use a
vault_activitytable. If you set the vault up before that table existed, re-runschema.sql(it iscreate table if not exists, so it is safe) — until then those two features are simply inert; everything else works.
This example is written to be run in production, not just demoed:
- Session in httpOnly cookies. Sign-in happens on the server; the access and refresh tokens are
set as
httpOnly,SameSite=Laxcookies the page's scripts can never read, so an XSS cannot lift the session. The access token is refreshed transparently from the refresh cookie when it expires. SetCOOKIE_SECURE=true(orNODE_ENV=production) behind TLS so the cookies areSecure. - CSRF on every state change. A double-submit token: the server sets a readable
vault_csrfcookie and requires a matchingX-CSRF-Tokenheader on everyPOST. A cross-site page cannot read the cookie to forge the header. - Locked-down headers. A strict Content-Security-Policy (own origin, the crypto SDK, fonts, and
the ORK network — nothing else),
X-Frame-Options: DENY,nosniff, a referrer policy, and HSTS when secure. - Rate limiting on sign-in and on the voucher endpoints (in-memory; put a shared store in front when you run more than one instance).
- Server-to-service auth. In production, authenticate to minidauth with a signing-key
assertion (
MINIDAUTH_CLIENT_NAME+MINIDAUTH_CLIENT_KEY) over TLS rather than the shared bearer token — see../shared/minidauth.js.
There is no demo mode: with nothing configured the app refuses to start, and until you are signed in it shows only a sign-in gate.
Its data model is a shared firm vault by design — any signed-in user lists the record set and,
if they hold vault-reader, can decrypt it. If you need per-user confidentiality instead, scope the
queries to created_by (and add row-level-security policies keyed on the user).
The browser runs the real crypto flows (@tideorg/js) with a guest session key instead of a
doken — the flows fall back to a plain gSessKey when no token is present. What authorises each
operation is a voucher, and the server issues vouchers through
../shared/minidauth.js:
| browser → server | server → minidauth | gate |
|---|---|---|
/api/vault/voucher/sign |
POST /tide/vouchers |
any signed-in user may seal a field |
/api/vault/voucher/decrypt |
POST /vault/voucher {uid, role} |
the user's id must hold the role |
The uid on the decrypt side is taken from the verified server session, never from the browser, so a page cannot voucher a read for another user. See examples/tideless for the bare round trip and its trade-off (minidauth becomes the read authority for these users).
Needs minidauth up with a vendor key, an encrypt policy, and a PUBLIC decrypt policy (voucher-gated; see docs/running.md).
-
Database. Create a Supabase project, run
schema.sqlin the SQL editor, and grant your user id the role through the quorum:curl -sX POST localhost:8081/iga/change-requests/role -H "Authorization: Bearer $ALICE" \ -H 'Content-Type: application/json' -d '{"vuid":"<your user id>","role":"vault-reader","tideless":true}' # $BOB and $CAROL authorize, then commit
-
Configure and start.
npm install cat > .env <<'EOF' SUPABASE_URL=https://<project>.supabase.co SUPABASE_ANON_KEY=<publishable key> SUPABASE_SERVICE_ROLE_KEY=<secret key> MINIDAUTH_URL=https://minidauth.internal # TLS in production # production server-to-service auth (preferred over a shared token): # MINIDAUTH_CLIENT_NAME=vault # MINIDAUTH_CLIENT_KEY=<base64 pkcs8 ed25519 private key> MINIDAUTH_TOKEN=<relying-party token> # dev fallback VAULT_ROLE=vault-reader # NODE_ENV=production # or COOKIE_SECURE=true, behind TLS # TRUST_PROXY=1 # when behind a TLS-terminating proxy EOF npm start # http://localhost:3007
| variable | required | purpose |
|---|---|---|
SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY |
yes | auth (anon) and storage (service role, server-only) |
MINIDAUTH_URL |
yes | where minidauth is reached |
MINIDAUTH_CLIENT_NAME + MINIDAUTH_CLIENT_KEY |
prod | signing-key assertion to minidauth |
MINIDAUTH_TOKEN |
dev | shared bearer token, if not using the assertion |
VAULT_ROLE |
no | the role that gates decrypt (default vault-reader) |
COOKIE_SECURE |
prod | true to set Secure cookies (default: on when NODE_ENV=production) |
TRUST_PROXY |
prod | hop count to trust when behind a proxy |
PORT |
no | default 3007 |




