An open-source, self-hostable team inbox + ticketing platform for iMessage — and beyond.
Think Beeper for enterprise, with a help desk built in. Share an iMessage number across your team, assign conversations as tickets, use macros, and reply together — all from a clean, fast UI you host yourself.
Comms turns a shared iMessage number into a collaborative support inbox. It connects to BlueBubbles (the open-source iMessage bridge you run on a Mac) and layers a real ticketing workflow on top: assignment, statuses, priorities, internal notes, tags, and macros.
iMessage is the first channel. The architecture is channel-agnostic, with WhatsApp and others planned.
- 📨 Shared inbox — every iMessage conversation in one place, in real time (SSE-powered live updates)
- 🎫 Ticketing — status (open / pending / snoozed / closed), priority, assignment to people, tags
- 📝 Internal notes — discuss a conversation privately without messaging the customer
- ⚡ Macros — one-click canned responses
- 👥 Multi-agent safe — per-conversation send serialization + echo reconciliation so two agents on one number never collide
- 🔌 BlueBubbles bridge — auto webhook registration, Private API feature detection, history backfill
- 🔐 Flexible auth — local email + password (zero config), magic-link email, and Google/GitHub SSO
- 🎨 Clean, enterprise UI — black & white, shadcn/ui + Framer Motion
- 🚀 One-click Railway deploy — almost zero environment variables to set
You'll create one Railway project with four services — Postgres, Redis, a web service, and a worker service — all from this one repo. It takes about 10 minutes, and you don't type a single value yourself.
The whole config in one glance — just 2 variables, the same on both the web and worker services:
DATABASE_URL=${{ Postgres.DATABASE_URL }} REDIS_URL=${{ Redis.REDIS_URL }}Both are one-click references, not values you invent. Everything else is automatic: Comms generates and stores its own app secret on first boot, derives its public URL, and runs database migrations for you. Email, AI, file attachments, and OAuth are optional and added later from inside the app or as extra variables — never required to get started.
You'll need:
- A free Railway account (sign in with GitHub).
- A fork of this repo on your own GitHub account (click Fork at the top of this page).
- A Mac signed into iMessage running BlueBubbles — this is what connects Comms to iMessage. You can set this up after deploying (see How the iMessage bridge works).
Nothing to install on your own computer — the whole app runs on Railway.
- Go to railway.com → New Project → Deploy from GitHub repo → pick your fork of
comms. - Railway creates one service from the repo. We'll make this the web service in step 4.
- In the project, click New → Database → Add PostgreSQL. Leave it named
Postgres.
- Click New → Database → Add Redis. Leave it named
Redis.
Keep the default names
PostgresandRedis— the variable references below use them. If you rename a database, update the references to match.
Open the repo service from step 1, then:
a. Point it at the web config. Settings → Config-as-code / Railway Config File → set the path to:
apps/web/railway.json
This tells Railway to build the Docker image, run database migrations automatically before each deploy, start the web server, and health-check /api/health.
b. Set the web service variables. On the web service → Variables → Raw Editor → paste exactly:
DATABASE_URL=${{ Postgres.DATABASE_URL }}
REDIS_URL=${{ Redis.REDIS_URL }}c. Give it a public URL. Settings → Networking → Generate Domain.
- Click New → GitHub Repo → pick the same
commsrepo again. - Settings → Config File → set the path to:
apps/worker/railway.json
- Variables → Raw Editor → paste the same two lines as the web service:
DATABASE_URL=${{ Postgres.DATABASE_URL }}
REDIS_URL=${{ Redis.REDIS_URL }}- The worker has no public port — do not generate a domain for it.
- Railway builds and deploys all four services. The web service runs migrations automatically on its first deploy.
- Open the web service's domain (e.g.
https://your-app.up.railway.app). You'll see the first-run setup wizard — create your admin account. - Go to Settings → Inboxes → Connect BlueBubbles and paste your BlueBubbles server URL + password (see the next section).
That's it — you're live. 🎉
| Variable | Required? | What to set | Why |
|---|---|---|---|
DATABASE_URL |
✅ Yes | ${{ Postgres.DATABASE_URL }} |
Connects to Postgres. A reference — no value to type. |
REDIS_URL |
✅ Yes | ${{ Redis.REDIS_URL }} |
Queues + realtime. A reference — no value to type. |
| (app secret) | ⚙️ Auto | — | Generated and stored in the database on first boot. Nothing to set. |
| (public URL) | ⚙️ Auto | — | Derived from Railway's generated domain. Nothing to set. |
APP_SECRET |
⬜ Optional | ${{ secret(48) }} (a shared variable) |
Override the auto-generated secret. Recommended for production hardening — keeps the encryption key out of the database. If you set it, set it on both services with the same value. |
ANTHROPIC_API_KEY |
⬜ Optional | your Claude API key | Enables AI summaries, draft replies, and auto-triage. |
S3_* |
⬜ Optional | from a Storage Bucket / S3 | Enables file attachments (see below). |
SMTP_* |
⬜ Optional | your mail provider | Enables magic-link sign-in, invites, notifications. |
GOOGLE_* / GITHUB_* |
⬜ Optional | OAuth app creds | Enables Google / GitHub sign-in. |
See .env.example for the full list with descriptions.
Attachments need S3-compatible storage. The easiest on Railway is a native Storage Bucket (New → Storage Bucket). Then add these to both the web and worker services, mapping the bucket's variables:
S3_ENDPOINT=${{ Bucket.ENDPOINT }}
S3_BUCKET=${{ Bucket.BUCKET_NAME }}
S3_ACCESS_KEY_ID=${{ Bucket.ACCESS_KEY_ID }}
S3_SECRET_ACCESS_KEY=${{ Bucket.SECRET_ACCESS_KEY }}
S3_REGION=auto(Exact variable names depend on the bucket plugin — open the bucket's Variables tab to confirm. Any S3-compatible store works too: Cloudflare R2, Backblaze B2, AWS S3.)
- Web crashes on boot with a DB/Redis error → the variable references didn't resolve. Confirm
PostgresandRedisare the exact service names, and that the three variables are set on the service. - Webhook didn't register when connecting BlueBubbles → your app needs a public URL BlueBubbles can reach. Make sure you generated a domain (step 4d); then in Settings → Inboxes click Re-register webhook.
- Messages don't arrive / send → check the worker service logs; it must be running with the same three variables as web.
iPhone/Mac ⇄ iMessage ⇄ BlueBubbles server (your Mac) ⇄ Comms (web + worker) ⇄ your team
- You run BlueBubbles on a Mac signed into iMessage and expose it (Cloudflare Tunnel, ngrok, or your own domain).
- In Comms, go to Settings → Inboxes → Connect BlueBubbles and paste the server URL + password. Comms verifies the connection, detects whether the Private API is available (reactions, typing, edits), registers an inbound webhook, and backfills recent history.
- Inbound messages flow in via webhook → worker → your shared inbox. Replies are queued and sent through a per-conversation lock so multiple agents stay in sync.
For the richest feature set (reactions, typing indicators, edit/unsend, read receipts), enable the BlueBubbles Private API on the Mac. Comms automatically falls back to basic mode if it isn't available.
A pnpm monorepo, one Docker image, two runtime roles:
apps/
web/ Next.js 15 (App Router) — UI, API routes, server actions, auth
worker/ BullMQ worker — inbound ingestion, outbound sends, attachments, backfill, heartbeats
packages/
db/ Drizzle ORM schema + migrations + client (Postgres)
core/ Shared libs — config, crypto, Redis/queues, S3 storage, realtime, BlueBubbles client
- Database: Postgres via Drizzle ORM
- Queues + realtime: Redis (BullMQ for jobs, pub/sub for live UI updates over SSE)
- Storage: any S3-compatible store (Railway Storage Bucket, Cloudflare R2, MinIO, AWS S3)
- Auth: Auth.js (NextAuth v5) — credentials + magic-link + OAuth
- UI: Tailwind CSS + shadcn/ui + Framer Motion
The fastest way — the whole stack (Postgres + Redis + MinIO + web + worker) in one command:
docker compose up --build # then open http://localhost:3000Or run it with pnpm. Prerequisites: Node 22+, pnpm 10+, and a local Postgres + Redis.
pnpm install
cp .env.example .env # set DATABASE_URL and REDIS_URL (that's all)
pnpm --filter @comms/db build # build the db package
pnpm db:generate # generate the SQL migration from the schema
pnpm db:migrate # apply migrations
pnpm dev # runs web (http://localhost:3000) + workerOpen http://localhost:3000 and complete the setup wizard to create your admin account.
| Command | Description |
|---|---|
pnpm build |
Build all packages and apps |
pnpm dev |
Run web + worker in watch mode |
pnpm db:generate |
Generate a Drizzle migration from schema changes |
pnpm db:migrate |
Apply pending migrations |
pnpm db:studio |
Open Drizzle Studio |
pnpm typecheck |
Type-check the whole repo |
pnpm test |
Run the unit test suite (Vitest) |
./scripts/smoke-test.sh |
Boot the full Docker stack and verify it comes up healthy |
- Outbound attachments & reactions from the UI
- Command palette + keyboard shortcuts
- SLA timers & reporting
- WhatsApp and additional channels
- Saved views & advanced search
- Multi-tenant (hosted) mode
MIT — see LICENSE.