Templates

Reusable message bodies with {{variable}} substitution. Scoped to an org, keyed by slug.

Dashboard CRUD

Internal API: /api/internal/templates (session auth, org-scoped).

Create

POST /api/internal/templates
{
  "slug": "welcome-email",
  "subject": "Welcome to {{company}}",
  "html": "<p>Hi {{name}}, thanks for signing up.</p>",
  "text": "Hi {{name}}, thanks for signing up.",
  "channel": "email"
}
  • slug — lowercase letters, digits, dashes; unique per org.
  • channelemail or sms. SMS templates use text only; subject is ignored by the SMS send path.
  • html/text — either or both. For email, at least one must render non-empty.

409 if slug collides within org.

List / Delete

GET    /api/internal/templates
DELETE /api/internal/templates?id=tpl_...

Preview

Render the template with sample variables without sending. Available to any session member.

POST /api/internal/templates/preview
{
  "id": "tpl_...",        // or "slug": "welcome-email"
  "variables": { "name": "Fareed" }
}

Response:

{
  "id": "tpl_...",
  "slug": "welcome-email",
  "channel": "email",
  "subject": "Welcome, Fareed",
  "html": "<p>Hi Fareed, ...</p>",
  "text": "Hi Fareed, ..."
}

Test send

Render + dispatch a real test message via the provider. Tagged template-test and written to messages with metadata.source = "test_send" so test sends are easy to filter out of usage dashboards.

POST /api/internal/templates/test-send
{
  "id": "tpl_...",
  "from": "hello@yourdomain.com",
  "to": "you@example.com",
  "variables": { "name": "Fareed" }
}

Response: { "id": "msg_...", "status": "sent" }.

502 is returned if the provider call fails; the error message is surfaced to the client.

Both endpoints are also surfaced in the dashboard (/overview/templatesPreview / test) — HTML renders in a sandboxed iframe.

Test sends reject viewers — test-send is a real provider call and is gated on requireWriteSession.

Using in a send

POST /api/v1/emails
{
  "from": "hi@yourdomain.com",
  "to": ["user@example.com"],
  "template": "welcome-email",
  "variables": { "name": "Fareed", "company": "Acme" }
}

Rendering rules (src/lib/api/template.ts):

  • {{key}} → variable lookup in variables.
  • Whitespace allowed: {{ key }} works.
  • Missing variable → empty string.
  • No dotted paths, no expressions, no conditionals — intentional simplicity.

Overrides

If the send body supplies subject / html / text / body directly, those override the template's rendered versions. This lets you override a single field while keeping the rest.

Schema

templates table (src/lib/db/schema/templates.ts):

Column Type Notes
id text PK tpl_...
org_id FK scope
slug text unique per org
subject text used for email
html text optional
text text required for sms if used, optional for email
channel text email / sms
created_at/updated_at timestamps