guide

Reaffirm your app

Gemmein enforces the rules on the server, so your frontend is never the source of truth. Because enforcement is server-side, a script of live calls can verify it: reaffirm.mjs ships inside @gemmein/sdk. Copy it next to your app, fill in the CONFIG block, and run it on every deploy.

PUBLIC_KEY=pk_test_... SECRET_KEY=sk_dev_... node reaffirm.mjs

Exit codes: 0 all proven · 1 boundary drift · 2 could not complete, meaning a config or connectivity failure, named as such and never reported as drift. A format-invalid collection name throws at the collection() line; a wrong-but-well-formed name surfaces as unknown_collection and exits 2 with "fix the CONFIG block".

The CONFIG block

PRIVATE_COLLECTION is required. PUBLIC_COLLECTION, DIRECT_COLLECTION, COMMUNITY_COLLECTION and GATED_COLLECTION each unlock more probes; leave any "" and its probes are skipped with the reason printed, so the CI log always says what was proven and what was not. PROBE_FIELD/TEXT_FIELD are your shapes' own field names (development shapes learn from writes, so the probes speak your app's shape). TEST_USERS are three dev test emails.

Tier A: anonymous, safe against live

Read-and-refusal only, no session: an anonymous caller must be refused reading and writing the private collection (denied), and the public collection's exposure is stated with a record count.

Tier B: cross-user isolation, dev environments only

Sessions are created without a sign-in code: gemmeinServer(sk_dev).testSession(email) returns a token you pass to gemmein(pk, { tokenStore }). sk_live throws test_session_forbidden_live, and the server refuses live environments too. Dev and live enforce the same rules, so what is proven in dev holds in live.

What the probes prove:

  • B can't read A's private record; B's list excludes it.
  • currentUser().userId and record.data.* carry the shapes your UI reads.
  • The live-data contract: a plain list carries the watermark to start from; a malformed since is refused (invalid_since); a delta answers the next watermark.
  • A made-up file reference is refused on write (unknown_file).
  • Sealed delivery: the uploader can link() their own file; another user is refused (not_found).
  • On a direct collection: the recipient sees the record, a third user is refused, and a file uploaded { for } one person opens for that person only.
  • On a community collection: HTML is refused (html_not_allowed) and an unpublished draft is invisible to the public.
  • On a gated collection: a user with no plan gets entitlement_required carrying err.requires.

What it costs

Probe records are deleted afterwards; probe uploads remain in dev storage (files have no delete API yet), at two tiny PNGs per configured run. Pointed at a local runtime older than engine 0.4.8, the since/ghost-ref/handed-file probes fail with a message naming the engine as the likely cause; the hosted API is always current.

Add a probe whenever you add a feature. You reaffirm because Gemmein enforces; these checks are never the enforcement themselves.