A shared inbox for collectives. One address (yourcollective@collective.email, or hello@yourdomain.org) that the whole group can read and answer: every member signs in with their own email, any conversation is assigned to someone in one tap, and the internal discussion sits right next to the email, invisible to the sender.
This repository is the code that runs collective.email. It is open source under the AGPL-3.0: use the hosted service, or run your own instance and move your inbox between the two whenever you like.
- What it does
- How it works
- Local development
- Run your own instance
- Move an inbox to your instance (import a backup)
- Configuration reference
- Project layout
- License
- One address, no shared password. Members sign in with their personal email and a 6-digit code. A person can belong to several collectives and switch between them.
- Roles. Readers follow along, commenters discuss internally, senders answer as the collective, admins manage members. Guests see only the threads shared with them.
- Assignments. Every thread has an owner, set in one click on the web or one tap from a notification email. A live badge in each notification shows who has the thread now.
- Internal notes and @mentions. Next to the email, never sent to the sender.
- Works from any mailbox. Notifications as they arrive or as daily/weekly digests. Reply to a notification and the answer goes out as the collective. If a teammate answered first, your reply is held back and you are told.
- Drafts. Prepare a new email or a reply, share it for review, send when it's right.
- AI agents as members. Invite an agent with a link; it reads, leaves notes and proposes replies through a plain HTTP API (
/llms.txt). It can never send email by itself. - Your own domain. Receive by forwarding or MX, send from
hello@yourdomain.orgonce DKIM/SPF verify. - Rules, tags, contacts, spam, attachments, keyboard shortcuts.
- Full export. Any admin downloads a zip with a browsable offline
inbox.html, the data as JSON and every attachment, and it imports into another instance (see below).
All mail flows through Resend: inbound via a webhook, outbound via its API. There is no IMAP or SMTP server to run.
sender ──► <slug>@your-domain ──► Resend inbound ──► POST /webhooks/resend
│ fetch the full email + raw MIME
▼
route by address → collective
thread · store · rules · auto-assign
│
members' own mailboxes ◄── notifications (Reply-To: a signed per-member address)
│ reply
▼
webhook again → verified member → sent to the original sender as the collective
└─ someone already answered? held back, the member is told
Stack: Node 22, Hono with server-rendered JSX (no client framework, except a small React/Tiptap bundle for the reply editor), libSQL (a local SQLite file, or Turso), mailparser, sanitize-html, satori + resvg for the generated images. Product decisions and history: SPECS.md.
npm install
npm run dev # http://localhost:3000
npm test # node:test, ~300 tests, no networkWithout RESEND_API_KEY, every email (login codes, notifications, outbound replies) is printed to stdout instead of being sent, so the whole flow works offline. Sign-in codes appear in the terminal. To simulate an incoming email:
curl -X POST localhost:3000/webhooks/resend -H 'Content-Type: application/json' -d '{
"type": "email.received",
"data": { "email_id": "t1", "from": "Marie <marie@example.org>",
"to": ["yourcollective@collective.email"],
"subject": "Hello!", "message_id": "<m1@example.org>", "text": "Hi there" }
}'scripts/seed-dev.ts creates a collective with a member and prints a session token.
You need:
- a domain or subdomain for the addresses, e.g.
mail.example.org(collectives becomename@mail.example.org), plus a hostname for the web app, e.g.inbox.example.org; - a Resend account (the free tier is enough to start);
- somewhere to run it: any Docker host (one container, one volume), or Vercel with Turso and Vercel Blob.
- Create an API key with full access →
RESEND_API_KEY. - Domains → Add domain: your mail domain (
mail.example.org). Add the DKIM/SPF records Resend shows you at your DNS provider. - Receiving: enable inbound for that domain and add the MX record Resend gives you. From then on, mail to any
<anything>@mail.example.orgreaches the webhook. - Webhooks → Add endpoint:
https://inbox.example.org/webhooks/resend, eventemail.received. Copy the signing secret →RESEND_WEBHOOK_SECRET.
The image keeps everything (SQLite database, attachments, generated secret) in the /data volume.
git clone https://github.com/opencollective/email.git collective-email && cd collective-email
docker build -t collective-email .
docker run -d --name collective-email -p 3000:3000 -v collective-email-data:/data \
-e BASE_URL=https://inbox.example.org \
-e EMAIL_DOMAIN=mail.example.org \
-e ADMIN_EMAIL=you@example.org \
-e BILLING=off \
-e RESEND_API_KEY=re_xxx \
-e RESEND_WEBHOOK_SECRET=whsec_xxx \
-e RESEND_FROM="Inbox <notifications@mail.example.org>" \
collective-emailPut it behind any HTTPS reverse proxy (Caddy, Traefik, nginx) on BASE_URL. The container has a healthcheck on GET /health and sends digests on its own; no cron needed.
Back up the /data volume (it holds requests.db, attachments/ and .secret).
The app runs as a Vercel function (api/index.js wraps the Hono app, api/og.js serves the generated images; vercel.json routes everything). A function has no persistent disk, so state lives in managed services:
| Concern | Service | Env vars |
|---|---|---|
| Database | Turso (libSQL) | TURSO_DATABASE_URL, TURSO_AUTH_TOKEN |
| Attachments | Vercel Blob, private store | BLOB_READ_WRITE_TOKEN |
| Digests | Vercel Cron → GET /cron/digest hourly (already in vercel.json) |
CRON_SECRET |
| Signing | sessions and one-click links | SECRET (required, 32+ random characters) |
vercel link, set the variables from .env.example, vercel deploy --prod, then point BASE_URL's hostname at the project.
Open BASE_URL, go to /claim, pick an address and confirm your email with the code. The collective is live immediately and you are its admin; invite the others from the banner in the inbox. With BILLING=off every collective has every feature (own domain included) and nothing ever expires. The accounts in ADMIN_EMAIL also get the /admin dashboard.
To use an address on another domain for a collective (hello@example.org), open Settings → Your domain in that inbox: it detects your MX records and tells you whether to forward from your current provider or point MX at Resend.
You can leave collective.email (or move between any two instances) at any time, without losing anything.
-
Export. In the inbox, an admin opens Settings → Data → Download archive (.zip). The zip contains
inbox.html(open it in a browser to read everything offline),data/*.jsonandattachments/. -
Import on your instance, with the same environment variables as the server:
# from a checkout npm run import -- ~/Downloads/yourcollective.zip # or inside the Docker container docker cp yourcollective.zip collective-email:/data/ docker exec collective-email node dist/cli-import.js /data/yourcollective.zip
Options:
--slug newaddressto import under another address,--name "New name".The importer creates the collective and brings over members (with their roles and notification settings), threads, messages, drafts, internal notes, tags, assignment history and attachments. All ids are remapped, so it works on an instance that already has other collectives. It refuses to merge into an address that already exists. Agents are not imported: their API tokens belong to the old instance, so invite them again.
-
Sign in. Members sign in with the same email address as before. No new invitations are needed.
-
Move the mail.
- Your own domain (
hello@example.org): set up the domain on the new instance (Settings → Your domain) and point your forward or MX records there. The people writing to you notice nothing. - An
@collective.emailaddress: that domain stays with the hosted service. Tell your contacts the new address, and write to hello@collective.email to arrange forwarding while they update.
- Your own domain (
-
Close the old inbox from Settings → Data once you're done. It can be reopened for 30 days, then it is deleted.
The same archive format imports into collective.email, so moving back works the same way.
All configuration is environment variables; see .env.example for a commented template.
| Variable | Required | Purpose |
|---|---|---|
BASE_URL |
yes | Public URL of the web app, used in every link and email |
EMAIL_DOMAIN |
yes | Domain of the collective addresses (<slug>@EMAIL_DOMAIN) |
ADMIN_EMAIL |
yes | Comma-separated platform admins (the /admin dashboard) |
RESEND_API_KEY |
yes | Sending, receiving (fetching inbound emails) and custom domains |
RESEND_WEBHOOK_SECRET |
yes | Verifies inbound webhook signatures |
RESEND_FROM |
Sender of app emails (codes, notifications). Default collective.email <notifications@EMAIL_DOMAIN> |
|
SECRET |
on Vercel | Signs sessions and links. Generated into DATA_DIR/.secret when unset elsewhere |
BILLING |
off for a self-hosted instance: no trials, plans or payments |
|
DATA_DIR |
Local database, attachments and secret. Default ./data (/data in Docker) |
|
TURSO_DATABASE_URL, TURSO_AUTH_TOKEN |
Use Turso instead of the local SQLite file | |
BLOB_READ_WRITE_TOKEN |
Store attachments in Vercel Blob instead of DATA_DIR |
|
CRON_SECRET |
Protects /cron/digest (Vercel sends it automatically) |
|
TZ, DIGEST_HOUR |
Timezone and local hour of the daily/weekly digests (default 8) | |
SIGN_REPLIES |
false to stop appending "— Name, for Collective" to replies |
|
STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET |
Only for running a paid service like collective.email | |
OPENCOLLECTIVE_TOKEN |
collective.email only: verifies owners of opencollective.com names |
src/
index.ts Node/Docker entry (serves static files, runs the digest timer)
app.ts the Hono app (also used by api/index.js on Vercel)
web/ routes and server-rendered pages
client/ the reply editor (bundled to public/static/composer.js)
ingest.ts inbound email: threading, rules, auto-assign
outbound.ts replies, forwards, composed emails
notify.ts notification emails and digests
agents.ts the agent API
export.ts the archive zip; import.ts + cli-import.ts read it back
db.ts schema (additive migrations at startup) and queries
test/ node:test suites (npm test)
scripts/ development and maintenance scripts
api/ Vercel function entries
video/ the Remotion promo video
Contributions are welcome: open an issue or a pull request. Run npm run typecheck && npm test before submitting.
GNU Affero General Public License v3.0 or later. You may run, modify and redistribute it; if you offer a modified version as a network service, you must make its source available to its users.