Local Letter
Transactional email that speaks every language. Design a template once in a visual editor, translate it per locale, and render it from any codebase with one typed SDK call. Open source and self-hosted — your templates, your database, your infrastructure.
await letters.send({
template: 'welcome-email',
to: 'customer@example.com',
variables: { first_name: 'Sagar' },
locale: 'fr',
})What it is
Local Letter splits an email into the two things that change at different speeds:
- The template — subject line and HTML body, edited visually in the dashboard, versioned per locale. Marketing and support can change copy without a deploy.
- The call site — a single
send()in your backend that names a template key, hands over some variables, and says which locale the recipient reads.
Rendering happens in your API. Delivery happens through Resend , called directly from your server, so your Resend key never reaches Local Letter.
The pieces
| Piece | What it does |
|---|---|
API (apps/api) | Express + Prisma + Postgres. Auth, projects, templates, locales, API keys, and the render endpoint the SDKs call. |
Dashboard (apps/web) | React + Vite. The visual template editor, translations, projects, and API-key management. |
| SDKs | Node, Python and Go — the same send() in each. |
Marketing site (apps/site) | The public site at the project’s front door. |
Docs (apps/docs) | This site. |
Where to start
Install and run it locallySend your first emailHow templates and locales fit togetherThe render endpoint
Design decisions worth knowing up front
- Your Resend key stays yours. The SDK renders against your API, then calls Resend itself. Local Letter never sees the key and never sends mail.
- A missing variable is loud. An unmatched
{{token}}is left in the output rather than blanked, so a mistake is visible in the email instead of silently producing a gap. See Variables. - Locales fall back, they don’t fail. Requested locale, then your fallback, then the template’s default. See Locales.
- Templates are per project. A template key is unique within a project, and an API key is scoped to exactly one project. See Projects.
Last updated on