---
title: "Slides: Developer Guide"
description: "The Slides data model, persistence model, full action inventory, routes, and where to make common changes when customizing or extending the template."
---

# Developer Guide

This page is for anyone customizing the Slides template or extending it: the SQL data model, how concurrent writers are kept safe, every agent-callable action, the route map, and where to make common changes.

## Quick start

```bash
npx --yes @agent-native/core@latest create my-slides --standalone --template slides
```

See [Getting started](/docs/getting-started) for the rest of the setup (`cd`, `pnpm install`, `pnpm dev`) and the framework fundamentals.

## Persistence model

Each deck is one JSON blob in `decks.data`. Every write goes through a server-side read-modify-write action that holds a per-deck lock, so concurrent writers, a human and the agent, or two humans, touching different slides of the same deck never overwrite each other's work.

- **Agent actions** (`add-slide`, `update-slide`) use their own dedicated, granular actions and share that same lock.
- **The browser editor** enqueues granular ops (`patch-slide`, `delete-slide`, `reorder-slides`, `add-slide`, `patch-deck-fields`) through `patch-deck` instead of a full-deck PUT.

<Callout id="doc-block-slides10" tone="warning">

If you're extending the editor's save path, enqueue a new op through `patch-deck`. Do not add a new full-deck PUT. That would reintroduce the last-write-wins race the lock exists to prevent.

</Callout>

## Data model

All deck data lives in SQL via Drizzle ORM. Schema: `templates/slides/server/db/schema.ts`.

<DataModel
  id="doc-block-slides7"
  title="Slides data model"
  summary={
    "A deck owns its slides as JSON in decks.data. Comments, versions, shares, and design systems hang off it."
  }
  entities={[
    {
      id: "decks",
      name: "decks",
      note: "Slides live as JSON in data, with ownableColumns",
      fields: [
        {
          name: "id",
          type: "text",
          pk: true,
          note: "e.g. deck-1712345-abc",
        },
        {
          name: "title",
          type: "text",
        },
        {
          name: "data",
          type: "text",
          note: "JSON: { title, slides: [{ id, content, layout }] }",
        },
        {
          name: "design_system_id",
          type: "text",
          nullable: true,
        },
        {
          name: "created_at",
          type: "text",
        },
        {
          name: "updated_at",
          type: "text",
        },
      ],
    },
    {
      id: "slide_comments",
      name: "slide_comments",
      fields: [
        {
          name: "id",
          type: "text",
          pk: true,
        },
        {
          name: "deck_id",
          type: "text",
          fk: "decks.id",
        },
        {
          name: "slide_id",
          type: "text",
          note: "Slide the comment lives on",
        },
        {
          name: "thread_id",
          type: "text",
          note: "Threading",
        },
        {
          name: "parent_id",
          type: "text",
          nullable: true,
        },
        {
          name: "content",
          type: "text",
        },
        {
          name: "quoted_text",
          type: "text",
          nullable: true,
        },
        {
          name: "author_email",
          type: "text",
        },
        {
          name: "author_name",
          type: "text",
          nullable: true,
        },
        {
          name: "resolved",
          type: "boolean",
        },
      ],
    },
    {
      id: "deck_versions",
      name: "deck_versions",
      note: "Point-in-time snapshots for restore",
      fields: [
        {
          name: "id",
          type: "text",
          pk: true,
        },
        {
          name: "deck_id",
          type: "text",
          fk: "decks.id",
        },
        {
          name: "title",
          type: "text",
        },
        {
          name: "data",
          type: "text",
          note: "Full deck JSON",
        },
        {
          name: "change_label",
          type: "text",
          nullable: true,
        },
      ],
    },
    {
      id: "design_systems",
      name: "design_systems",
      note: "Reusable brand tokens, with ownableColumns",
      fields: [
        {
          name: "id",
          type: "text",
          pk: true,
        },
        {
          name: "title",
          type: "text",
        },
        {
          name: "description",
          type: "text",
          nullable: true,
        },
        {
          name: "data",
          type: "text",
          note: "colors / typography / spacing",
        },
        {
          name: "assets",
          type: "text",
          nullable: true,
        },
        {
          name: "custom_instructions",
          type: "text",
        },
        {
          name: "is_default",
          type: "boolean",
        },
      ],
    },
    {
      id: "deck_share_links",
      name: "deck_share_links",
      note: "Persisted public share-link snapshots",
      fields: [
        {
          name: "token",
          type: "text",
          pk: true,
        },
        {
          name: "title",
          type: "text",
        },
        {
          name: "slides",
          type: "text",
          note: "JSON slides snapshot",
        },
        {
          name: "aspect_ratio",
          type: "text",
          nullable: true,
        },
        {
          name: "created_at",
          type: "text",
        },
      ],
    },
  ]}
  relations={[
    {
      from: "decks",
      to: "slide_comments",
      kind: "1-n",
      label: "comments",
    },
    {
      from: "decks",
      to: "deck_versions",
      kind: "1-n",
      label: "snapshots",
    },
    {
      from: "design_systems",
      to: "decks",
      kind: "1-n",
      label: "applied to",
    },
  ]}
/>

Framework shares tables (`deck_shares`, `design_system_shares`) map principals, users or orgs, to viewer / editor / admin roles per resource, and are created via the shared `createSharesTable` helper rather than hand-written.

#### Slide structure

Each slide inside `decks.data` is:

```json
{
  "id": "slide-1",
  "layout": "title",
  "content": "<div class=\"fmd-slide\" style=\"...\">...</div>"
}
```

`content` is raw HTML. The renderer provides the fixed aspect ratio and background, and the HTML provides everything inside. Rich embedding is supported too: hand-drawn diagrams and live-rendered charts can live inside a slide alongside plain HTML.

## Action inventory

### Deck lifecycle

- `create-deck` — new deck, optionally pre-populated with slides, or an atomic bulk replace of an existing deck's slides. Omit `slides` or pass `[]` for an empty deck. The action opens the new deck through application state.
- `get-deck` — full deck JSON, or a compact slide-summary view
- `list-decks` — every deck the caller can see, with light/compact/full-slide projections for cheap polling
- `duplicate-deck` — copy a deck with fresh slide ids
- `update-deck-aspect-ratio` — 16:9, 1:1, 9:16, or 4:5

`add-deck`, `save-deck`, and `delete-deck` back the browser editor's optimistic-create, undo/redo, and delete flows. The agent uses `create-deck`, `add-slide`/`update-slide`/`patch-deck`, and the framework's own sharing actions instead.

### Access requests

- `request-deck-access`, `approve-deck-access-request`, `get-deck-access-status` — the private-deck access-request flow: a visitor without access requests it, the owner approves or denies from the app or the `/access-request/approve` route, and either side can poll the current status

### Slide edits

- `add-slide` — append (or insert at a position) one slide. Call it sequentially after an empty `create-deck` so the editor can show progress.
- `update-slide` — surgical find/replace or full-content slide edit
- `patch-deck` — the granular op queue the browser editor uses instead of a full-deck PUT
- `get-layout-overflows` — read browser-measured slide-fit results, so an edit that no longer fits its layout can be caught and revised

### History

- `list-deck-versions`, `get-deck-version`, `restore-deck-version` — every write snapshots first, so any point in time can be listed and restored

### Comments

- `list-slide-comments`, `add-slide-comment`, `update-slide-comment`, `delete-slide-comment`

### Import

- `import-pptx`, `import-docx`, `import-google-doc` — dedicated single-format importers
- `import-google-slides-reference` — import a Google Slides deck as a reusable reference deck, rather than pulling its content into the working deck
- `import-file` — one entry point for PPTX, DOCX, or PDF, or a Figma `.fig` file (which routes into Builder design-system indexing instead of a slide import)
- `extract-pdf` — CLI-only raw text dump of a PDF, mostly for local debugging

### Export

- `export-pptx` — native, editable PowerPoint
- `export-google-slides` — generates the same PPTX plus a ready Google Slides import link
- `export-html` — a standalone, self-contained HTML file with built-in keyboard navigation

### Design systems

- `create-design-system`, `update-design-system`, `delete-design-system`, `get-design-system`, `list-design-systems`, `set-default-design-system`, `apply-design-system` (link one to a deck)
- `get-workspace-defaults`, `set-workspace-defaults` — the workspace-wide default deck and design system. Setting one is admin-only
- `analyze-brand-assets`, `import-document`, `import-from-url`, `import-code` — gather raw brand signals (website CSS/fonts, uploaded document or code file contents) for a **local**, agent-drafted design system
- `index-design-system-with-builder`, `sync-design-system-with-builder`, `import-design-project` — the Builder-indexed path for Figma files, connected code/GitHub repos, and `design.md`. `import-design-project` starts from an existing design system rather than a raw source

### Creative Context

- `clone-context-slide` — insert one pinned Library slide into a deck unchanged, without regenerating its HTML/CSS
- `clone-creative-context-deck` — recreate one exact governed deck version
- `get-deck-reference-context` — extract an existing deck's visual language as a reusable pattern library
- Retrieval and Library submission (search, fetching a context item, managing Library membership) are shared framework actions the agent calls the same way in every app that supports Creative Context. See the `creative-context` skill for the retrieval procedure and reuse ladder.

### Escalation and staged data

- `provider-api-catalog`, `provider-api-docs`, `provider-api-request` — raw Google Drive API access when a canned import/export action is too narrow. Slides resolves auth from the user's connected Google Docs account
- `list-staged-datasets`, `query-staged-dataset`, `delete-staged-dataset` — aggregate a large staged provider result without re-fetching it

### Navigation and context

- `view-screen` — what the user is currently looking at: open deck, current slide, and any active visual selection
- `navigate` — move the UI to a deck, slide, or view

## Routes reference

| Route                            | Renders                                                                |
| -------------------------------- | ---------------------------------------------------------------------- |
| `/`                              | Public marketing/SEO landing page                                      |
| `/home`                          | Deck list, the private app home                                        |
| `/deck/:id`                      | The editor                                                             |
| `/deck/:id/present`              | Full-screen presentation                                               |
| `/p/:id`                         | Public/agent-readable live deck preview, distinct from `/share/:token` |
| `/share/:token`                  | Public read-only snapshot, frozen at share time                        |
| `/slide`                         | Single-slide embed used for chat previews                              |
| `/design-systems`                | Manage saved design systems                                            |
| `/access-request/approve`        | Approve a request for access to a private deck                         |
| `/agent`, `/extensions`, `/team` | Redirect into the relevant Settings page                               |

## Customize it

The Slides template is fully customizable. Key places to look when extending it:

#### Actions — `templates/slides/actions/`

Every agent-callable operation lives here as a TypeScript file, mounted automatically at `POST /_agent-native/actions/:name` and callable from the CLI as `pnpm action <name>`. Add a new file here to give the agent a new capability.

<AnnotatedCode
  id="doc-block-slides9"
  title="Anatomy of a slide edit action"
  filename="actions/update-slide.ts (simplified)"
  language="ts"
  code={
    'import { defineAction, fail } from "@agent-native/core/action";\nimport { assertAccess } from "@agent-native/core/sharing";\nimport { eq } from "drizzle-orm";\nimport { z } from "zod";\n\nimport { getDb, schema } from "../server/db/index.js";\nimport { notifyClients } from "../server/handlers/decks.js";\nimport { withDeckLock } from "./patch-deck.js";\n\nexport default defineAction({\n  description:\n    "Surgically edit a slide\'s content using search-replace or full replacement. " +\n    "Syncs live to open editors. Prefer this over full deck rewrites.",\n  schema: z.object({\n    deckId: z.string(),\n    slideId: z.string(),\n    find: z.string().optional(),\n    replace: z.string().optional(),\n    fullContent: z.string().optional(),\n  }),\n  run: async ({ deckId, slideId, find, replace, fullContent }) => {\n    await assertAccess("deck", deckId, "editor");\n    return withDeckLock(deckId, async () => {\n      const db = getDb();\n      const [row] = await db\n        .select()\n        .from(schema.decks)\n        .where(eq(schema.decks.id, deckId));\n      if (!row) {\n        fail(`Deck ${deckId} not found`, { errorCode: "deck_not_found", statusCode: 404 });\n      }\n\n      const deck = JSON.parse(row.data);\n      const slide = deck.slides.find((s: { id: string }) => s.id === slideId);\n      if (!slide) {\n        fail(`Slide ${slideId} not found`, { errorCode: "slide_not_found", statusCode: 404 });\n      }\n\n      if (fullContent) {\n        slide.content = fullContent;\n      } else if (find) {\n        const idx = slide.content.indexOf(find);\n        if (idx === -1) {\n          fail("Text not found", { errorCode: "slide_find_no_match" });\n        }\n        slide.content =\n          slide.content.slice(0, idx) +\n          (replace ?? "") +\n          slide.content.slice(idx + find.length);\n      }\n\n      deck.updatedAt = new Date().toISOString();\n      await db\n        .update(schema.decks)\n        .set({ data: JSON.stringify(deck), updatedAt: deck.updatedAt })\n        .where(eq(schema.decks.id, deckId));\n\n      notifyClients(deckId, { slideId, actor: "agent" });\n      return { ok: true, deckId, slideId };\n    });\n  },\n});'
  }
  annotations={[
    {
      lines: "22",
      label: "Access check",
      note: "`assertAccess` throws before anything else runs if the caller isn't at least an editor on this deck.",
    },
    {
      lines: "23",
      label: "Per-deck lock",
      note: "`withDeckLock` serializes this against every other write to the same deck. `add-slide` and the browser's `patch-deck` share the same lock, so two writers touching different slides never clobber each other.",
    },
    {
      lines: "29-37",
      label: "Fail loud, don't return quietly",
      note: '`fail(...)` throws a structured error instead of returning a plain value. This action used to return `{ ok: false, message: "Text not found" }` for a bad match, which a caller could too easily read as success. Throwing is the only channel that reliably says the deck was not modified.',
    },
    {
      lines: "39-51",
      label: "Surgical edit",
      note: "A `find`/`replace` pair changes only the matched text, and now fails loudly on no match too. `fullContent` swaps the whole slide. Prefer the narrower form for real edits.",
    },
    {
      lines: "53-57",
      label: "SQL is the source of truth",
      note: "The whole deck is one JSON blob in `decks.data`. There's no separate `slides` table, so every write reads, edits, and rewrites that one column.",
    },
    {
      lines: "59",
      label: "Live sync",
      note: "`notifyClients` pushes the change to any open editor immediately. Polling is the fallback path. See [Real-Time Collaboration](/docs/real-time-collaboration).",
    },
  ]}
/>

The real action also validates that exactly one edit mode was provided, snapshots a version before writing, and writes with an optimistic-concurrency check so a stale read can never silently overwrite a newer one, on top of recording Creative Context reuse provenance and waiting briefly for the editor to report a layout-overflow measurement. This example trims all of that down to the core access, lock, read-modify-write, notify shape.

#### Layout

<FileTree
  id="doc-block-slides8"
  title="templates/slides/ shape"
  entries={[
    {
      path: "actions/create-deck.ts",
      note: "New deck, optionally pre-populated with slides, or an atomic bulk replace",
    },
    {
      path: "actions/add-slide.ts",
      note: "Append one slide, used for streaming generation",
    },
    {
      path: "actions/update-slide.ts",
      note: "Find/replace or full-content slide edit",
    },
    {
      path: "actions/patch-deck.ts",
      note: "Granular ops the browser editor uses instead of a full-deck PUT",
    },
    {
      path: "actions/index-design-system-with-builder.ts",
      note: "Starts Builder design-system indexing from Figma, GitHub, or code",
    },
    {
      path: "app/routes/deck.$id.tsx",
      note: "The deck editor",
    },
    {
      path: "app/routes/deck.$id_.present.tsx",
      note: "Full-screen presentation mode",
    },
    {
      path: "app/routes/agent.tsx",
      note: "Full-page Agent tab",
    },
    {
      path: "app/routes/p.$id.tsx",
      note: "Public/agent-readable single-deck preview",
    },
    {
      path: "app/routes/share.$token.tsx",
      note: "Public read-only snapshot page",
    },
    {
      path: "app/components/editor/",
      note: "Slide canvas, toolbar, sidebar, and panels",
    },
    {
      path: "server/db/schema.ts",
      note: "decks, slide_comments, deck_versions, design_systems, deck_share_links",
    },
    {
      path: ".agents/skills/create-deck/SKILL.md",
      note: "Exact slide HTML templates for every layout",
    },
    {
      path: ".agents/skills/creative-context/SKILL.md",
      note: "The reuse ladder and retrieval procedure",
    },
  ]}
/>

Most editor UI customization happens under `app/components/editor/`: the slide canvas, toolbar, sidebar, bubble menu, slash menu, and the history panel.

#### Skills — `templates/slides/.agents/skills/`

Agent skills that explain patterns when the agent needs to modify code:

- `create-deck` — how to create a new deck, with exact slide HTML templates for every layout
- `slide-editing` — how to edit individual slides
- `deck-management` — how decks are stored and accessed
- `design-systems` — design-system data shape and how tokens apply to generated slides
- `creative-context` — the reuse ladder, retrieval procedure, and Library submission

#### AGENTS.md

`templates/slides/AGENTS.md` is the short router the agent reads on every conversation. It points at the skills under `.agents/skills/` and lays out the core rules, persistence model, and application-state contract. Update the relevant skill whenever you add or change a slide layout or design-system source.

## What's next

- [**Features**](/docs/template-slides-features) — the user-facing editing, presenting, sharing, design-system, and import/export flow this data model backs
- [**Talk to the Agent**](/docs/template-slides-agent) — prompts by task and what the agent sees
- [**Slides in a Multi-App Workspace**](/docs/template-slides-integrations) — how these actions get called from outside Slides
- [**Sharing**](/docs/sharing) — the share-grant model `deck_shares` and `design_system_shares` build on
- [**Real-Time Collaboration**](/docs/real-time-collaboration) — the Yjs pipeline decks share with other collaborative surfaces
- [**Slides overview**](/docs/template-slides) — back to the app summary
