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().userIdandrecord.data.*carry the shapes your UI reads.- The live-data contract: a plain list carries the
watermarkto start from; a malformedsinceis 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
directcollection: the recipient sees the record, a third user is refused, and a file uploaded{ for }one person opens for that person only. - On a
communitycollection: 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_requiredcarryingerr.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.