Self-hosting
Run your own server on your own Cloudflare and Google Cloud accounts. Your mail, tokens and settings then never touch anyone else's infrastructure.
This takes about 30 minutes. Setup steps run from a clone of the repository; the README.md there has the same steps.
You'll need
- macOS with Xcode Command Line Tools, Node.js, pnpm and Rust (to build the app)
- Terraform, and the
gcloudCLI logged in withgcloud auth application-default login - A Google Cloud project with billing linked (Pub/Sub requires it)
- A Cloudflare account. Log in with
pnpm --filter worker exec wrangler login.
pnpm install
1. Create the Cloudflare resources
From apps/worker:
pnpm exec wrangler d1 create echelon-inbox
pnpm exec wrangler queues create echelon-triage
pnpm exec wrangler r2 bucket create echelon-inbox-updates
pnpm exec wrangler vectorize create echelon-inbox-threads --dimensions=768 --metric=cosine
Put the database_id that d1 create prints into apps/worker/wrangler.jsonc. The Vectorize index powers AI search; to skip it, remove the vectorize binding from wrangler.jsonc.
Deploy once to learn the Worker's URL, such as https://echelon-inbox.<you>.workers.dev. Sign-in won't work yet.
pnpm --filter worker run deploy
2. Set up Google Cloud
APIs and Pub/Sub
cd infra/gcp
cp terraform.tfvars.example terraform.tfvars
Set project_id and push_endpoint = "https://<your-worker>/push/gmail", then:
terraform init
terraform apply
This enables the Gmail, People, Drive, Calendar and Pub/Sub APIs, creates the gmail-push topic, and creates a push subscription that forwards Gmail notifications to your Worker, signed by a service account. Note the gmail_topic and push_auth_service_account outputs for step 4.
If the project is in a Google Workspace organization and the publisher grant fails with a domain-restriction error, set allow_gmail_push_publisher = true and apply again.
OAuth consent screen and client
Google has no API for these, so open the oauth_console_url output and do them by hand:
- Consent screen: choose External, add yourself as a test user, then click Publish app. You don't need Google's verification for personal use.
- OAuth client: type Web application, with redirect URIs
https://<your-worker>/auth/callbackandhttp://localhost:8790/auth/callback.
Leaving the consent screen in Testing makes refresh tokens expire after 7 days, and you'll have to sign in again every week.
3. Set the Worker's secrets
cd apps/worker
pnpm exec wrangler secret put GOOGLE_CLIENT_ID
pnpm exec wrangler secret put GOOGLE_CLIENT_SECRET
pnpm exec wrangler secret put TOKEN_ENCRYPTION_KEY # paste the output of: openssl rand -base64 32
Keep TOKEN_ENCRYPTION_KEY safe. Without it, stored refresh tokens can't be decrypted and everyone has to sign in again.
Optionally, set a server-wide default model for drafting replies. Any OpenAI-compatible endpoint works. Users can still pick their own in Settings.
pnpm exec wrangler secret put LLM_BASE_URL
pnpm exec wrangler secret put LLM_MODEL
pnpm exec wrangler secret put LLM_API_KEY
4. Configure and deploy
In apps/worker/wrangler.jsonc, under vars:
| Variable | Value |
|---|---|
ALLOWED_EMAIL_DOMAINS | Comma-separated email domains allowed to sign in, such as example.com,gmail.com. Empty allows any Google account, so set it. |
GMAIL_TOPIC | The gmail_topic Terraform output |
PUSH_SERVICE_ACCOUNT | The push_auth_service_account Terraform output |
TRIAGE_MODEL | Workers AI model for triage. Default @cf/cloudflare/clef-flash. |
pnpm --filter worker run db:migrate:remote
pnpm --filter worker run deploy
5. Point the app at your server
Either use the downloaded app and choose Server → Change on the sign-in screen, or build your own copy:
cp apps/desktop/.env.example apps/desktop/.env # set VITE_WORKER_URL to your Worker URL
pnpm dev:desktop
Sign in, then run Triage inbox from the command palette to sort the mail you already have.