User guide · Contacts

Contacts and forms

Your own sign-up forms post to stet, and stet records each person in your own Postgres: which list they joined, when, and from which page. Every email you send can carry a one-click unsubscribe that stet serves. Your own email provider does the sending.

Contacts live in a database. A snapshot-only site adds one in the first step.

01Add a database

Install the Postgres driver, point STET_DATABASE_URL at your database, and run upgrade. It creates the contact tables and prints the store block to add to stet.config.json.

$ npm install pg
$ npx stet upgrade --store pg
…
applied 003_contacts.sql

add this to stet.config.json (this command never edits it):
{
  "store": {
    "adapter": "pg",
    "urlEnv": "STET_DATABASE_URL"
  }
}

02Create a list

A group is one list people can join: a newsletter, a waitlist, early access. It opens as soon as you create it.

$ npx stet contacts group add newsletter --name "Harbour newsletter"
group newsletter: created, open — no properties
join endpoint: POST <your forms mount>/join/newsletter

03Point your form at it

createStetFormsHandler serves two routes: the join your form posts to, and the unsubscribe page your emails link to. Mount it at /api/stet in your app, or in a small Worker for a static site.

Your form posts the address as JSON and gets {"ok":true} back. The contacts reference has the full setup for Next.js and for a Worker.

// the forms mount
import { createStetFormsHandler } from '@getstet/stet/server';

const forms = createStetFormsHandler({
  store, secret,
  unsubscribeBase: 'https://example.com/api/stet',
  guard: rateLimit,
  onJoin: sendWelcome,
});

// your form
POST /api/stet/join/newsletter  {"email":"[email protected]"}
→ {"ok":true}

04Welcome and unsubscribe

When someone joins, stet calls your onJoin with who they are and a ready-made unsubscribe link, and your email provider sends the welcome. The link opens a page stet serves, and one click records the unsubscribe.

// what onJoin receives
{ group: "newsletter", email: "[email protected]", isNew: true,
  unsubscribeUrl: "https://example.com/api/stet/unsubscribe?token=…", … }

05See who joined

contacts list shows a group's members. contacts get shows one person: when and where they joined, and whether they unsubscribed. export and erase answer a person's request for their data.

$ npx stet contacts get [email protected]
[email protected] in project default — first seen 26 Sep 2026 08:56
  newsletter   joined 26 Sep 2026 08:56 from https://harbour.example/ (form footer)
  suppressed: marketing, 26 Sep 2026 08:56 (one-click)