Introduction
Most AI assistants live behind a chat widget. The bar to interact with them is “open the website, sign in, paste your question.” Email is the opposite — everyone already lives in it, every business has the muscle memory, and you can address the assistant the way you’d address any other contact in your inbox.
This is a complete walkthrough for building an AI email assistant that you trigger by sending an email and that replies with a Claude-generated answer. Stack: Next.js + TypeScript on the server, Drizzle ORM on SQLite for storage, Postmark for transactional email in and out, and the Anthropic API for the model. The end result is an assistant with a real email address that handles inbound conversations, sends welcome emails on first contact, broadcasts to a contact list when you ask it to, and keeps every conversation threaded.
📚 Table of contents
- What you’re building
- Stack overview and why each piece is chosen
- The architecture in one diagram’s worth of words
- Project setup — Next.js, env vars, dependencies
- The data model — messages and contacts in Drizzle
- Inbound webhook — receiving an email and replying
- Outbound — sending emails programmatically
- Welcome flow for first-time senders
- Threading — keeping conversations coherent
- Broadcast endpoint for one-to-many sends
- Local testing and deploying
- Best practices
- Common mistakes
- Frequently asked questions
📬 What you’re building
A web service that:
- Owns a real email address (e.g.
assistant@your-domain.com) - Receives any incoming email and replies via Claude
- Sends a welcome email on a sender’s first contact
- Threads replies so conversations stay coherent
- Maintains a contacts table and message log in SQLite
- Exposes a broadcast endpoint to email the whole list
- Provides a small dashboard for visibility
🧰 The stack
Next.js + TypeScript
API routes are perfect for webhooks. TypeScript keeps the payload shapes honest.
Postmark
Best-in-class deliverability for transactional email, native inbound webhooks, low setup friction.
Drizzle ORM + SQLite
Zero-config storage that’s plenty for a personal assistant. Drizzle gives you typed queries without the heavy ORM tax.
Anthropic SDK
Claude Sonnet for replies. Cheap, fast, and great at structured email responses.
🗺️ The architecture
- Someone emails your assistant address.
- Postmark receives it and POSTs the JSON payload to
/api/inbound. - The route saves the message, upserts the contact, and runs the welcome-check.
- Anthropic generates a reply using recent conversation history as context.
- Postmark sends the reply with the right thread headers.
- Outbound is logged so the next inbound can see history.
⚙️ Project setup
A typical npx create-next-app with
TypeScript. Add the dependencies you’ll need:
postmark— SDK for sending@anthropic-ai/sdk— model callsdrizzle-orm+better-sqlite3— storagedrizzle-kit— migrations
Environment variables
POSTMARK_SERVER_TOKENANTHROPIC_API_KEYSENDER_EMAILASSISTANT_NAME
🗄️ The data model
Two tables cover the assistant entirely: messages
and contacts. Drizzle schema in
plain TypeScript.
messages
idauto-increment PKthread_idtext not nullroleenum: user, assistantcontenttext not nullcreated_attimestamp default now
contacts
idauto-increment PKemailunique not nulldisplay_namenullablewelcome_sent_atnullable timestampcreated_at,updated_at
Helper queries to write up front: saveMessage,
getRecentMessageLog,
upsertContactFromInbound,
shouldSendWelcome,
markWelcomeSent, and
getAllContacts.
📥 Inbound webhook
Postmark POSTs to /api/inbound with a
rich JSON payload (from, subject, text body, HTML body, headers, message ID). The route:
- Parses and validates the payload (skip if no body).
- Upserts the contact row.
- If this is their first inbound, fires the welcome flow.
- Saves the message under a thread key derived from the original message ID.
- Pulls the recent message log for that thread.
- Calls Anthropic with a system prompt and the conversation history.
- Sends the reply via Postmark with proper threading headers (In-Reply-To, References).
- Logs the assistant’s reply to
messages.
📌 Use Sonnet rather than Opus for replies. The cost difference is meaningful and Sonnet produces clean email-shaped responses without burying you in tokens.
📤 Outbound emails
Postmark’s Node SDK is straightforward: instantiate a server client with your token, call
sendEmail with from, to, subject,
text body, optional HTML body, and headers. The two things to get right:
- Threading headers. Set
In-Reply-Toto the inbound message ID, andReferencesto the running thread. - Subject convention. Prepend
Re:when responding so clients render the conversation correctly.
👋 Welcome flow
First-time senders get a welcome email before (or alongside) their reply. Logic lives in
shouldSendWelcome(email) — returns
true if there’s no contact row or the row’s welcome_sent_at
is null.
After sending, call markWelcomeSent(email)
to flip the flag. Keep the welcome short and templated — it’s not the place for AI
creativity.
🧵 Threading conversations
The simplest robust scheme: use the original inbound message ID as the conversation key. Every
follow-up inherits that key. Replies set In-Reply-To
and References so Gmail, Outlook, and
Apple Mail group the conversation correctly.
Recent-history retrieval is just SELECT ...
FROM messages WHERE thread_id = ? ORDER BY created_at DESC LIMIT 20. Pass that into the
Anthropic call as conversation history; the model handles context from there.
📣 Broadcast endpoint
A POST to /api/broadcast takes a
subject and body, fetches all contacts, and sends each one a tailored copy. Useful for newsletters,
announcements, or scheduled drip campaigns.
Compliance
- Always include an unsubscribe footer with a unique tokenized link
- Respect anti-spam laws (CAN-SPAM, CASL, GDPR) — only message people who opted in
- Throttle the send loop — Postmark enforces rate limits, and slamming them gets you flagged
🧪 Local testing and deploying
- Tunneling: Use ngrok or Cloudflare Tunnel to expose your local dev server to Postmark webhooks during development.
- Deploy: Vercel, Fly.io, or Hostinger’s VPS plan. Anywhere that runs Next.js works.
- SQLite caveat: SQLite isn’t suitable for high-concurrency. For a personal assistant it’s fine; for scale switch to Postgres.
- DNS: Point your custom domain at Postmark for inbound, configure SPF/DKIM/DMARC for deliverability.
✅ Best practices
- Keep a short system prompt — long ones bleed into email tone in unfortunate ways
- Cap output tokens around 800–1200 for emails — longer replies feel AI-generated
- Always sign with the same friendly name (env var) so threading and tone stay consistent
- Strip quoted history before sending to the model; only send the new message + your own log
- Index
thread_idin SQLite for fast history retrieval - Verify Postmark webhook signatures to prevent spoofed payloads
- Log every send (success and error) for debugging deliverability issues
❌ Common mistakes
- Forgetting In-Reply-To headers — conversations become disjointed in email clients
- Sending the entire quoted email history to the model — you pay for context the user didn’t add
- Skipping welcome state tracking and emailing the same person “welcome” ten times
- Putting the sender email in a string literal instead of an env var
- Using Opus instead of Sonnet and watching the bill climb fast on volume
- Not verifying webhook signatures — anyone can post fake payloads
- Building the broadcast endpoint without rate limiting and getting blacklisted
Conclusion
Email as an interface for an AI assistant is underrated. No app to install, no signup flow, no custom UI to maintain. Everyone already knows how to use it. With Postmark for transport, Drizzle on SQLite for storage, and Anthropic for the brains, the moving parts fit on one page.
Ship the inbound webhook first, get one round-trip working, then layer on welcomes, threading, and broadcast. A weekend gets you the core; a second weekend gets it polished and deployed.
Explore More on DevShelf
-
Claude AI — Tool Profile
In-depth review of Anthropic's Claude — the model powering this assistant's reply generation.
-
Hermes Agent — Self-Improving AI Bot
A prebuilt agent that handles Telegram, voice, and briefings — an alternative if you want an out-of-the-box inbox assistant.