guide

Test your safety rules in CI

Check on every deploy that your safety rules still hold. Gemmein enforces your safety rules on the server, so your frontend is never the source of truth and a script of live calls can check them. That script, reaffirm.mjs, is included in @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: every check passed.
  • 1: an access check failed.
  • 2: the run could not finish because of a config or connectivity problem. It is named as such and never reported as a failed access check.

A collection name with an invalid format throws at the collection() line. A well-formed name that does not exist returns unknown_collection and exits 2 with "fix the CONFIG block".

The CONFIG block

const PRIVATE_COLLECTION   = "notes";  // a collection with the `private` rule (required)
const PUBLIC_COLLECTION    = "";       // a `community`/`public_read` collection, e.g. "board" ("" to skip)
const DIRECT_COLLECTION    = "";       // a `direct` collection: checks recipient scoping and handed files
const COMMUNITY_COLLECTION = "";       // a `community` collection: checks plain text and drafts
const GATED_COLLECTION     = "";       // a collection unlocked by a paid plan: checks paid access
const PROBE_FIELD = "probe";           // a field your private collection's shape allows
const TEXT_FIELD  = "text";            // the text field your direct/community shapes use
const TEST_USERS = ["reaffirm-a@test.dev", "reaffirm-b@test.dev", "reaffirm-c@test.dev"];

PRIVATE_COLLECTION is required. PUBLIC_COLLECTION, DIRECT_COLLECTION, COMMUNITY_COLLECTION and GATED_COLLECTION each add more checks. Leave any of them "" and its checks are skipped with the reason printed, so the CI log always says what was checked and what was not. PROBE_FIELD and TEXT_FIELD are field names from your own collections; Development collections take their shape from writes, so the checks write data shaped like your app's. TEST_USERS are three Development test emails.

Tier A: anonymous access, safe against Live

Reads and refusals only, with no session. An anonymous caller must be refused reading and writing the private collection (denied), and the public collection's exposure is reported with a record count.

Tier B: isolation between people, Development 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. Development and Live enforce the same rules, so what passes in Development holds in Live.

What the checks confirm:

  • B can't read A's private record, and B's list excludes it.
  • currentUser().userId and record.data.* carry the shapes your UI reads.
  • Live updates with since: a plain list returns the watermark to start from, a malformed since is refused (invalid_since), and a delta read returns the next watermark.
  • A made-up file reference is refused on write (unknown_file).
  • Sealed files: the uploader can link() their own file, and another person is refused (not_found).
  • On a direct collection: the recipient sees the record, a third person 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 collection that needs a paid plan: a person with no plan gets entitlement_required carrying err.requires.

What it costs

Test records are deleted afterwards. Test uploads remain in Development storage (the SDK has no call that deletes a file): two tiny PNGs per configured run. On a local runtime older than engine 0.4.8, the since, made-up-file-reference and handed-file checks fail with a message naming the local runtime's version as the likely cause. The hosted API is always current.

Add a check whenever you add a feature.

Was this page helpful?

Last updated