Reaffirm your app
Gemmein enforces the rules server-side, so your frontend is never the source of truth. That's what lets you move fast — and it's provable. Ship a small script that exercises your boundaries with live calls and fails the build on drift.
If a rule ever stops matching what your UI assumes, you catch it on deploy — not in front of a user. You reaffirm because Gemmein enforces; these checks are never the enforcement themselves.
A ready-to-edit reaffirm.mjs ships inside the npm
package — copy it out of node_modules/@gemmein/sdk/, name your
collections in the CONFIG block, and run it in CI:
PUBLIC_KEY=pk_test_... SECRET_KEY=sk_dev_... node reaffirm.mjs
Tier A — anonymous probes (zero setup, safe against live)
No session needed. Catches the loudest drift:
g.collection("private_notes") // a misnamed collection throws HERE, loudly, in CI
await refuse("anon can't read private", "denied", () => g.collection("private_notes").list())
await refuse("anon can't write community", "denied", () => g.collection("board").create({ x: 1 }))
// and the deliberate reminder: a community/public_read collection IS readable by anyone.
// Client-side "privacy" on a public rule is a leak. Never put secrets in it.
Tier B — cross-user isolation (dev environments only)
Proving "user B genuinely can't read user A's private record" needs two real sessions. Mint them without an OTP round-trip:
const srv = gemmeinServer(process.env.SECRET_KEY) // sk_dev — never in a browser
const a = await srv.testSession("reaffirm-a@test.dev") // { token, expiresAt, user }
const b = await srv.testSession("reaffirm-b@test.dev")
// act as each user via the token:
const A = gemmein(PK, { tokenStore: { get: async () => a.token, set: async () => {}, clear: async () => {} } })
const note = await A.storage.collection("private_notes").create({ probe: "a-secret" })
await refuse("B can't read A's record", "not_found", () => B.storage.collection("private_notes").get(note.id))
testSession() works only in a development
environment. An sk_live key throws
test_session_forbidden_live client-side, and the server refuses live
environments independently — the invariant that keeps scripted sessions off real user
data. Dev and live enforce the same rules, so isolation proven in dev holds in
live.
What to probe
- Every collection your UI writes: its rule refuses who it should refuse.
- The shapes your screens read:
currentUser().userId,record.data.*,record.published. - Cross-user isolation on every
privatecollection that holds anything sensitive. - Add a probe whenever you add a feature — the script grows with the app.