> Lettrove docs 1.x · https://docs.lettrove.com/docs/data/own-design-storage

# Your own design storage

By default Lettrove saves your users' designs. To keep them **only in your own database**, tell the
editor so and give it two functions: one that loads a design, one that saves it. Lettrove then holds
no design at all — it renders and exports what is on screen, and keeps only a record of each export.

```ts
import { designProviders } from './design-providers.js';

createEditor({
  // …container, publishableKey, getToken
  designId: savedId ?? null, // your id for it; leave it out to start a new design
  storage: { designs: 'host' },
  providers: designProviders,
  on: { 'design:saved': ({ designId }) => rememberDesignId(designId) },
});
```

The two providers call your server:

```js title="design-providers.js"
export const designProviders = {
  'design:load': async ({ id }, { signal }) => {
    const response = await fetch(`/my-designs/${encodeURIComponent(id)}`, { signal });
    if (!response.ok) return { error: { message: 'That design is not there.', code: 'design_not_found' } };
    return await response.json(); // { design, revision }
  },
  'design:save': async ({ id, design, revision, idempotencyKey }, { signal }) => {
    const response = await fetch(`/my-designs/${encodeURIComponent(id)}`, {
      method: 'PUT',
      headers: { 'content-type': 'application/json', 'idempotency-key': idempotencyKey },
      body: JSON.stringify({ design, revision }),
      signal,
    });
    return await response.json(); // { id, revision, savedAt } — or { conflict: { theirs, revision } }
  },
};
```

## The contract

### `design:load({ id }, { signal })`

Answer `{ design, revision }`: the design document exactly as the editor gave it to you, and the
revision you stored it at. To refuse: `{ error: { message, code: 'design_not_found' } }`.

### `design:save({ id, design, revision, idempotencyKey }, { signal })`

| You get | |
|---|---|
| `id` | The design's id. For a new design the editor makes one (`dsg_…`); store the design under it. |
| `design` | The design document. Store it exactly as it is (JSON). |
| `revision` | The revision this save is based on: the one you last answered (`0` for a new design). |
| `idempotencyKey` | The same on a retry of the same save. Answer a repeat with the same answer. |

| You answer | When |
|---|---|
| `{ id, revision, savedAt }` | Saved. `revision` is the new one (the old one plus one), `savedAt` an ISO time. |
| `{ conflict: { theirs, revision } }` | Your stored revision is not the one this save is based on: someone else saved since. `theirs` is the design you hold now, `revision` its revision. Nothing is overwritten: the editor tells your user, who chooses **Use the saved version** (the editor shows `theirs`) or **Keep mine** (its version is saved again, based on `revision`). |
| `{ error: { message } }` | Refused, in your own words. |

The editor saves as your user works, so this is called often.

## A complete example: your server keeps the designs

```js title="design-server.mjs"
import express from 'express';

const app = express();
const designs = new Map(); // id → { design, revision }. In your product: your database.
const answered = new Map(); // idempotency key → the answer given

app.get('/my-designs/:id', (req, res) => {
  const found = designs.get(req.params.id);
  if (!found) return res.status(404).json({ error: { message: 'No such design.', code: 'design_not_found' } });
  res.json(found);
});

app.put('/my-designs/:id', express.json({ limit: '2mb' }), (req, res) => {
  const key = req.get('idempotency-key');
  if (key && answered.has(key)) return res.json(answered.get(key)); // a retry: the same answer
  const { design, revision } = req.body;
  const current = designs.get(req.params.id);
  let answer;
  if (!current && revision !== 0) {
    answer = { error: { message: 'No such design.', code: 'design_not_found' } };
  } else if (current && current.revision !== revision) {
    answer = { conflict: { theirs: current.design, revision: current.revision } };
  } else {
    const next = { design, revision: revision + 1 };
    designs.set(req.params.id, next);
    answer = { id: req.params.id, revision: next.revision, savedAt: new Date().toISOString() };
  }
  if (key) answered.set(key, answer);
  res.json(answer);
});

app.use(express.static('public'));
app.listen(3300, () => console.log('Listening on http://localhost:3300'));
```

:::warning Guard these two routes
The example answers anyone, to stay short. These routes are yours, so they sit behind your own sign-in: before loading or saving, check the signed-in user may open that design, exactly as you would for any other record of theirs. Lettrove never calls them; only your page does.
:::

## What changes when you keep designs

- **Your users' designs exist only with you.** `lettrove.designs.list` and `designs.get` have nothing
  to list, and there is no `design.saved` webhook — you saw each save as it happened.
- **Exports still work** from the page: `exportHtml`, `exportPdf` and the rest render the design on
  screen and record the export (with your id and revision). To export from your server, use
  `lettrove.render({ doc, designId })` with the design from your database.
- **History** (restore points) is yours to keep: the editor's History panel is hidden.
- **Keep the id the editor gives a new design** (`design:saved`'s `designId`, or the `id` in the first
  save) to open it again with `designId`.

You can keep designs, images, or both: `storage: { designs: 'host', images: 'host' }` with all four
providers leaves nothing your users made with Lettrove.
