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:
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
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.listanddesigns.gethave nothing to list, and there is nodesign.savedwebhook — you saw each save as it happened. - Exports still work from the page:
exportHtml,exportPdfand the rest render the design on screen and record the export (with your id and revision). To export from your server, uselettrove.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'sdesignId, or theidin the first save) to open it again withdesignId.
You can keep designs, images, or both: storage: { designs: 'host', images: 'host' } with all four
providers leaves nothing your users made with Lettrove.