Drop-in email inbox for Next.js. Unlimited identities on any verified domain. Powered by Resend + Postgres.
npm install sendbox
Or install from GitHub (the prepare script auto-builds dist/):
npm install github:coreyepstein/sendbox
- Send email from any
@yourdomain.comaddress - Receive email via Resend inbound webhooks
- Thread management — automatic threading by headers and subject
- Inbox UI — dark-themed React components (thread list, conversation view, compose, identity management)
- Unlimited identities — email aliases are just database rows, no per-seat cost
Run schema.sql against your Postgres database (Neon, Supabase, or any Postgres):
-- Copy from node_modules/sendbox/schema.sql
-- Creates: identities, threads, messages tablesDATABASE_URL=postgresql://...
RESEND_API_KEY=re_...// app/api/emails/send/route.ts
import { createSendHandler } from "sendbox/handlers";
export const POST = createSendHandler();
// app/api/webhooks/email/route.ts
import { createWebhookHandler } from "sendbox/handlers";
export const POST = createWebhookHandler();You'll also need basic CRUD routes for the UI to call:
// app/api/identities/route.ts
import { getIdentities, createIdentity } from "sendbox";
import { NextResponse } from "next/server";
export async function GET() {
const identities = await getIdentities();
return NextResponse.json({ identities });
}
export async function POST(req: Request) {
const body = await req.json();
const identity = await createIdentity(body);
return NextResponse.json({ identity });
}
// app/api/threads/route.ts
import { getThreadsWithPreview } from "sendbox";
import { NextResponse, NextRequest } from "next/server";
export async function GET(req: NextRequest) {
const identityId = req.nextUrl.searchParams.get("identity_id") || undefined;
const threads = await getThreadsWithPreview(identityId);
return NextResponse.json({ threads });
}
// app/api/threads/[id]/route.ts
import { getThreadWithIdentity, getThreadMessages } from "sendbox";
import { NextResponse } from "next/server";
export async function GET(_req: Request, { params }: { params: { id: string } }) {
const thread = await getThreadWithIdentity(params.id);
if (!thread) return NextResponse.json({ error: "Not found" }, { status: 404 });
const messages = await getThreadMessages(params.id);
return NextResponse.json({ thread, messages });
}// app/inbox/page.tsx
"use client";
import { Inbox } from "sendbox/ui";
import { useRouter } from "next/navigation";
export default function InboxPage() {
const router = useRouter();
return <Inbox onThreadClick={(id) => router.push(`/inbox/${id}`)} />;
}
// app/inbox/[threadId]/page.tsx
"use client";
import { Thread } from "sendbox/ui";
import { useRouter, useParams } from "next/navigation";
export default function ThreadPage() {
const router = useRouter();
const { threadId } = useParams();
return <Thread threadId={threadId as string} onBack={() => router.push("/inbox")} />;
}
// app/inbox/identities/page.tsx
"use client";
import { Identities } from "sendbox/ui";
export default function IdentitiesPage() {
return <Identities domain="yourdomain.com" />;
}
// app/inbox/layout.tsx
"use client";
import { InboxLayout } from "sendbox/ui";
export default function Layout({ children }: { children: React.ReactNode }) {
return <InboxLayout>{children}</InboxLayout>;
}- Verify your domain in Resend
- Add MX, SPF, DKIM records to your DNS
- Set up an inbound webhook pointing to
https://yourdomain.com/api/webhooks/email
Types: Identity, Thread, ThreadWithPreview, Message, InboundEmailPayload, ProcessResult, SendboxConfig, HandlerOptions
Config: configureSendbox(config) — set custom SQL executor, API keys, or database URL
DB: getIdentities, createIdentity, deleteIdentity, getThreads, createThread, getThreadsWithPreview, getThread, getThreadWithIdentity, getThreadMessages, createMessage
Email: processInboundEmail, matchIdentity, matchThread, normalizeSubject, parseEmailAddress
Resend: getResend — returns the Resend client singleton
createSendHandler(options?) — Next.js POST handler for outbound email
createWebhookHandler(options?) — Next.js POST handler for Resend inbound webhooks
Both accept HandlerOptions:
{
authorize?: (req: Request) => Promise<void> | void;
onMessageSent?: (data) => Promise<void> | void;
onMessageReceived?: (data) => Promise<void> | void;
}<Inbox /> — Thread list with identity filter and compose modal. Props: apiBase?, apiPrefix?, onThreadClick?, className?
<Thread /> — Conversation view with reply composer. Props: threadId, apiBase?, apiPrefix?, onBack?, className?
<Identities /> — Identity CRUD. Props: apiBase?, apiPrefix?, domain?, className?
<InboxLayout /> — Tab navigation wrapper. Props: basePath?, className?
Path configuration — All three components build URLs as {apiBase}{apiPrefix}/{resource}. Default apiBase="" and apiPrefix="/api" yield /api/threads, /api/identities, /api/emails/send (matches the built-in handlers when mounted at /api/*). To host the handlers under a different prefix (e.g. /api/inbox/*), pass apiPrefix="/api/inbox" — and ensure your server mounts the send handler at {prefix}/emails/send (the other paths — threads, identities — are already just appended).
By default, sendbox uses @neondatabase/serverless, which connects through Neon's HTTP proxy (api.{host}). This only works with Neon Postgres — Supabase, Railway, and self-hosted databases need a custom SQL executor.
Pass any tagged-template function to configureSendbox() that returns Promise<Record<string, unknown>[]>.
postgres supports tagged templates natively:
// lib/sendbox.ts — import this file in your API routes
import { configureSendbox } from "sendbox";
import postgres from "postgres";
const sql = postgres(process.env.DATABASE_URL!, { ssl: "require" });
configureSendbox({
sql: (strings, ...values) =>
sql(strings, ...values).then((rows) => rows.map((r) => ({ ...r }))),
});Supabase: Use the connection pooler URL (
postgresql://postgres.{ref}:{password}@aws-0-{region}.pooler.supabase.com:5432/postgres), not the directdb.{ref}.supabase.coURL.
Inbound: MX → Resend → webhook POST /api/webhooks/email → Postgres
Outbound: Compose UI → POST /api/emails/send → Resend API → Postgres
UI: React components fetch from /api/* routes
- Next.js 14+
- React 18+
- PostgreSQL (Neon, Supabase, or any Postgres — see Custom SQL Adapter for non-Neon setups)
- Resend account with verified domain
- Tailwind CSS (for UI components)
MIT