> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qirtaas.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Access-control content with signatures

> Gate cross-user document reads behind your own ACL: run the access check on your backend, then mint a per-document HMAC signature for the renderer.

The first two tutorials cover authors reading their own documents (embed
token) and fully public documents (share token). This one covers the case in
between: **another signed-in user reads a document, and *your app* decides
whether they may** — a student viewing a teacher's lesson, a member viewing
club-only content.

Embed tokens can't express this: they are scoped to one identity's *own*
documents, and minting the author's token for a reader would let them edit.
Instead, the renderer accepts a per-document, expiring **HMAC signature** that
your backend computes *locally* — no network call — after running whatever
access check you like:

```
signature = hex(HMAC_SHA256(signing_secret, "<document_id>|<exp>"))
```

Qirtaas holds the same signing secret and recomputes the HMAC to verify. The
signature is your backend's short-lived approval stamp: "this reader may read
this document until `exp`". The ACL itself — enrolments, roles, payments —
never leaves your database.

**The example:** a course platform. Teachers author lesson documents (the
[creation flow](/tutorials/document-creation), 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:

```
lessons(id, course_id, title, qirtaas_doc_id, …)
enrolments(user_id, course_id, …)
```

A lesson row holds its `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.

<CodeGroup>
  ```ts Node theme={null}
  // env: QIRTAAS_SIGNING_SECRET=…
  import { createHmac } from "node:crypto";

  const SIGNING_SECRET = process.env.QIRTAAS_SIGNING_SECRET!;

  export function signRead(docId: string, ttlSeconds = 3600) {
    const exp = Math.floor(Date.now() / 1000) + ttlSeconds;
    const signature = createHmac("sha256", SIGNING_SECRET)
      .update(`${docId}|${exp}`)
      .digest("hex");
    return { signature, exp };
  }
  ```

  ```python Python theme={null}
  # env: QIRTAAS_SIGNING_SECRET=…
  import hashlib, hmac, os, time

  SIGNING_SECRET = os.environ["QIRTAAS_SIGNING_SECRET"]

  def sign_read(doc_id: str, ttl: int = 3600) -> dict:
      exp = int(time.time()) + ttl
      sig = hmac.new(
          SIGNING_SECRET.encode(),
          f"{doc_id}|{exp}".encode(),
          hashlib.sha256,
      ).hexdigest()
      return {"signature": sig, "exp": exp}
  ```
</CodeGroup>

## 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 until `exp` — the check must happen before it is
minted, because Qirtaas cannot re-ask you.

<CodeGroup>
  ```ts Node (Express) theme={null}
  import { signRead } from "./qirtaasSigning";

  app.get("/api/lessons/:id/signature", requireAuth, async (req, res) => {
    const lesson = await db.lessons.findUnique({
      where: { id: Number(req.params.id) },
    });
    if (!lesson) return res.status(404).json({ error: "Lesson not found" });

    // The ACL check — entirely yours. Here: must be enrolled in the course.
    const enrolled = await db.enrolments.findFirst({
      where: { user_id: req.user.id, course_id: lesson.course_id },
    });
    if (!enrolled) return res.status(403).json({ error: "not_enrolled" });

    res.json(signRead(lesson.qirtaas_doc_id)); // { signature, exp }
  });
  ```

  ```python Python (FastAPI) theme={null}
  from .qirtaas_signing import sign_read

  @app.get("/api/lessons/{lesson_id}/signature")
  async def lesson_signature(lesson_id: int, user=Depends(get_current_user)):
      lesson = await db.lessons.find_first(id=lesson_id)
      if lesson is None:
          raise HTTPException(404, "Lesson not found")

      # The ACL check — entirely yours. Here: must be enrolled in the course.
      enrolled = await db.enrolments.find_first(
          user_id=user.id, course_id=lesson.course_id
      )
      if enrolled is None:
          raise HTTPException(403, "not_enrolled")

      return sign_read(lesson.qirtaas_doc_id)  # { signature, exp }
  ```
</CodeGroup>

<Note>
  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.
</Note>

## 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):

```tsx title="src/LessonView.tsx" theme={null}
import { QirtaasRenderer } from "@qirtaas/react";

export function LessonView({ lesson }: { lesson: Lesson }) {
  async function getSignature() {
    const res = await fetch(`/api/lessons/${lesson.id}/signature`);
    if (!res.ok) throw new Error(`signature fetch failed (${res.status})`);
    return res.json(); // { signature, exp }
  }

  return (
    <QirtaasRenderer
      key={lesson.qirtaas_doc_id}
      documentId={lesson.qirtaas_doc_id}
      getSignature={getSignature}
      locale="ar"
      theme="light"
      onError={(code) => console.error("render failed:", code)}
    />
  );
}
```

The student never receives a token, never gets a Qirtaas identity, and can't
write anything — the signature authorizes exactly one document, read-only,
until `exp`.

## 5. Handle expiry

Unlike embed tokens, signatures are **not refreshable**: an expired or
invalid one is a hard `403 { "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
  returns `403`, the user genuinely lost access — show your paywall or
  enrolment prompt instead.

```tsx theme={null}
const [attempt, setAttempt] = useState(0);

<QirtaasRenderer
  key={`${lesson.qirtaas_doc_id}:${attempt}`}
  documentId={lesson.qirtaas_doc_id}
  getSignature={getSignature}
  onError={(code) => {
    if (code === "invalid_signature") setAttempt((n) => n + 1);
  }}
/>
```

## 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:

```
lessons(…, qirtaas_doc_id, draft_doc_id)
```

* **Only ever sign `qirtaas_doc_id`** — the published pointer. Drafts sit in
  `draft_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
  [`duplicateDocument`](/sdk/client#client-methods) and 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 via `deleteDocument`.

```tsx theme={null}
// Teacher clicks "Revise" on a published lesson:
const { id: draftId } = await qirtaas.duplicateDocument(lesson.qirtaas_doc_id);
await fetch(`/api/lessons/${lesson.id}/draft`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ draft_doc_id: draftId }),
});
// …then mount <QirtaasEditor documentId={draftId}> as usual.
```

Note the division of labor holds throughout: document lifecycle
(create/clone/delete) runs on the frontend over the embed channel; your
backend only records pointers and decides what to sign.

## 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`.

Full reference: [Authentication — signatures](/backend/authentication#signatures-cross-user-reads).
