Access control
How rifleDB decides who may read, write, manage and call actions, as implemented today. The normative version (with the pseudo-code) is architecture.md §4–§5; this page explains it with the moving parts in one place.
Secret means access-controlled, not end-to-end encrypted. The server stores and can read every value. rifleDB makes no zero-knowledge claims.
Identities and keys
- Device keys. Each browser (or Node process) has an Ed25519 key, made with WebCrypto, non-extractable, kept in
IndexedDB per server origin (
identity: 'auto'). The user id is derived from the first device key:uid = 'u_, so it stays the same when more devices join. There are no passwords.' + b64url(sha256('rifle/ uid/ v1\n' + rawPub)). slice(0, 22) - Registration happens when a new key signs in with
auth {register: true}. The policy is theregistrationprop of<Users>inserver/:rifle. config. tsx open(the shipped config),invite(needs a claim-invite token in the same message) orclosed. At most 10 registrations per client address per hour. - More devices: an existing device creates a link code (
identity., valid 5 minutes, single use) and the new device redeems it (createLink() identity.); its key joins the same uid. A passkey can also bring a new device in (linkDevice(code) identity., below).recover() - Folder keys belong to a folder and carry a permission (
read,writeoradmin); a folder can have any number of them. They come from<Key>in the config, from the folder's managers (rifle., below) or from an elevated admin (share. createKeyInvite keys.).registerFolderKey - Service keys are folder keys with
authority: server code (for example a Next.js server withrifledb/) can then also write the folder's server-only paths. Mint one withnext/ server pnpm service-key(SDK guide); they are declared in the config. - Keys can expire and be revoked. Revocation, expiry, grant, role and mode changes trigger a revocation sweep:
affected subscriptions end with an
access {path,message right away.code}
Every message on a socket is covered by the session's proven keys (a signature over the server's per-connection nonce,
rifle/); every REST request carries its own signature (Protocol).
Folders and modes
A folder is the unit of access, loading and persistence. Paths resolve to the folder with the longest matching
prefix; folders nest. Static folders come from server/, dynamic ones are created by admins, and every
user gets a set from the <Users> template:
| Folder | Mode | Who |
|---|---|---|
users/ | private | the owner |
users/ | public | everyone reads, the owner writes |
users/ | secret | the owner, after a passkey step-up |
users/ | private, authority: ['**'] | the owner reads; only server code writes |
With open registration in mind, what a user stores is bounded: in users/, / and / at most 1000
keys per folder, values of at most 16 KB, and 1 MB across the three together (E_ beyond; writes that shrink
or delete are always allowed). <Users limits={{ maxKeys, changes these, also for existing
users. users/, written by server code only, keeps the server defaults (10,000 keys of up to 64 KB).
The three modes:
- public: anyone reads, anonymous sessions included. Writes need a grant or key, unless the folder has
publicWrite: true(then anyone writes, rate-limited per address and per session) orpublicWrite: 'own'(anyone writes, but only keys whose first segment below the folder is their own slot — the server-issuedhello.of their connection,slot rifle.in the SDK — or their uid when signed in: presence entries nobody else can move or delete).status. slot - private: the owner, grants and folder keys.
- secret: exactly one principal, and only while elevated by a passkey step-up: the owner's device key for a
user's secret folder, an admin's device key for
system/. Grants, folder and service keys, invites and public actions are ignored. Admins never read users' secret values (they get metadata and a wipe action, below).*
Other folder settings: inherit (grants and keys of ancestors apply; never across a secret folder), grants
(uid:<uid> or role:<role> with a permission), authority (globs of server-only paths), schema (per-path Standard
Schema validation, config folders only), actions, limits (maxKeys, maxValueBytes, and opsPerSec: writes per
second to the folder from all clients together, over every transport and including action calls — E_ with
scope: 'folder'), and fast (in-memory, coalesced, optionally persisted snapshots and TTLs; primitives only).
The decision, in order
For every read, write, manage (keys, grants, mode, subfolders) and action call:
- No folder at the path → refused.
- Server code (the internal session) → allowed.
- Writing a server-only (
authority) path → only an authority service key for that folder, or an elevated admin (E_otherwise). In secret folders only elevated admins, and only underAUTHORITY system/.* - Secret folder → the one principal, elevated; the right principal without elevation gets
E_, anonymous sessionsELEVATION_ REQUIRED E_, everyone elseAUTH_ REQUIRED E_.FORBIDDEN - Elevated admin (the
adminrole proven by a device key, plus an active step-up) → every non-secret folder: read, write, manage, actions. This is what lets the admin explorer use ordinary subscriptions and ops. - Public folder: read allowed; write allowed with
publicWrite: true, and withpublicWrite: 'own'for keys under the caller's own slot or uid (other keys go on to the rules below). - Owner (device key) → allowed.
- Grants and keys: the highest permission from grants (and ancestors' while
inherit) and from the session's proven folder/service keys must reach what the call needs (read,write,adminfor manage, the action'saccess). OtherwiseE_for anonymous sessions,AUTH_ REQUIRED E_for signed-in ones.FORBIDDEN
Signature failures (unknown key, bad signature, key/pub mismatch) all answer the same generic E_;
revocation and expiry are reported only after a signature verified.
Actions
Actions are server-side functions on a folder (<Action name args access rate handler>). access: 'public' lets
anyone call them; otherwise the caller needs that permission on the folder. They run as a transaction: all writes and
events apply together, a throw applies nothing. Arguments are validated with the action's schema, calls are
rate-limited per user (or session) and per address, and results are JSON up to 64 KB. Actions may only write their own
folder. The demo's drop and the playground's reset are examples.
Sharing a folder
Whoever manages a folder — its owner, anyone with an admin grant or folder key on it, and elevated admins —
shares it without admin help, over the socket (rifle., the share. messages) or signed REST
(/, Protocol):
await rifle.share.addGrant(`users/${uid}`, `uid:${friendUid}`, 'read') // a personal grant
await rifle.share.removeGrant(`users/${uid}`, `uid:${friendUid}`) // live subscriptions end at once
const { link, tokenHash } = await rifle.share.createClaimInvite({
folder,
perms: 'write',
maxUses: 5,
ttlMs: 86_400_000,
})
await rifle.share.revokeInvite(tokenHash)
const { link: keyLink, kid } = await rifle.share.createKeyInvite({ folder, perms: 'read', label: 'tablet' })
await rifle.share.revokeKey(kid)The same rules hold as for the admin actions: secret folders cannot be shared (no grants, invites or keys), system/
never, grants and claim invites of config folders are set in the config (folder keys are allowed there). Changes are
audited with the caller as actor and rate-limited (10 a minute per person, 30 per address). rifle.
and createKeyInvite use this API too, unless the client was created with manage (the admin app uses ADMIN_,
the step-up-guarded admin actions).
Invites
Invite links carry everything in the URL fragment (#rifle-invite=…), which browsers never send to servers or
Referer headers. They are created by the folder's managers (above) or elevated admins (invites.,
keys.), and redeemed with rifle..
- Claim invite (preferred): a random token, stored hashed with
maxUses(≤ 10,000) and an expiry (≤ 30 days). Claiming it with a signed request gives the claimer's uid a personal grant, revocable per person. The grant cannot be passed on; the link itself works for whoever holds it until its uses run out or it expires. - Key invite: carries a bearer folder key (private key in the fragment, made by the inviting client; the server only
learns the public half); the SDK imports it non-extractable. Anyone holding the link can use the key and pass it on:
transferability cannot be prevented cryptographically. Revoke the key (
rifle.) to end it.share. revokeKey(kid) - Secret folders refuse both.
Passkeys and step-up
Passkeys (WebAuthn, @simplewebauthn) do not sign people in; device keys do. Passkeys elevate a session for 10
minutes (stepUp()), which secret folders and admin mutations require, and they recover an account on a new device.
- Relying parties come from
RIFLE_(WEBAUTHN [{rpID,); the one used is picked by the request'srpName, origins}] Origin. A passkey only works on its rpID. - First-passkey rule: a user with no passkey yet may register one with a device key alone (that key is already
their strongest credential; audited as
first-passkey). Every further passkey needs an elevated session. At most 16 passkeys per user. - Elevation is bound to one socket (or, over REST, to an elevation token that only works together with a signature by the same device key). A reconnect loses it; the SDK re-subscribes refused scopes after the next step-up. Removing a passkey ends the elevations it granted.
identity.signs in on a new device with a discoverable passkey: the new device's key joins the uid and the session is elevated.recover() - Users list and remove their own passkeys (
rifle./passkeys. list() remove(credId), elevated). Removing the last one is allowed; the first-passkey rule then lets a device key add a new one.
Admins
- Bootstrap. While no admin exists, the server prints a one-time setup token at startup (32 random bytes,
stored hashed, valid 60 minutes, single use; a restart prints a new one;
RIFLE_disables it). The admin app's sign-in page claims the role with it (ADMIN_ SETUP=off admin.), then registers the first passkey under the first-passkey rule. Deployment steps:bootstrap deploy/README.md. - Without elevation an admin reads what grants give the
adminrole:system/andmetrics system/(live dashboards keep running when elevation expires) and the read-only views onops system/(admin tree.,list folders.,get users.,list users.,get keys.,list connections.,list audit.,query secret.,meta data., andmeta metrics.onhistory system/).metrics - Elevated, an admin reaches every non-secret folder (rule 5) and the mutations on
system:folders.,create folders.,update folders.,delete grants.,add grants.,remove keys.,revoke keys.,registerFolderKey invites.,createClaim users.(the last admin cannot lose the role),setRoles users.,resetPasskeys connections.,kick secret.. Config folders (and their grants) are set in the config only;wipe system/cannot be created, changed or deleted through the API; of a user-template folder only the grants can be changed.* - Users' secret folders are never readable by admins:
secret.returnsmeta {exists,andkeys, bytes, lastWrite} secret.deletes the contents.wipe - Stolen device:
users.revokes the stolen device key(s) and removes the passkeys in one audited operation. Resetting passkeys without revoking the device key is not enough: under the first-passkey rule the thief's key could register a new passkey right away.resetPasskeys {uid, revokeDevices: [kid] | 'all'} - Admin actions are audited (actor, action, target, metadata without secrets) in the Redis stream
audit.
Rate limits and address keys
Per-address limits key IPv4 addresses as they are and IPv6 addresses by their /64. Anonymous writes to publicWrite
folders draw on 30/s per session and 200/s per address — fast folders on their own 1000/s per address, enough for
presence of 50 visitors behind one NAT at 20 Hz; beyond it every visitor's updates slow down evenly instead of queueing
(the SDK keeps one unsent write per key and holds back only that folder). One address may hold 64 sockets at once
(RIFLE_). Behind a reverse proxy the server
only sees the proxy's address unless RIFLE_ lists the proxy's network; see
Operations.