DS DevShelfHub Projects · AI tools
Articles / Build an AI Email Assistant: Postmark + Drizzle + Anthropic + Next.js

AI Engineering

Build an AI Email Assistant: Postmark + Drizzle + Anthropic + Next.js

By DevShelfHub

A complete tutorial for building an AI assistant that lives at a real email address — Next.js webhooks for Postmark, Drizzle ORM on SQLite for messages and contacts, Claude Sonnet for replies, plus welcomes, threading, and broadcasts.

Build an AI Email Assistant: Postmark + Drizzle + Anthropic + Next.js

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

  1. Someone emails your assistant address.
  2. Postmark receives it and POSTs the JSON payload to /api/inbound.
  3. The route saves the message, upserts the contact, and runs the welcome-check.
  4. Anthropic generates a reply using recent conversation history as context.
  5. Postmark sends the reply with the right thread headers.
  6. 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 calls
  • drizzle-orm + better-sqlite3 — storage
  • drizzle-kit — migrations

Environment variables

  • POSTMARK_SERVER_TOKEN
  • ANTHROPIC_API_KEY
  • SENDER_EMAIL
  • ASSISTANT_NAME

🗄️ The data model

Two tables cover the assistant entirely: messages and contacts. Drizzle schema in plain TypeScript.

messages

  • id auto-increment PK
  • thread_id text not null
  • role enum: user, assistant
  • content text not null
  • created_at timestamp default now

contacts

  • id auto-increment PK
  • email unique not null
  • display_name nullable
  • welcome_sent_at nullable timestamp
  • created_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:

  1. Parses and validates the payload (skip if no body).
  2. Upserts the contact row.
  3. If this is their first inbound, fires the welcome flow.
  4. Saves the message under a thread key derived from the original message ID.
  5. Pulls the recent message log for that thread.
  6. Calls Anthropic with a system prompt and the conversation history.
  7. Sends the reply via Postmark with proper threading headers (In-Reply-To, References).
  8. 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-To to the inbound message ID, and References to 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_id in 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

Build an AI Email Assistant: Postmark + Drizzle + Anthropic + Next.js FAQ

Why Postmark over SendGrid or Resend?

All three work. Postmark’s deliverability and inbound webhook ergonomics are among the cleanest. Resend is a strong alternative if you prefer their developer experience. SendGrid scales furthest but has heavier setup.

Can I use a free Gmail or personal mailbox instead?

Technically yes via IMAP, but inbound webhooks are much cleaner. A transactional email provider also protects your domain reputation by separating assistant traffic from your personal sending.

Is SQLite enough?

For a personal or small-team assistant, easily. For thousands of contacts and high throughput, move to Postgres — Drizzle works there too with minimal changes.

How much does this cost to run?

Postmark’s free tier covers 100 emails/month, paid plans start ~$15/month for 10K emails. Anthropic costs depend on traffic — a few cents per conversation with Sonnet. Hosting on Vercel free or a $5 VPS.

Can I give the assistant tools (web search, calendar)?

Yes — pass tools to the Anthropic call and parse tool-use responses before sending the reply email. The architecture supports it; you just add tool handling between the model call and the Postmark send.

How do I keep replies from sounding robotic?

Tight system prompt focused on tone (“warm but efficient,” “short paragraphs,” “no disclaimers”). Cap max_tokens. Sign with a human name. Strip your own AI-tells manually if you spot them in early replies.

What about spam and abuse?

Implement a per-sender rate limit, validate Postmark webhook signatures, and consider a whitelist or OTP confirmation flow if you don’t want random senders. Spam protection is your responsibility, not Postmark’s.