guide

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 private collection that holds anything sensitive.
  • Add a probe whenever you add a feature — the script grows with the app.