[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-1842":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":16,"subscribersCount":16,"size":16,"stars1d":17,"stars7d":15,"stars30d":18,"stars90d":16,"forks30d":16,"starsTrendScore":19,"compositeScore":20,"rankGlobal":10,"rankLanguage":10,"license":21,"archived":22,"fork":22,"defaultBranch":23,"hasWiki":22,"hasPages":22,"topics":24,"createdAt":10,"pushedAt":10,"updatedAt":28,"readmeContent":29,"aiSummary":30,"trendingCount":16,"starSnapshotCount":16,"syncStatus":17,"lastSyncTime":31,"discoverSource":32},1842,"saasmail","choyiny\u002Fsaasmail","choyiny","Self-hosted email server for SaaS teams on Cloudflare Workers","",null,"TypeScript",198,40,4,7,0,2,32,6,55.54,"Apache License 2.0",false,"main",[25,26,27],"cloudflare-workers","email","saas","2026-06-12 04:00:11","\u003Cp align=\"center\">\n  \u003Cimg src=\"public\u002Fsaasmail-logo.png\" alt=\"saasmail\" width=\"480\" \u002F>\n\u003C\u002Fp>\n\n\u003Cp align=\"center\">\n  \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fchoyiny\u002Fsaasmail\u002Factions\u002Fworkflows\u002Ftest.yml\">\u003Cimg alt=\"Tests\" src=\"https:\u002F\u002Fgithub.com\u002Fchoyiny\u002Fsaasmail\u002Factions\u002Fworkflows\u002Ftest.yml\u002Fbadge.svg\" \u002F>\u003C\u002Fa>\n  \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fchoyiny\u002Fsaasmail\u002Factions\u002Fworkflows\u002Fe2e.yml\">\u003Cimg alt=\"E2E\" src=\"https:\u002F\u002Fgithub.com\u002Fchoyiny\u002Fsaasmail\u002Factions\u002Fworkflows\u002Fe2e.yml\u002Fbadge.svg\" \u002F>\u003C\u002Fa>\n  \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fchoyiny\u002Fsaasmail\u002Factions\u002Fworkflows\u002Fcodeql.yml\">\u003Cimg alt=\"CodeQL\" src=\"https:\u002F\u002Fgithub.com\u002Fchoyiny\u002Fsaasmail\u002Factions\u002Fworkflows\u002Fcodeql.yml\u002Fbadge.svg\" \u002F>\u003C\u002Fa>\n  \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fchoyiny\u002Fsaasmail\u002Freleases\u002Flatest\">\u003Cimg alt=\"Latest release\" src=\"https:\u002F\u002Fimg.shields.io\u002Fgithub\u002Fv\u002Frelease\u002Fchoyiny\u002Fsaasmail?sort=semver\" \u002F>\u003C\u002Fa>\n  \u003Ca href=\"LICENSE\">\u003Cimg alt=\"License: Apache 2.0\" src=\"https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Flicense-Apache%202.0-blue.svg\" \u002F>\u003C\u002Fa>\n  \u003Ca href=\"https:\u002F\u002Fworkers.cloudflare.com\u002F\">\u003Cimg alt=\"Cloudflare Workers\" src=\"https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Fruns%20on-Cloudflare%20Workers-F38020?logo=cloudflare&logoColor=white\" \u002F>\u003C\u002Fa>\n\u003C\u002Fp>\n\n**The centralized inbox for SaaS teams.** One unified timeline per customer — marketing, notifications, and support emails collapsed into a single view, per person.\n\nEvery interaction with a customer matters, and context compounds. saasmail pulls the promo blast, the billing receipt, and the support thread into the same conversation, so anyone on your team can respond with the full history already in hand.\n\nSelf-hosted on Cloudflare Workers. Receive with **Cloudflare Email Workers**. Send with **Cloudflare Email Sending**, **Resend**, or **Bavimail**.\n\n\u003Cimg width=\"5088\" height=\"3106\" alt=\"saasmail-new\" src=\"https:\u002F\u002Fgithub.com\u002Fuser-attachments\u002Fassets\u002F407a8b4e-3ba0-4ed9-ae8a-f39dee861e56\" \u002F>\n\n## Sponsors\n\n\u003Ca href=\"https:\u002F\u002Fgivefeedback.dev\u002Fsaas\">\u003Cimg width=\"200\" height=\"44\" alt=\"givefeedback.dev\" src=\"https:\u002F\u002Fgithub.com\u002Fuser-attachments\u002Fassets\u002F7da9ef06-cc47-4aa5-94b1-2108a302439c\" \u002F>\u003C\u002Fa>\nGiveFeedback.dev uses AI to turn client screen recordings into actionable tasks and prevent scope creep.\n\n## Demo Video\n\nhttps:\u002F\u002Fgithub.com\u002Fuser-attachments\u002Fassets\u002Ffe3a3811-1902-4b0b-8b94-f8c72f1afab4\n\n## Provider Matrix\n\n|               | Cloudflare | Resend | Bavimail |\n| ------------- | ---------- | ------ | -------- |\n| **Sending**   | ✅         | ✅     | ✅       |\n| **Receiving** | ✅         | ❌     | ❌       |\n\nPick one outbound provider at deploy time:\n\n- **Cloudflare Email Sending** — add a `send_email` binding (`EMAIL`) in `wrangler.jsonc` and onboard your domain at [Email Service](https:\u002F\u002Fdash.cloudflare.com\u002F?to=\u002F:account\u002Femail-service).\n- **Resend** — set `RESEND_API_KEY` as a secret.\n- **Bavimail** — set `BAVIMAIL_API_KEY` and `BAVIMAIL_ALIAS_ID` as secrets. The alias ID identifies the sending alias configured in your Bavimail dashboard.\n\nSelection precedence at runtime: **Bavimail** (when both env vars are set) > **Resend** (when `RESEND_API_KEY` is set) > **Cloudflare Email Sending** (when the `EMAIL` binding exists). If none are configured, send attempts return a \"No email provider configured\" error.\n\n## How much does it cost?\n\n**$5\u002Fmonth** for the Cloudflare Workers Paid plan, which includes **3,000 emails per month** of Cloudflare Email Sending at no extra cost. That's it.\n\nNo VM to rent. No sprawling cloud console to learn. Just a domain, a Cloudflare account, and the Workers Paid plan.\n\n## Features\n\n### One Timeline Per Customer\n\nEvery email from a given person — marketing campaigns, transactional notifications, support replies — lands on a single timeline. People are sorted by recency with unread counts, so the customer who needs attention is always on top. Click in to see the latest message, and open the thread sidebar to replay the full history. Messages render as sanitized HTML with a Slack-style reply composer.\n\n### Multi-Inbox with Team Permissions\n\nRun multiple inbound addresses from a single deployment. Admins configure display names per inbox (`support@`, `sales@`, etc.) and assign members to specific inboxes. Members only see email, templates, and sequences scoped to the inboxes they're allowed to access.\n\n### Thread or Chat, Per Inbox\n\nDifferent inboxes call for different UX. Set each inbox to render as **Thread** or **Chat**:\n\n- **Thread** — traditional email threading with subject lines, quoted history, and formatted HTML. The right fit for `marketing@` and `newsletters@`, where context lives inside the message.\n- **Chat** — bubble-style conversation view that strips away subjects and signatures so replies feel like iMessage. The right fit for `support@`, where customers expect a back-and-forth, not a formal thread.\n\nOne deployment, one person timeline, but the interaction model matches the channel.\n\n### Email Templates\n\nCreate reusable HTML email templates with `{{variable}}` interpolation. Edit templates with a live HTML editor, preview rendered output, and send them via the API or the UI. Variables are automatically extracted and validated before sending. Templates are scoped to allowed inboxes.\n\n### Email Sequencing\n\nBuild multi-step drip campaigns. Enroll a contact into a sequence and saasmail sends templated emails on a schedule. Supports step skipping, delay overrides, custom variables, and automatic cancellation when the contact replies. Enrollment is enforced against the member's allowed inboxes.\n\n### User Management\n\nAdmin-controlled onboarding via one-time invite links. New members sign up with email + password, and can register a passkey for passwordless login on subsequent sessions. Roles: `admin` (full access + user management) and `member` (scoped by inbox assignment).\n\n### API Keys\n\nIssue scoped API keys for programmatic access to send email, manage templates, enroll contacts in sequences, and query inbox data. Keys are hashed at rest and follow the `sk_…` format.\n\n## Architecture\n\n| Layer               | Technology                                                                |\n| ------------------- | ------------------------------------------------------------------------- |\n| **Receive email**   | Cloudflare Email Workers                                                  |\n| **Send email**      | Cloudflare Email Sending, Resend, or Bavimail                             |\n| **Runtime**         | Cloudflare Workers + Hono                                                 |\n| **API**             | Zod + `@hono\u002Fzod-openapi` (OpenAPI 3.1)                                   |\n| **Database**        | Cloudflare D1 (SQLite)                                                    |\n| **File storage**    | Cloudflare R2 (attachments)                                               |\n| **Queue**           | Cloudflare Queues (sequence processing)                                   |\n| **Realtime + Push** | Durable Object (`NotificationsHub`, one per user) — WebSockets + Web Push |\n| **Web Push**        | VAPID + `aes128gcm` payload encryption (RFC 8291), implemented in-worker  |\n| **Service Worker**  | `public\u002Fsw.js` — receives push events, renders OS notifications           |\n| **Cron**            | Hourly trigger for sequence email scheduling                              |\n| **Frontend**        | React + Tailwind CSS + TipTap editor                                      |\n| **ORM**             | Drizzle                                                                   |\n| **Auth**            | BetterAuth with passkey support                                           |\n\n### Architecture Diagram\n\n```mermaid\nflowchart LR\n    EmailRouting[\"Email Routing\u003Cbr\u002F>(inbound)\"]\n    EmailSending[\"Email Sending\u003Cbr\u002F>(outbound)\"]\n\n    Worker[\"Worker\"]\n    DO[\"NotificationsHub\u003Cbr\u002F>(Durable Object, per user)\"]\n\n    D1[(\"D1\")]\n    R2[(\"R2\u003Cbr\u002F>(attachments)\")]\n    Q[[\"Queue\u003Cbr\u002F>(sequence processing)\"]]\n\n    EmailRouting --> Worker\n    Worker --> EmailSending\n    Worker --> DO\n    Worker --> D1\n    Worker --> R2\n    Worker \u003C--> Q\n    DO --> D1\n```\n\nThe `NotificationsHub` Durable Object is keyed per user (`idFromName(userId)`). On inbound mail the worker fans out to each recipient's hub, which pushes WebSocket frames to live tabs and sends encrypted Web Push to registered devices. The queue carries scheduled sequence emails — the cron trigger enqueues due steps and a queue consumer in the same worker sends them.\n\n## Quick Start\n\n### Recommended: install with Claude Code\n\nsaasmail ships with two [Claude Code](https:\u002F\u002Fclaude.ai\u002Fclaude-code) skills that do the install and upgrade for you. This is the path we actively maintain — everything in the manual setup below is what the skills automate.\n\n```bash\ngit clone https:\u002F\u002Fgithub.com\u002Fchoyiny\u002Fsaasmail.git\ncd saasmail\nclaude\n```\n\nThen, inside Claude Code:\n\n- **`\u002Fsaasmail-onboarding`** — interactive setup wizard. Walks you through Cloudflare login, creating D1\u002FR2\u002FQueue resources, filling out `wrangler.jsonc` and `.dev.vars`, running migrations, configuring Email Routing, and deploying. Expect ~30–40 minutes; most of that is DNS propagation, not typing.\n- **`\u002Fupdate-saasmail`** — pull the latest upstream changes. Adds the `upstream` remote if missing, rebases your local commits on top, and resolves conflicts in favor of upstream so you don't get stuck. Run this anytime you want to sync with `choyiny\u002Fsaasmail`.\n\nDon't have Claude Code? The manual steps below cover the same ground.\n\n### Manual setup\n\n### Prerequisites\n\n- [Node.js](https:\u002F\u002Fnodejs.org\u002F) v18+\n- [Yarn](https:\u002F\u002Fyarnpkg.com\u002F)\n- [Wrangler CLI](https:\u002F\u002Fdevelopers.cloudflare.com\u002Fworkers\u002Fwrangler\u002F) (`npm install -g wrangler`)\n- A [Cloudflare](https:\u002F\u002Fdash.cloudflare.com\u002F) account with Email Routing available for your domain\n- _Optional:_ a [Resend](https:\u002F\u002Fresend.com\u002F) account and API key (only if you prefer Resend over Cloudflare Email Sending)\n- _Optional:_ a [Bavimail](https:\u002F\u002Fbavimail.com\u002F) account, API key, and alias ID (only if you prefer Bavimail)\n\n### 1. Clone and install\n\n```bash\ngit clone https:\u002F\u002Fgithub.com\u002Fchoyiny\u002Fsaasmail.git\ncd saasmail\nyarn install\n```\n\n### 2. Authenticate with Cloudflare\n\n```bash\nwrangler login\n```\n\n### 3. Create Cloudflare resources\n\n```bash\n# D1 database\nwrangler d1 create saasmail-db\n\n# R2 bucket\nwrangler r2 bucket create saasmail-attachments\n\n# Queue for email sequencing\nwrangler queues create saasmail-sequence-emails\n```\n\n### 4. Configure wrangler\n\nCopy the example config and fill in your values:\n\n```bash\ncp wrangler.jsonc.example wrangler.jsonc\n```\n\nEdit `wrangler.jsonc`:\n\n- Set `account_id` to your Cloudflare account ID\n- Set the `database_id` in `d1_databases` to the ID from step 3\n- Set `BASE_URL` to your deployed URL\n- Set `TRUSTED_ORIGINS` to include your deployed URL\n- If using Cloudflare Email Sending, uncomment the `send_email` binding\n\n### 5. Configure secrets\n\nCopy the example and fill in your values:\n\n```bash\ncp .dev.vars.example .dev.vars\n```\n\nEdit `.dev.vars`:\n\n- `BAVIMAIL_API_KEY` and `BAVIMAIL_ALIAS_ID` — your Bavimail bearer token and alias UUID (only if using Bavimail; both must be set)\n- `RESEND_API_KEY` — your Resend API key (omit if using Cloudflare Email Sending or Bavimail)\n- `BETTER_AUTH_SECRET` — generate a random string (`openssl rand -hex 32`)\n\nFor production, set these as Cloudflare secrets:\n\n```bash\nwrangler secret put BETTER_AUTH_SECRET\nwrangler secret put RESEND_API_KEY      # only if using Resend\nwrangler secret put BAVIMAIL_API_KEY    # only if using Bavimail\nwrangler secret put BAVIMAIL_ALIAS_ID   # only if using Bavimail\n```\n\n### 6. Run migrations\n\n```bash\n# Local development database\nyarn db:migrate:dev\n\n# Production database\nyarn db:migrate:prod\n```\n\nRun the production migration before opening the deployed app for the first setup. If the production D1 database has not been initialized, the onboarding screen will show **Database migration required** with the same command.\n\n### 7. Configure email routing\n\nIn the [Cloudflare dashboard](https:\u002F\u002Fdash.cloudflare.com\u002F), go to your domain's **Email Routing** settings and add a catch-all rule that routes to your saasmail worker.\n\n### 8. Deploy\n\n```bash\nyarn deploy\n```\n\nVisit your deployed URL to create your first admin account. Once signed in, go to **Inboxes** to name your inbound addresses and **Users** to invite additional team members.\n\n## Updating saasmail\n\n### Recommended: `\u002Fupdate-saasmail`\n\nFrom inside Claude Code, run **`\u002Fupdate-saasmail`**. It links the `upstream` remote to `https:\u002F\u002Fgithub.com\u002Fchoyiny\u002Fsaasmail`, fetches the latest, and rebases your local commits on top. Any unresolvable conflicts are auto-resolved in favor of upstream so the sync never gets stuck.\n\n### Manual\n\n```bash\ngit remote add upstream https:\u002F\u002Fgithub.com\u002Fchoyiny\u002Fsaasmail.git  # first time only\ngit fetch upstream\ngit rebase upstream\u002Fmain -X ours\n```\n\nThe `-X ours` flag tells rebase to prefer upstream for conflicting hunks (during a rebase, \"ours\" is the branch being rebased onto). Your local commits are still replayed on top.\n\n## Local Development\n\n```bash\n# Start dev server (frontend + worker)\nyarn dev\n\n# Run tests\nyarn test\n\n# Type-check\nyarn tsc --noEmit\n\n# Generate a migration after schema changes\nyarn db:generate\n\n# Apply migrations locally\nyarn db:migrate:dev\n\n# Seed the local database with mock inboxes, people, and email threads\nyarn db:seed:dev\n\n# Open Drizzle Studio (local)\nyarn db:studio:dev\n```\n\nSince Cloudflare Email Routing can't deliver to `wrangler dev`, the seed script populates `seeds\u002Fdemo.sql` so you can exercise the inbox UI without real inbound email.\n\nThe API is generated from Zod schemas in `worker\u002Fsrc\u002Frouters\u002F` and exposes an OpenAPI 3.1 spec at `\u002Fapi\u002Fdoc` in the running worker.\n\n### End-to-end tests\n\nPlaywright drives the UI against a local `vite dev` running in demo mode (port 8788).\n\n```bash\n# Install Playwright browsers (first run only)\nyarn playwright install chromium\n\n# Run the full E2E suite (wipes and re-seeds the local D1 first)\nyarn test:e2e\n\n# Interactive runner\nyarn test:e2e:ui\n\n# Debug a single spec\nyarn test:e2e e2e\u002Fspecs\u002Fcompose.spec.ts\n```\n\nThe E2E suite **wipes and re-seeds the local D1 database** (`.wrangler\u002Fstate\u002Fv3\u002Fd1\u002F`) every time it runs. If you have hand-seeded dev data you want to keep, re-run `yarn db:seed:dev` after the E2E suite finishes.\n\nRequirements in `.dev.vars`: `DEMO_MODE=1` and `DISABLE_PASSKEY_GATE=true` — both are in `.dev.vars.example`. The suite also expects `http:\u002F\u002Flocalhost:8788` in `TRUSTED_ORIGINS` in `wrangler.jsonc`.\n\n## Configuration\n\n### wrangler.jsonc\n\nYour Cloudflare Workers configuration. Created from `wrangler.jsonc.example`. This file is gitignored so each deployer maintains their own config. Key sections:\n\n- `d1_databases` — D1 database binding\n- `r2_buckets` — R2 bucket for attachments\n- `queues` — Queue for sequence email processing\n- `triggers.crons` — Hourly cron to check for due sequence emails\n- `send_email` (optional) — Binding for Cloudflare Email Sending\n- `vars.BASE_URL` — Your deployed URL (used for OAuth redirects and BetterAuth)\n- `vars.TRUSTED_ORIGINS` — CORS allowed origins\n- `vars.COOKIE_PREFIX` — Prefix for better-auth session cookies\n- `vars.VAPID_PUBLIC_KEY` \u002F `vars.VAPID_SUBJECT` — public VAPID config for\n  browser push notifications. Generate with `yarn vapid:generate` and store\n  the private key via `wrangler secret put VAPID_PRIVATE_KEY`. Leave blank\n  to disable push.\n\nTo rebrand the UI, drop a replacement `public\u002Fsaasmail-logo.png` — it's used as both the favicon and the in-app logo. The `\u002Fsaasmail-onboarding` skill will do this for you interactively.\n\n### .dev.vars\n\nLocal development secrets. Created from `.dev.vars.example`. This file is gitignored.\n\n- `BAVIMAIL_API_KEY` — Bavimail API bearer token (required for Bavimail, must be paired with `BAVIMAIL_ALIAS_ID`)\n- `BAVIMAIL_ALIAS_ID` — Bavimail alias UUID identifying the sending alias (required for Bavimail)\n- `RESEND_API_KEY` — Resend API key (if using Resend)\n- `BETTER_AUTH_SECRET` — Secret for session signing\n- `DISABLE_PASSKEY_GATE` — Local-only: set to `\"true\"` to skip the server-side passkey requirement so you can sign in with email+password during development. **Never set this in production.**\n\n## Roadmap\n\n- **Agentic email steering** — AI-driven conversation flows that intelligently gather information from contacts through multi-turn email exchanges\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md). All participants are expected to follow the [Code of Conduct](CODE_OF_CONDUCT.md). Security issues: see [SECURITY.md](SECURITY.md).\n\nThis repo ships a `CLAUDE.md` at the project root with a few notes the maintainer uses when pairing with [Claude Code](https:\u002F\u002Fclaude.ai\u002Fclaude-code). It's harmless to ignore if you're not using Claude Code.\n\n## License\n\n[Apache License 2.0](LICENSE)\n\nThe name \"saasmail\" and the saasmail logo are used by the original project to identify it. You are free to fork and redistribute the source under the Apache 2.0 license, but please rename your fork (and replace `public\u002Fsaasmail-logo.png`) if you run it as a branded product, so users aren't confused about which project they're installing.\n","saasmail 是一个为SaaS团队设计的自托管邮件服务器，运行在Cloudflare Workers上。它通过将营销、通知和支持邮件整合到一个统一的时间线中，为每个客户提供单一视图，从而简化了客户沟通管理。该项目采用TypeScript编写，支持使用Cloudflare Email Workers接收邮件，并可以通过Cloudflare Email Sending、Resend或Bavimail发送邮件。这种集中化的邮件管理方式特别适合需要保持与客户高效沟通的SaaS产品团队使用，在确保上下文连贯的同时提高了响应速度和协作效率。","2026-06-11 02:46:20","CREATED_QUERY"]