exp”. The ACL itself — enrolments, roles, payments —
never leaves your database.
The example: a course platform. Teachers author lesson documents (the
creation flow, unchanged); enrolled
students read them. This mirrors how production hosts embed Qirtaas.
Prerequisite: your organization’s signing secret, provisioned alongside
the API key. Like the secret key, it must never reach the browser.
1. Backend — the data model
The first two tutorials needed no host-side model — each user’s embed identity was the whole access story. ACL is where one becomes necessary: before your backend can decide to authorize a read, it has to know which document belongs to which lesson. So store the pointer:qirtaas_doc_id (recorded from onDocumentCreated
when the teacher authors it — the content itself stays in Qirtaas); enrolments
are whatever your ACL already is — the signature flow doesn’t care about its
shape, only that you can answer “may this user read this lesson?“.
2. Backend — a signing helper
The signature is computed locally with your platform’s standard crypto library. The returned shape{ signature, exp } matches the SDK renderer’s
getSignature contract directly, so you can hand the response straight
through.
3. Backend — the gated signature endpoint
This endpoint is where your ACL and Qirtaas meet: authenticate the caller, run the access check, and only then sign. A signature is an unconditional read grant for that document untilexp — the check must happen before it is
minted, because Qirtaas cannot re-ask you.
Alternatively, inline the signature into the response your lesson endpoint
already returns (
{ lesson: …, signature: … }) and save the round trip —
the renderer’s getSignature can resolve from anywhere. A separate endpoint
keeps this tutorial composable.4. Frontend — render with getSignature
Give the renderer getSignature instead of getToken. The renderer sends
the pair as ?sig=&exp= query parameters on its read (signed-URL style, so
cross-origin reads stay CORS-simple):
src/LessonView.tsx
exp.
5. Handle expiry
Unlike embed tokens, signatures are not refreshable: an expired or invalid one is a hard403 { "error": "invalid_signature" }, surfaced
through the renderer’s onError. Two practical consequences:
- Pick a TTL that covers a reading session — an hour is a sensible default. Short TTLs add security margin but expire under long reads.
- Recover by remounting. On the error, fetch a fresh signature and
remount the renderer (bump a
key). If the signature endpoint itself returns403, the user genuinely lost access — show your paywall or enrolment prompt instead.
6. Going further — drafts and publishing
Signatures compose with a two-pointer pattern for draft/publish flows, where the ACL question isn’t just who may read but which version they may read. Give the lesson two document pointers:- Only ever sign
qirtaas_doc_id— the published pointer. Drafts sit indraft_doc_id, which the signature endpoint never signs, so students simply cannot read them: the read gate is the signing decision. - Authoring a new lesson writes the fresh document id into
draft_doc_id; publishing promotes it (qirtaas_doc_id = draft_doc_id; draft_doc_id = "") in one transaction. - Revising a published lesson: the teacher’s frontend clones the live
document with the client’s
duplicateDocumentand registers the clone as the new draft. Students keep reading the stable published version while the teacher edits the clone; publishing promotes it and the old published document becomes an orphan the frontend deletes viadeleteDocument.
Security checklist
- The signing secret lives in backend env/config only — it is as sensitive as the API key.
- Check access before signing, and sign only the specific document the check covered. Never sign ids taken from the request unchecked.
- Signatures grant read-only access to one document — there is no
wildcard signature, so a leaked one is contained to a single document until
its
exp.
