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
| word | leaves from it | a reply to it |
|---|---|---|
account | sign-in codes; notify() with kind: "account" | lands in the Inbox labelled account |
support | your own answers from the Inbox | the address people write to |
news | notify() with kind: "event" (the default); a relay's email_person | lands 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.