guide

Email

Your app sends three kinds of email and every one of them comes from your own domain: the sign-in code, a notification your server sends a person, and the answer you write in the Inbox. Replies come back to the same place. Plain text, a footer that says why it arrived, and a cap a bug cannot get past.

What it is. One envelope for everything your app sends. Once a domain is verified on the Domains page with a purpose of email or both, every email leaves as <word>@yourdomain and a customer who answers reaches your Inbox. Three sender words live on the domain's page in the console: account for sign-in codes and account notices, support for the address people write to and the answers you send, news for the notifications your server and your relays send.

What it does. Sends a sign-in code to anyone who signs in. Sends a plain-text notification to one of your app's own people when your server calls notify() or a relay runs email_person, from the news word, with a footer naming why it arrived, at most five event emails per person per day and two hundred per app per hour. Sends an account notice, kind: "account", outside the per-person cap, from the account word. Keeps every send in your Inbox as a conversation, and when receiving is verified, lands the customer's reply in the same thread labelled by the word they answered. Sends your Inbox answer from the support word. Makes a send at-most-once through retries when you give it a key.

What it does not. Does not send from a Gemmein address once you have a domain: until one is verified, notify() and email_person are refused 409 sender_domain_required, nothing leaves, no cap is spent, and the same key sends once you verify. Does not address an email to a string: the recipient is a person id, and someone who never signed in cannot be emailed. Does not send HTML, a template or a campaign. Does not send a no-reply: every kind can be answered. Does not touch the mailbox on your apex: receiving lives on mail.yourdomain unless you choose the apex, and the customer sees your apex on the From line either way. Does not retry a send from a Gemmein address when your provider refuses yours: you get 502 send_failed and may retry.

Needs something else when. You want a newsletter or an announcement to everyone at once → this is a notification, not a campaign, and broadcasts are not a Gemmein feature. You want rich HTML → send it from your own tool. You want to email someone who has never signed in → they must sign in first; a secret key addresses people, not addresses. Your sign-in codes arrive as <App name> (via Gemmein) → that is the one email Gemmein sends on your behalf before a domain is verified; add the domain with a purpose of email and the code arrives from your account word.

Example. An order ships; your server tells the person.

import { gemmeinServer } from "@gemmein/sdk";
const server = gemmeinServer(process.env.GEMMEIN_SECRET_KEY);

await server.notify(order.ownerUserId, {
  subject: "Your poster is on its way",
  text: "It left the studio this morning. Reply to this email if it hasn't arrived by Friday.",
  key: `shipped:${order.id}`,
});
// → { sent: true, id, threadId, replyRail: true }

The sending domain

Add your domain on the Domains page and say what it is for: your web app, email, or both. A domain for email gets its records at once — SPF and DKIM for sending, an MX for receiving — and the page rechecks them until they are found. The count you are told is the whole count. A mobile-only app has no web domain and can still have a sending one.

Receiving is chosen when the domain is added: on mail.yourdomain, so sending and receiving live on one record set and your apex's own mail is untouched, or on the apex when its MX is Gemmein's. Whichever you choose, the From line a customer sees is your apex, and the Reply-To wears the apex too whenever a forward carries it there.

The three words

wordleaves from ita reply to it
accountsign-in codes; notify() with kind: "account"lands in the Inbox labelled account
supportyour own answers from the Inboxthe address people write to
newsnotify() with kind: "event" (the default); a relay's email_personlands in the Inbox labelled news

Each word is a real address. Two kinds can never share one word; the console refuses it. The defaults come from the word you gave the domain when you added it.

What a send answers

notify() answers { sent, deduped?, recorded?, id, threadId, replyRail }. deduped: true is the same key answering again; recorded: false means the email went but the Inbox could not record it, said plainly; replyRail states whether a reply can reach you right now. A concurrent twin of the same key answers 409 in_flight. The caps answer 429 with resetAt. The full contract, with every code, is in the SDK reference.

The Inbox

Every send appears in the console's Inbox as a conversation, and once receiving is verified a customer's reply lands in the same thread. Your answer goes back out from the support word, so the whole exchange stays in one place for both of you. The room's walk is in the Inbox guide; the domain's walk in the Domains guide.