# Embed the editor

> Put the Proof of Thought editor in your page, signed in as your user, sized and styled to fit, and wired to your app.

By the end of this guide, a document opens inside your own page. Your users sign in to your app, not to Proof. The editor runs in an iframe served from `embed.proof.exalto.ai`, and talks to Proof directly over its own socket. Your page tells it which document to open and how it should look, and it tells your page when something happens that your app should know about.

You need a secret test key (`sk_test_…`) from the [console](https://developer.proof.exalto.ai/console), and a document to open. The [quickstart](/quickstart/) creates one.

## How the pieces fit

Your server is the only thing that holds your secret key. When your page wants to show a document, it asks your server for a session token: a short-lived token for one user and one document. The page hands that token to the editor, and the editor uses it to connect to Proof. When the token is about to lapse, the editor asks your page for a new one, and your page asks your server again.

Because the editor is in its own origin, nothing on your page can reach into it. That is what lets Proof tell typed words from pasted ones, and people from scripts. See the [trust model](/concepts/trust-model/) for why that matters.

## Embed a document

<Steps>

1. **Allow your origins.** In the console, under **Embed origins**, or with [`PATCH /v1/platform`](/reference/api/platform/), add each origin your app runs on, such as `https://acme.com`. The editor only loads inside pages on these origins; anywhere else the browser refuses the frame. In test mode, `http://localhost` and `http://127.0.0.1` on any port are allowed without adding them.

2. **Add a session route to your server.** Check your own sign-in first: this route is where your app vouches for who the user is. Then start a session for the user's Proof id, creating the Proof user the first time.

    ```ts title="server.ts"
    import express from "express";
    import Proof from "@exalto/proof";

    const app = express();
    const proof = new Proof(process.env.PROOF_SECRET_KEY);

    // Your user's Proof id, created at Proof and stored on your user the first time.
    async function proofUserId(user) {
      if (user.proofUserId) return user.proofUserId;
      const proofUser = await proof.users.upsert(user.id, { name: user.name, avatar_url: user.avatarUrl });
      await db.users.update(user.id, { proofUserId: proofUser.id });
      return proofUser.id;
    }

    app.post("/proof/session", requireSignIn, async (req, res) => {
      const documentId = String(req.query.doc);

      try {
        const session = await proof.sessions.create({ user: await proofUserId(req.user), document: documentId });
        res.json({ token: session.token, role: session.role });
      } catch (error) {
        if (error.code === "no_access") return res.status(403).json({ error: "no_access" });
        throw error;
      }
    });
    ```

    Pass `group` with the id of the person's own sign-in session with you, and when they sign out, end every Proof session it started with [`proof.users.endSessions`](/reference/api/sessions/#end-a-users-sessions): their open editors stop at once.

    The session's `role` is the user's role in the document: `owner`, `editor`, `suggester` or `viewer`. Proof refuses the session with `no_access` when the user isn't a member and the document's link access doesn't let them in. When someone changes their name or picture in your product, call `proof.users.upsert` again with your id, and every open editor shows the change. See [Sessions](/reference/api/sessions/) for every field, and [Users](/concepts/users/) for how your ids and Proof's fit together.

3. **Mount the editor in your page.** Give it an element, the document id, and a `getToken` function that calls your route.

    ```ts title="editor-page.ts"
    import { mountEditor } from "@exalto/proof-embed";

    const documentId = "doc_8Hq2Lm4R";

    const editor = mountEditor(document.querySelector("#editor"), {
      document: documentId,
      getToken: async () => {
        const response = await fetch(`/proof/session?doc=${documentId}`, { method: "POST" });
        if (!response.ok) throw new Error("No access to this document");
        return (await response.json()).token;
      },
    });
    ```

    The editor calls `getToken` once when it mounts and again two minutes before each token lapses (tokens last 15 minutes). Don't cache tokens yourself.

    If you don't use a bundler, load the script instead. It puts the same functions on `window.Proof`:

    ```html
    <script src="https://embed.proof.exalto.ai/v1.js"></script>
    <div id="editor"></div>
    <script>
      Proof.mountEditor(document.getElementById("editor"), { document: "doc_8Hq2Lm4R", getToken });
    </script>
    ```

4. **Remove it when you leave the page.** In a single-page app, call `editor.destroy()` when the route changes, so the socket closes and the last edits are saved first.

</Steps>

## Size it

The editor sizes itself in one of two ways:

- `height: "auto"` (the default) grows the iframe with the text, so your page scrolls as it would for any long article. Use this when the document is the page.
- `height: "fill"` makes the editor fill its container and scroll inside it. Use this when the document sits in a fixed panel, such as beside a sidebar. Give the container a height.

```ts
mountEditor(panel, { document: documentId, getToken, height: "fill" });
```

Where the frame is too narrow, comment and suggestion cards move into a panel whatever you choose for `comments`.

## Make it look like your app

Appearance is set with options, not CSS: your stylesheets can't reach inside the iframe.

```ts
mountEditor(element, {
  document: documentId,
  getToken,
  colorScheme: "auto", // "light", "dark", or follow the system
  toolbar: "floating", // "header", "floating" or "none"
  comments: "right", // "right", "left" or "hidden"
  rails: "left", // the attribution rail: "left", "right" or "hidden"
});
```

The seal's own colours never change: they mean the same thing on every platform. Keyboard shortcuts such as ⌘K (the editor's command palette) and ⌘P (print) belong to the editor while it has focus, and to your page otherwise.

To change something after mounting, such as the colour scheme when your user flips your own theme switch, call `editor.setOptions({ colorScheme: "dark" })`. When the person changes a display setting inside the editor, it sends a `settings` event with the new options: keep them with your other preferences and pass them back next time.

Your own controls can sit over the editor's header, as a menu button or a notification bell would: keep room for them with `headerStart` and `headerEnd`, in pixels, and add your own commands to the editor's ⌘K palette with `commands`. See [Options](/reference/embed/#options).

## Choose a mode

People with the `editor` or `owner` role write directly by default. Pass `mode: "suggest"` to start them in suggestion mode instead, or switch later:

```ts
editor.setMode("suggest");
```

People with the `suggester` role always suggest, and viewers only read and comment where their role allows. The mode never grants more than the role.

## Wire it to your app

The editor tells your page about the things your app owns. Listen with `editor.on`:

```ts
editor.on("title", ({ title }) => {
  // The document's name, or its first line when it has none.
  documentList.rename(documentId, title);
  document.title = `${title} · Acme Notes`;
});

editor.on("open-document", ({ document: id }) => {
  // Someone followed a link to another of your documents. Open it your way.
  router.push(`/notes/${id}`);
});

editor.on("mention", ({ user }) => {
  // A name was clicked in the text, a comment or the margin.
  router.push(`/people/${user}`);
});

editor.on("save-state", ({ state, unsaved }) => {
  savingIndicator.show(state); // "saving", "saved" or "offline"
  window.onbeforeunload = unsaved ? () => true : null;
});

editor.on("seal", ({ code, url, line }) => {
  // The owner sealed the document from the editor's Seal button.
  toast(`Sealed: ${url}`);
});

editor.on("error", ({ code, message }) => {
  // For example "no_access" after the user was removed from the document.
  if (code === "no_access") router.push("/notes");
});
```

You can also drive the editor from your own buttons:

```ts
editor.focus();
editor.print(); // the editor's own print layout, typeset for PDF
const markdown = await editor.export("markdown"); // or "html", "docx"
editor.openBreakdown(); // the seal's breakdown: where the words came from, sittings, check-ins
editor.openSeal(); // the owner's seal dialog
```

Every option, event and method is in the [embed API reference](/reference/embed/).

## What the editor doesn't do

The iframe is what makes the record trustworthy, so some things stay inside it:

- **No custom content.** You can't add your own block types, or put your own components inside the text. Links, images, tables, checklists and code are built in.
- **No styling beyond the options.** Your CSS doesn't reach in, and the editor doesn't read your fonts.
- **No reaching into the page.** Your scripts can't read the editor's DOM or send it keystrokes. Text your page puts in by other means, such as through the clipboard, counts as pasted.
- **No cookies.** Browsers wall off storage inside third-party frames, so everything rides on the session token. A user who is signed in to your app is signed in to the editor; nothing else is needed.
- **No document list.** The editor shows one document. Lists, folders and search across documents are yours to build, with [`GET /v1/users/{user_id}/documents`](/reference/api/users/).

## Next

- [Share documents](/guides/share-documents/) with members and links.
- [Connect agents](/guides/connect-agents/) so your users can bring Claude or Codex.
- [Seal and verify](/guides/seal-and-verify/) when a document is done.