Email

Author and send newsletter issues (Preview)

Preview · Available to selected organizations. Write a topic's newsletter issues in Markdown, preview email and web output, and pin a saved issue to a campaign.

An issue is a Markdown document that belongs to a topic. Its frontmatter supplies the title and optional email preheader; its layout turns the body into email or a web preview. Save the issue, then give a campaign its path and saved commit so later edits cannot change the content that run sends.

Create an issue

Use topicIssues on the Promise client or TopicIssues from samva/effect/topic-issues. List issues returns each issue's path, its draft title, and its latest campaign send (latestSend, null until a campaign schedules it), plus the current revision. A topic with no issue project returns projectId: null, revision: null, and an empty list.

Create an issue creates the topic's project and layout on the first request. Omit expectedRevision for that request and keep the same idempotencyKey and content when retrying it. After the project exists, new creates require its current revision.

create-issue.ts
import { createClient } from "samva";

const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });
const topic = await samva.topics.create({ name: "Weekly digest", default: "opt_in" });
const path = "issues/weekly-digest.md";
const content = `---
title: This week at Example
slug: weekly-digest
topic: ${topic.id}
preheader: New features and a guide to getting started
---

## What shipped

We added a faster way to review your messages.

[Read the guide](https://example.com/guide).
`;

const draft = await samva.topicIssues.create({
  id: topic.id,
  path,
  content,
  idempotencyKey: "weekly-digest-create-1",
});

console.log(draft.workspace.revision);

An issue path names a Markdown file under issues/. The required frontmatter keys are title, slug, and topic. preheader supplies the email preview text. The frontmatter topic is the owning topic ID, matching the API request and the campaign audience. A mismatch blocks validation and Save. The optional gate.leadIn is described under subscribe wall.

Creating or editing an issue changes the shared draft. It does not create a saved commit or send email.

Check and preview

Check the issue at the revision returned by the create or update call. Read diagnostics and incompatibilities, then choose a fixture from declaration.fixtures. The initial layout declares default.

preview-issue.ts
const checked = await samva.topicIssues.check({
  id: topic.id,
  path,
  expectedRevision: draft.workspace.revision,
});
console.log(checked.diagnostics, checked.incompatibilities);

const preview = await samva.topicIssues.render({
  id: topic.id,
  path,
  expectedRevision: draft.workspace.revision,
  fixture: "default",
  target: "email",
});
console.log(preview.rendered?.html);

Render a fixture accepts target: "email" or target: "web". Email output is in rendered; web output is in page. Inspect findings when the requested output is null. Web output is the page readers get, in the topic's brand, with a Wall starts here marker where a frontmatter gate begins. It is a preview, not a public issue URL.

In the dashboard, open Issues from a topic's actions, or select the topic's name. The page lists every issue with its status (draft, scheduled, sending or sent), its date and its recipients. Select an issue to open it in the editor, or choose Write issue to start a new one.

A new topic's first issue also creates its layout: a masthead with your brand logo and the topic's name, a type scale for the issue's headings, paragraphs, lists and quotes, and your brand's footer. The layout takes your default brand's colors and fonts, and every issue in the topic shares it.

The editor lets you switch between Email and Web previews. To edit Email prose, select a paragraph, open Design, and change its Text field. In Web, double-click a paragraph to edit it directly. To edit the whole issue, frontmatter included, add Source from the + menu. Every change is saved to the issue's draft; the topic's layout can't be edited from an issue.

Edit and save

Open the workspace to read its files and current revision. Read an issue with that revision, then update it by sending the complete Markdown content and expectedRevision. Each successful edit returns the new revision.

Remove an issue also requires the current revision and an idempotency key. The project's default issue must remain. Removal changes the draft; saved commits remain available.

Save validates the project and returns the commit that a campaign can pin:

save-issue.ts
const saved = await samva.topicIssues.save({
  id: topic.id,
  expectedRevision: draft.workspace.revision,
  expectedHead: draft.workspace.remoteHead,
  message: "Save weekly digest",
  idempotencyKey: "weekly-digest-save-1",
});
console.log(saved.commit);

expectedRevision protects the draft you inspected. expectedHead protects the saved branch head. If either has changed, the request fails with ConflictError. Read the current workspace, reconcile your changes, and submit a new edit or Save request with the current values. Preserve the original request and idempotency key when retrying after a lost response.

Use Head to read the current revision and saved head, and History to list saved commits.

Pin the issue to a campaign

Set content.issue to the issue path and the commit returned by Save. audience.topicId selects the topic that owns the issue. The saved issue supplies the subject and preheader; do not combine an issue pin with inline subject, HTML, text, or a template selection.

If your layout needs recipient data, use email.templateData for fallback values when a recipient has no matching field. Email delivery options and fallback data are preserved when you copy a campaign in the dashboard.

issue-campaign.ts
const campaign = await samva.campaigns.create({
  name: "Weekly digest",
  channel: "email",
  audience: { topicId: topic.id },
  content: {
    channel: "email",
    issue: { path, commit: saved.commit },
    from: { email: "newsletter@your-domain.com", name: "Example" },
    email: {},
  },
});
console.log(campaign.id);

Use a verified sender and review the audience before scheduling the run. The run keeps its issue path and commit. A later draft edit or Save does not change that run's content. Subscription preferences, suppression, and campaign audience filters still determine who receives it.

In the dashboard, Edit copies a campaign's saved issue and topic into the form. Save as new creates another campaign with that same saved issue.

Preview · Available to selected organizations.

Publish a public archive

Public publication is Preview and available to selected organizations. An archive requires an opt_in topic, archiveEnabled: true, and a publicationSlug. Enable it through Update a topic:

enable-archive.ts
await samva.topics.update({
  id: topic.id,
  default: "opt_in",
  archiveEnabled: true,
  publicationSlug: "example-weekly",
});

The hosted publication is at https://example-weekly.samva.app, following https://<publicationSlug>.samva.app. Its home page features the latest sent issue, /archive lists every issue by year, and sent issues appear at /p/<issue-slug>, using the slug in their frontmatter. Pages use the brand of the topic's default sender domain, or your default brand when the topic has no sender, and show "Powered by Samva" unless your plan removes it. A brand change reaches the site with the next published issue. Saving or previewing a draft does not publish it. A campaign must send the saved issue before it appears in the archive. Public pages show issue content without recipient personalization. Publish only content you intend anyone to read.

Set archiveEnabled: false to stop serving the public archive. An opt_out topic cannot enable a public archive. If Samva cannot confirm the withdrawal, the request returns an error with the settings already saved. Samva retries the withdrawal automatically; read the topic to check its current settings.

Gate an issue behind a subscribe wall

Add gate.leadIn to an issue's frontmatter to show only its opening on the public archive page:

issues/weekly-digest.md
---
title: This week at Example
slug: weekly-digest
topic: topic_123
gate:
  leadIn: 2
---

leadIn is the number of leading top-level blocks, such as paragraphs and headings, that readers see in full. The rest of the issue stays in the page, marked for search engines as not free (isAccessibleForFree: false), and fades behind an inline subscribe wall. A leadIn that is greater than or equal to the issue's block count gates nothing. The gate applies to the web page only: the email always carries the full issue.

The wall asks for an email address. Submitting it sends the topic's double opt-in confirmation email and unlocks the page right away in that browser on that publication host, for a 30-day period. The topic subscription starts only after the reader confirms. It is recorded with source wall, and its contact.subscribed event carries the issue URL as page. Wall submissions use the same rate limits and honeypot as public topic forms, and need public topic signup enabled for the organization.

Subscribe from the publication site

The publication site has a subscribe button in its header, a form on its home page, and a form after each open issue. They send the same double opt-in confirmation email as the wall, and a submission unlocks gated issues in that browser on that publication host for the same 30-day period. A confirmed signup from these forms is recorded with source publication and consentSource: "publication". Its contact.subscribed event has no formKey, and Samva sets page to the home page URL or the issue URL. Every publication form, the wall included, uses the same rate limits and honeypot.

Setting archiveEnabled: false also ends every reader's unlocked access.

Add a custom publication domain

Use samva.topicPublicationDomains in Promise applications, or import TopicPublicationDomains with import * as TopicPublicationDomains from "samva/effect/publication-domains" in Effect applications. The topic must have an enabled, published public archive before you add a domain.

publication-domain.ts
const hostname = "news.example.com";
const addKey = crypto.randomUUID();
const domain = await samva.topicPublicationDomains.add({
  id: topic.id,
  hostname,
  idempotencyKey: addKey,
});
console.log(domain.cname, domain.verificationRecords);

Add the returned cname and verificationRecords to your DNS configuration, then get the domain to recheck its status:

verify-publication-domain.ts
const status = await samva.topicPublicationDomains.get({ id: topic.id, hostname });
console.log(status.active, status.hostnameStatus, status.sslStatus, status.verificationErrors);

The hostname serves the publication only when ownership verification and certificate activation are complete and active is true. Preserve the original UUID idempotencyKey, hostname, and topic when retrying an add request after a lost response.

Remove the domain with a new UUID for that removal, preserving it across retries:

remove-publication-domain.ts
const removeKey = crypto.randomUUID();
const removed = await samva.topicPublicationDomains.remove({
  id: topic.id,
  hostname,
  idempotencyKey: removeKey,
});
console.log(removed.phase);

Removal stops publication access on the hostname before domain cleanup finishes. removing means cleanup is pending. You can remove a domain after disabling its topic's archive. After deleting the topic, use an organization-wide credential with the original topic ID to inspect or remove its domain. Namespace-scoped credentials cannot manage a deleted topic's claim.

An email's view-in-browser link is signed for that delivery. It opens the delivery's frozen content and is private, excluded from search indexing, and served without shared caching. Treat the URL as sensitive: anyone who has it can open the content while the link is valid. A private browser link does not publish the message in the topic's public archive.

Content must support web rendering for a browser link to be created. A send that requires a browser link fails validation when its template cannot produce web output.

CLI

Use samva topics issues --help for issue authoring commands and required revision arguments. Campaign creation selects saved content with --issue-path and --issue-commit together:

samva campaigns create --name "Weekly digest" --topic-id unsub_1234567890abcdef \
  --issue-path issues/weekly-digest.md \
  --issue-commit 1111111111111111111111111111111111111111

Use the commit returned by Save and set a default sender on the topic. An issue pin supplies its subject and body, so omit inline content and template options.

Related documentation

On this page