Skip to main content

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.

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:

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
idThe design's id. For a new design the editor makes one (dsg_…); store the design under it.
designThe design document. Store it exactly as it is (JSON).
revisionThe revision this save is based on: the one you last answered (0 for a new design).
idempotencyKeyThe same on a retry of the same save. Answer a repeat with the same answer.
You answerWhen
{ 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​

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.