Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .tegami/2026-10-03-align-samva-0.8.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
packages:
"@samva/better-auth": minor
"@samva/email-sdk": minor
---

## Target the Samva 0.8 SDK

Both packages now target `samva@^0.8.0`.

`@samva/email-sdk` reports a `DeliveryUnavailableError` (Samva could not hand the send to delivery)
as `delivery: "not_sent"`, so Email SDK can retry it with the same idempotency key.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ templates, handle delivery events, and verify signed webhooks.
- `cookbooks/*.md` — documentation-first, copy-pasteable recipes.
- `plugins/*` — coding-agent plugins and public documentation.

The maintained integrations target the published Samva 0.5 email SDK. Workspace
packages and examples use `samva@^0.5.0`; copyable external import maps pin
`samva@0.5.0`.
The maintained integrations target the published Samva 0.8 email SDK. Workspace
packages and examples use `samva@^0.8.0`; copyable external import maps pin
`samva@0.8.0`.

## Package integrations

Expand Down Expand Up @@ -79,8 +79,8 @@ Integrations land as individual pull requests. See
form action and raw email endpoint.
- [`tanstack-start-transactional`](./examples/tanstack-start-transactional) —
TanStack Start server function and server route.
- [`tsx-template-samva`](./examples/tsx-template-samva) — TSX template
authoring and preview with `@samva/vite`.
- [`tsx-template-samva`](./examples/tsx-template-samva) — a `defineTemplate`
email project: check, render, preview, and publish with the Samva CLI.

## Getting started

Expand Down
223 changes: 99 additions & 124 deletions bun.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions cookbooks/effect-sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ The payload is the same. This cookbook covers the Effect runtime.
## Setup

```sh
bun add samva effect@4.0.0-rc.112
bun add samva effect@4.0.0-rc.117
```

Keep `SAMVA_API_KEY` on the server.
Pin Effect 4 to `4.0.0-rc.112` to match the SDK peer dependency exactly.
Pin Effect 4 to `4.0.0-rc.117` to match the SDK peer dependency exactly.
Effect 4 is a release candidate. HTTP client modules live under
`effect/unstable/http`. `Client.layerFetch` already provides the fetch layer.

Expand Down
4 changes: 2 additions & 2 deletions cookbooks/inbound-email.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ const sent = await samva.email.send(
html: "<p>Thanks for your reply. We will follow up shortly.</p>",
inReplyToMessageId: reply.messageId,
},
{ headers: { "idempotency-key": verified.id } },
{ idempotencyKey: verified.id },
);
```

Expand All @@ -114,7 +114,7 @@ than once, and a transient send failure can be retried.

- Keep a durable set of processed webhook ids with a unique constraint. Use
`verified.id`, which is stable across retries.
- Pass that id as the `idempotency-key` header on the send.
- Pass that id as the `idempotencyKey` option on the send.
- Record completion only after the send succeeds. A failed send stays
unprocessed and can be retried.

Expand Down
2 changes: 1 addition & 1 deletion cookbooks/supabase-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ For a Supabase Edge Function, map those imports in `deno.json`:
"react": "npm:react@19.0.0",
"react/jsx-runtime": "npm:react@19.0.0/jsx-runtime",
"react-email": "npm:react-email@6.6.5",
"samva": "npm:samva@0.5.0",
"samva": "npm:samva@0.8.0",
"standardwebhooks": "npm:standardwebhooks@1.0.0"
}
}
Expand Down
165 changes: 113 additions & 52 deletions cookbooks/tsx-templates.md
Original file line number Diff line number Diff line change
@@ -1,105 +1,166 @@
# TSX email templates

Samva email templates are ordinary TSX projects. You author the source, preview
it locally, and publish an immutable publication that sends render. The
`samva-integrations` [`tsx-template-samva`](../examples/tsx-template-samva/)
example is the runnable version of this page.
Samva email templates are ordinary TSX projects. You author the source, check and preview it
locally, push it to Git, and publish an immutable publication that sends render. The
`samva-integrations` [`tsx-template-samva`](../examples/tsx-template-samva/) example is the runnable
version of this page.

## Install the authoring toolchain

```sh
bun add @samva/markup @samva/vite vite
bun add --dev @samva/cli
bun add @samva/markup
bun add --dev @samva/cli @samva/vite vite typescript
```

The `samva` executable comes from `@samva/cli`; run it through your package
runner so the local install resolves.
The `samva` executable comes from `@samva/cli`; run it through your package runner so the local
install resolves. The CLI loads the compiler and editor your project installs, so the project's
lockfile pins the version that previews, checks, and builds use. Commit that lockfile (Bun text
lockfile v2 or npm lockfile v3); hosted builds consume the locked graph and verify package
integrity.

Set `jsx: "react-jsx"` and `jsxImportSource: "@samva/markup/email"` in
`tsconfig.json`. A template module default-exports `defineEmail({ id, schema,
fixtures, render })` from `@samva/markup/template`; `render` receives validated
JSON and returns the subject, optional preheader, body, and optional text.
To start from the canonical project instead, run `samva templates init --name "Welcome email"
--dir welcome-email`. It creates a Samva-managed project and clones its Git repository.

```tsx title="emails/welcome.tsx"
Set `jsx: "react-jsx"` and `jsxImportSource: "@samva/markup/email"` in `tsconfig.json`.

## Write a template

A template is one `.tsx` file in `templates/` that default-exports `defineTemplate` from
`@samva/markup`: a project-unique kebab-case `id`, a JSON Schema input contract, named fixtures,
and an `email` channel of functions of the input.

```tsx title="templates/welcome.tsx"
/** @jsxImportSource @samva/markup/email */
import { defineTemplate } from "@samva/markup";
import { Email, Section } from "@samva/markup/email/components";
import { jsonSchema } from "@samva/markup/input-schema";
import { defineEmail } from "@samva/markup/template";

export default defineEmail({
export default defineTemplate({
id: "welcome",
schema: jsonSchema<{ firstName: string }>({
schema: jsonSchema<{ firstName: string; workspace: string }>({
type: "object",
properties: { firstName: { type: "string" } },
required: ["firstName"],
properties: {
firstName: { type: "string" },
workspace: { type: "string" },
},
required: ["firstName", "workspace"],
additionalProperties: false,
}),
fixtures: { default: { firstName: "Ada" } },
render: (input) => ({
subject: `Welcome, ${input.firstName}`,
body: (
fixtures: {
default: { firstName: "Ada", workspace: "Acme" },
},
email: {
subject: (input) => `Welcome to ${input.workspace}`,
preheader: (input) => `Welcome aboard, ${input.firstName}`,
body: (input) => (
<Email>
<Section>
<h1>Welcome, {input.firstName}</h1>
</Section>
</Email>
),
}),
},
});
```

Template ids are unique within the project and independent of the file name.
Imported helper modules are ordinary modules and stay out of the catalog.

## Preview in the editor

```ts title="vite.config.ts"
import { samvaEditor } from "@samva/vite";
import { defineConfig } from "vite";

export default defineConfig({ plugins: [samvaEditor({ templatesDir: "emails" })] });
`subject` and `preheader` belong to the `email` channel, not to `<Email>`. Plain text is derived
from the body. Every fixture is checked against the schema, and actual send input is validated
against the immutable publication schema.

## Stay inside the static profile

The compiler reads the file and never runs it, so nothing computes at send time. A body can:

- bind input values (`{input.name}`, `href={input.url}`) and build template strings;
- branch with `&&` and `?:`, using `===`, `!==`, `<`, `>`, `!`, `||` and `.length`;
- map over input lists with `.map`, and filter first with `.filter(...)`;
- format with `fmt.money`, `fmt.number`, `fmt.date`, `fmt.time`, `fmt.plural` and `fmt.list`,
imported from `@samva/markup/fmt`;
- do arithmetic on bound numbers inside a formatter argument or a condition
(`fmt.money(item.price * item.quantity, input.currency)`);
- call partials: functions from props to JSX in other project files.

```tsx title="templates/receipt.tsx (body)"
<Email>
{input.items.length > 0 ? (
input.items.map((item) => <p>{item.name}</p>)
) : (
<p>Nothing in this order.</p>
)}
<p>Total {fmt.money(input.total, input.currency)}</p>
{input.receiptUrl && <Button href={input.receiptUrl}>View receipt</Button>}
</Email>
```

Run `vite` and open `http://localhost:5173/`. Choose a fixture to render
concrete input; supported visual edits update the authored TSX.
Any other call, statement, hook, or import is a diagnostic with a stable code, a
`file:line:column` location, and a fix. Only `@samva/markup` and files in the project can be
imported. Compute the value in the caller and send it in the input. Read an optional field only
inside a guard. Locale and time zone are send options, not schema fields.

Style with Tailwind classes. `theme.css` holds the project tokens and layers by `@import`; an
organization brand arrives with `@import "samva:brand";`.

## Check, commit, and publish
## Check, render, preview

```sh
bunx samva templates check
bunx samva templates render receipt --fixture default
bunx samva templates snapshot receipt --fixture default
bunx samva templates dev
```

`check` runs the no-write TypeScript gate and executes the declared fixtures.
Commit the project with a supported lockfile (Bun text lockfile v2 or npm
lockfile v3); hosted builds consume that locked graph and verify package
integrity. Push the checked source, then publish an exact commit:
`check` type-checks, compiles every template, and renders every fixture, with `--watch` for a
continuous gate. `render` prints a fixture's subject, preheader and plain text (`--format html` for
the markup). `snapshot` writes desktop and mobile PNGs, light and dark, to `.samva/snapshots`, and
needs `playwright` or a system Chrome. `dev` starts the local visual editor with no
`vite.config.ts`; keep `.samva` gitignored. `@samva/vite` stays in the project because the CLI
loads it, and its `samvaEditor()` plugin is for custom Vite setups.

Preview is a structural canvas, not an exact Gmail, Outlook, Apple Mail, or Yahoo renderer.

## Commit and publish

```sh
git add .
git commit -m "feat: add the receipt email"
git push origin main
bunx samva templates publish --commit HEAD
```

Publishing pins the project, commit, and entry path and returns an immutable
receipt. Generated HTML and delivery artifacts are not source files.
A project holds several emails: `samva templates add templates/receipt.tsx` registers a new entry
under its `defineTemplate` id, and `samva templates publish --all` publishes every active entry at
one commit. Publishing pins the project, commit, and entry path and returns an immutable receipt; it
does not push or rewrite history. A pushed commit also reaches the hosted editor and agents on the
next workspace open.

## Send a published template

A template send renders the template's current publication unless you name one.
Inline content and a template are exclusive: pass `templateId`/`templateSlug`
with `templateData` and omit inline `subject`/`html`/`text`.
A template send renders the template's current publication unless you name one. Inline content and
a template are exclusive: pass `templateId` or `templateSlug` with `templateData` and omit inline
`subject`/`html`/`text`.

```ts
await samva.email.send({
to: "ada@example.com",
templateId: "tmpl_7q2xk9mvt4znw8rh",
inputContractId: "tcon_7q2xk9mvt4znw8rh",
templateData: { firstName: "Ada" },
templateSlug: "welcome",
templateData: { firstName: "Ada", workspace: "Acme" },
locale: "en-US",
timeZone: "America/New_York",
});
```

- `publicationId` pins one exact publication forever.
- `inputContractId` follows new publications while the input shape stays put.
- Naming both `publicationId` and `inputContractId` is rejected.
- Naming both `publicationId` and `inputContractId` is rejected, and each requires `templateId`.
- `locale` and `timeZone` format `fmt.*` output for the recipient.

Generate the input types for your application from the template catalog:

```sh
bunx samva templates types --out src/samva-templates.ts
```

`--check` exits `1` when the file no longer describes the catalog.

See the runnable [`tsx-template-samva`](../examples/tsx-template-samva/)
example for the full project, and the
[email templates docs](https://samva.dev/docs) for the template API surface.
See the runnable [`tsx-template-samva`](../examples/tsx-template-samva/) example for the full
project, and the [email templates docs](https://samva.dev/docs) for the template API surface.
2 changes: 1 addition & 1 deletion examples/astro-email/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"dependencies": {
"@astrojs/cloudflare": "^14.0.1",
"astro": "^7.0.3",
"samva": "^0.5.0"
"samva": "^0.8.0"
},
"devDependencies": {
"@astrojs/check": "^0.9.9",
Expand Down
2 changes: 1 addition & 1 deletion examples/better-auth-nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"next": "^16.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"samva": "^0.5.0"
"samva": "^0.8.0"
},
"devDependencies": {
"@types/node": "^24.0.0",
Expand Down
2 changes: 1 addition & 1 deletion examples/clerk-webhook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"next": "^16.2.9",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"samva": "^0.5.0",
"samva": "^0.8.0",
"server-only": "^0.0.1"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion examples/effect-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Set `SAMVA_API_KEY` in `.env`. The key must belong to a Samva account with a
verified sender. The `from` field is optional; when omitted, Samva sends from
the verified sender.

This example pins `effect@4.0.0-rc.112`, matching the current `samva/effect`
This example pins `effect@4.0.0-rc.117`, matching the current `samva/effect`
peer dependency. `Client.layerFetch` uses the platform fetch client.

## Send from a script
Expand Down
4 changes: 2 additions & 2 deletions examples/effect-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
"typecheck": "tsgo --noEmit"
},
"dependencies": {
"effect": "4.0.0-rc.112",
"samva": "^0.5.0"
"effect": "4.0.0-rc.117",
"samva": "^0.8.0"
},
"devDependencies": {
"@types/bun": "^1.4.0"
Expand Down
1 change: 1 addition & 0 deletions examples/effect-sdk/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"lib": ["ESNext", "DOM"],
"types": ["bun"]
},
"include": ["src"]
Expand Down
2 changes: 1 addition & 1 deletion examples/email-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"dependencies": {
"@opencoredev/email-sdk": "^1.1.0",
"@samva/email-sdk": "workspace:*",
"samva": "^0.5.0"
"samva": "^0.8.0"
},
"devDependencies": {
"@types/bun": "^1.4.0"
Expand Down
2 changes: 1 addition & 1 deletion examples/hono-cloudflare-workers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
},
"dependencies": {
"hono": "^4.12.27",
"samva": "^0.5.0"
"samva": "^0.8.0"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20260629.1",
Expand Down
2 changes: 1 addition & 1 deletion examples/inbound-email-replies/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"samva": "^0.5.0"
"samva": "^0.8.0"
},
"devDependencies": {
"@types/node": "^24.0.0",
Expand Down
2 changes: 1 addition & 1 deletion examples/inbound-email-replies/src/inbound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export async function handleInboundReply(
html: "<p>Thanks for your reply. We will follow up shortly.</p>",
inReplyToMessageId: reply.messageId,
},
{ headers: { "idempotency-key": verified.id } },
{ idempotencyKey: verified.id },
);

input.processedWebhookIds.add(verified.id);
Expand Down
1 change: 0 additions & 1 deletion examples/inbound-email-replies/tests/inbound.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ const createHarness = (
if (url.pathname.endsWith("/receiving")) {
return json({
success: true,
ruleName: "inbound-replies",
recipients: ["support@example.com"],
});
}
Expand Down
Loading
Loading