Skip to main content
In this tutorial you add a notes feature to an existing app: signed-in users write rich documents (Quran, hadith, matn, full Arabic RTL), see them in a list, reopen them, and delete them. The key architectural idea: Qirtaas is the notes backend. Documents are created, listed, and deleted from the frontend over the SDK’s embed-token channel, and each user’s embed identity scopes them to their own documents. Your backend needs exactly one change — a token endpoint — and zero new tables. What you’ll build
  • Backend: one token endpoint (the only backend change).
  • Frontend: a compose page with <QirtaasEditor>, a note list driven by listDocuments(), reopen-to-edit, and delete.
Frontend samples are React; the token endpoint is tabbed Node (Express) and Python (FastAPI). Prerequisites: a qrt_sk_… API key (see the Quickstart) and npm install @qirtaas/react.

1. Backend — add the token endpoint

The editor authenticates every request with a short-lived embed token, and only your backend — holding the secret key — can mint one. Gate the endpoint with your app’s own auth and pass the authenticated user’s id as external_user_id: Qirtaas auto-provisions an identity per distinct id, and that identity is what scopes each user to their own documents (creation, listing, and deletion all happen within it).
// .env: QIRTAAS_API_KEY=qrt_sk_…
app.post("/api/qirtaas-token", requireAuth, async (req, res) => {
  const upstream = await fetch("https://api.qirtaas.io/v1/embed/tokens/", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.QIRTAAS_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ external_user_id: String(req.user.id) }),
  });
  res.status(upstream.status).json(await upstream.json());
});
Whoever can call this endpoint can edit that identity’s documents — always gate it with your own authentication and derive external_user_id from the session, never from the request body.
That’s the entire backend. Everything below is frontend.

2. Frontend — one client, one token fetcher

Create the client once and share it across the feature — the same getToken powers the editor and the list/delete operations:
src/qirtaas.ts
import { createQirtaasClient } from "@qirtaas/react";

export async function getToken(): Promise<string> {
  const res = await fetch("/api/qirtaas-token", { method: "POST" });
  if (!res.ok) throw new Error(`token fetch failed (${res.status})`);
  const { token } = await res.json();
  return token;
}

export const qirtaas = createQirtaasClient({ getToken });
The SDK calls getToken on init, proactively before the token’s 1-hour expiry, and once more after a 401 — so it should simply mint fresh every call.

3. Frontend — the compose page

Mount <QirtaasEditor> without a documentId: the SDK lazily creates the document on the first keystroke and hands you the new id via onDocumentCreated. There is nothing to persist yourself — the document is already stored under the user’s identity, and autosave keeps it current as they type.
src/ComposeNote.tsx
import { QirtaasEditor } from "@qirtaas/react";
import { getToken } from "./qirtaas";

export function ComposeNote({ onCreated }: { onCreated: (id: string) => void }) {
  return (
    // The editor fills its container — give it a bounded height.
    <div style={{ height: "70vh" }}>
      <QirtaasEditor
        getToken={getToken}
        locale="ar"
        theme="light"
        // e.g. update the route from /notes/new to /notes/<id>
        onDocumentCreated={onCreated}
        onSaveStateChange={(state) => console.log("save:", state)}
      />
    </div>
  );
}
Use onDocumentCreated for navigation concerns (swap /notes/new for /notes/<id> so a refresh reopens the same note) and onSaveStateChange for a saved/saving badge.

4. Frontend — list the notes

listDocuments() returns the identity’s documents with server-derived titles (the first words of the content), so the list is directly displayable — no table of your own, no title field to maintain:
src/NoteList.tsx
import { useEffect, useState } from "react";
import type { DocumentSummary } from "@qirtaas/react";
import { qirtaas } from "./qirtaas";

export function NoteList({ onOpen }: { onOpen: (id: string) => void }) {
  const [notes, setNotes] = useState<DocumentSummary[]>([]);

  useEffect(() => {
    qirtaas.listDocuments().then(setNotes);
  }, []);

  return (
    <ul>
      {notes.map((note) => (
        <li key={note.id} onClick={() => onOpen(note.id)}>
          {note.title || "Untitled"}
          <time>{new Date(note.updated_at).toLocaleDateString()}</time>
        </li>
      ))}
    </ul>
  );
}
Render the list with your own markup — it’s plain data. Mount a Qirtaas embed only on the compose/read page, not per list item.

5. Frontend — reopen and keep editing

To continue an existing note, pass its id as documentId. The component mounts the editor once, so when the user switches notes, force a remount with a key:
src/EditNote.tsx
import { QirtaasEditor } from "@qirtaas/react";
import { getToken } from "./qirtaas";

export function EditNote({ docId }: { docId: string }) {
  return (
    <div style={{ height: "70vh" }}>
      <QirtaasEditor
        key={docId}
        documentId={docId}
        getToken={getToken}
        locale="ar"
        theme="light"
      />
    </div>
  );
}
Because the embed token is minted for the same external_user_id, the owner can reopen and edit any of their documents — no extra grant needed.

6. Frontend — delete a note

import { qirtaas } from "./qirtaas";

async function deleteNote(id: string) {
  await qirtaas.deleteDocument(id);
  setNotes(await qirtaas.listDocuments()); // refresh the list
}
The whole loop — create, list, edit, delete — never touched your backend again after step 1.

When you outgrow this

The zero-model setup carries you as far as “each user manages their own documents”. Add a table of your own the moment a document needs your data attached to it:
  • Host metadata — covers, tags, summaries, or a curated ordering that Qirtaas doesn’t store. Keep a row per note holding the qirtaas_doc_id pointer plus your fields; the content itself stays in Qirtaas.
  • Cross-user access — letting other users read a document requires your backend to know which document belongs to what, so it can decide whom to authorize. That’s the pointer-table pattern, covered in Access control.

Where you are

Users can author, list, reopen, and delete rich documents, and your backend grew by one endpoint. Next:

Document reading

Show these notes read-only with the renderer.

Access control

Let other users read them, gated by your own ACL.