Your inbox, on your own domain.
A self-hosted, open-source email client that sends and receives on your own custom domain via Resend — no Gmail, no Proton, no big-name provider. Ships as one Docker image on a self-hosted Convex backend.
vemail turns a domain you already own + a Resend account into a real, private inbox — the send/receive experience of Gmail or Proton, but you host it and you own the data. Point your domain's mail at Resend, connect it in the dashboard, and every address you claim lands in a clean, fast inbox you control.
- Real send + receive on your own domain — no forwarding hacks, no "via gmail.com".
- Live, reactive inbox — powered by Convex subscriptions, new mail appears instantly with no refresh and no polling, and read/starred state syncs across your devices in real time.
- Multi-user, private inboxes — invite others; each person owns their own
prefix@domainaddresses and sees only their own mail. Invite-only (no open registration). - Privacy-first — remote images and tracking pixels blocked by default, sender SPF/DKIM/DMARC trust badges, and inbound HTML sanitized on arrival.
- A proper client — rich-text composer, drafts (autosaved), attachments, threaded conversations, reply / reply-all / forward, undo-send, labels, star, archive, trash, and a ⌘K command palette that filters the folder you're in.
- Guided setup — vemail shows you the exact DNS records to add and polls until your domain verifies.
- Encrypted credentials — your Resend key + webhook secret are AES-256-GCM encrypted at rest; no API keys in your environment files.
- 2FA, light/dark themes, and a PWA.
vemail is not an air-gapped app. Resend delivers your inbound mail by calling a webhook, so your instance must be reachable from the public internet over HTTPS. If it can't be reached from the outside, you can send but you will not receive mail. Plan to run it behind a reverse proxy with a real domain and TLS.
- Docker + Docker Compose, and
openssl. - A domain you control (any registrar) + a free Resend account.
- A public HTTPS entry point (reverse proxy). Two Convex origins must be publicly reachable — see Deploy.
git clone https://github.com/sayedhfatimi/vemail && cd vemail
sh scripts/setup.sh # generates the 4 secrets into .env (mode 600)
# edit .env: set CONVEX_PUBLIC_URL, SITE_URL, CONVEX_SITE_PUBLIC_URL for your host
docker compose up -dscripts/setup.sh generates BETTER_AUTH_SECRET, a pinned CONVEX_INSTANCE_SECRET, the
matching CONVEX_SELF_HOSTED_ADMIN_KEY, and the VEMAIL_ENCRYPTION_KEY that encrypts your
Resend credentials. Pinning the instance secret keeps the admin key valid across redeploys
— no deploy→generate-key→redeploy dance. Deploy onto a fresh vemail-convex-data
volume so the pinned secret matches.
Self-hosted Convex serves two origins and both must be publicly reachable behind your proxy:
| Port | Purpose | Env var |
|---|---|---|
| 3210 | API / cloud — the dashboard/browser talks to this | CONVEX_PUBLIC_URL |
| 3211 | HTTP actions / site — Resend delivers inbound webhooks here | CONVEX_SITE_PUBLIC_URL |
| 3000 | the vemail web app | SITE_URL |
The Resend inbound webhook URL to configure is <CONVEX_SITE_PUBLIC_URL>/resend-webhook
— and once you set CONVEX_SITE_PUBLIC_URL, vemail shows you the exact URL (with a copy
button) in Settings → Administration.
If your domain's DNS is on Cloudflare, a Cloudflare Tunnel is the simplest way to expose vemail's three origins publicly over HTTPS — no open ports, no manual reverse proxy, no certificates to manage.
-
Install
cloudflared(sudo pacman -S cloudflared,brew install cloudflared, orsudo apt install cloudflared). -
In the Cloudflare dashboard → Zero Trust → Networks → Tunnels → Create a tunnel (cloudflared), name it (e.g.
vemail), and run the connector command it gives you on the host. (That token is a secret — don't commit it.) -
Add three public hostnames to the tunnel, mapping subdomains on your domain to vemail's local ports (Cloudflare creates the DNS records for you):
Public hostname Service mail.yourdomain.comhttp://localhost:3000convex.yourdomain.comhttp://localhost:3210convex-http.yourdomain.comhttp://localhost:3211 -
Set your
.envto match, thendocker compose up -d:SITE_URL=https://mail.yourdomain.com CONVEX_PUBLIC_URL=https://convex.yourdomain.com CONVEX_SITE_PUBLIC_URL=https://convex-http.yourdomain.com
Your Resend webhook URL is then
https://convex-http.yourdomain.com/resend-webhook.
Prefer a config file? The same three ingress rules work in ~/.cloudflared/config.yml:
tunnel: <TUNNEL_ID>
credentials-file: /root/.cloudflared/<TUNNEL_ID>.json
ingress:
- hostname: mail.yourdomain.com
service: http://localhost:3000
- hostname: convex.yourdomain.com
service: http://localhost:3210
- hostname: convex-http.yourdomain.com
service: http://localhost:3211
- service: http_status:404For a step-by-step walkthrough of Cloudflare Tunnels, see this guide: Cloudflare Tunnels for local dev.
| Var | Required | Purpose |
|---|---|---|
BETTER_AUTH_SECRET |
✓ | session signing (generated) |
CONVEX_INSTANCE_SECRET |
✓ | pins the backend so the admin key is stable (generated) |
CONVEX_SELF_HOSTED_ADMIN_KEY |
✓ | lets the app deploy Convex functions (generated) |
VEMAIL_ENCRYPTION_KEY |
✓ | AES-256-GCM key that encrypts Resend creds (generated) |
CONVEX_PUBLIC_URL |
✓ | browser-reachable backend API (port 3210) |
CONVEX_SITE_PUBLIC_URL |
✓ | public HTTP-actions origin (port 3211) for webhooks |
SITE_URL |
✓ | public URL of the vemail app |
POSTGRES_URL |
– | external Postgres instead of the bundled SQLite volume |
ANALYTICS_SCRIPT_URL / ANALYTICS_SITE_ID |
– | optional privacy-friendly analytics |
Your Resend API key, webhook secret, and domains are NOT set here — the admin enters them in the dashboard (Settings → Administration) and they're stored encrypted.
- Open the app and create the admin account (shown only on a fresh instance).
- Settings → Administration → connect Resend (paste API key + webhook secret).
- Add your domain; add the DNS records vemail shows; wait for verification.
- Claim an address (Settings → Your addresses), then invite others.
The full Resend + DNS + webhook walkthrough is in docs/USER-GUIDE.md.
docker compose pull && docker compose up -ddocker compose up -d convex # boot only the backend
bunx convex dev --once # push functions + generate types
bun install && bun run dev # app on http://localhost:3000Codegen needs a running backend — there is no always-on dev deployment. convex/_generated
is committed and never hand-edited (see CLAUDE.md).
vemail's M1 is a complete daily-driver. Contributions toward what's next are very welcome:
- Filters / rules engine (auto-label, archive, route inbound)
- Snooze, scheduled send, vacation auto-responder
- Contacts / address book with autocomplete + auto-collect
- Spam / junk handling + per-recipient bounce suppression
.eml/ mbox import & export- Message templates / snippets
- Catch-all addresses (receive all
*@domain) - Multi-admin management (promote/demote)
Issues and PRs welcome. Please keep the four quality gates green — biome check,
tsc --noEmit, bun run test, and bun run build — and read CLAUDE.md for the
project conventions (bun-only, self-hosted-Convex codegen, commit/tag rules).
Everything (database + file storage) lives in the vemail-convex-data Docker volume — back
it up as a unit.
MIT.