Operate
Running a hosted service
Plans, Stripe billing and operator setup — how app.squadmail.dev runs. Self-hosted instances don't need any of this.
Squadmail Cloud at app.squadmail.dev runs the same code as every self-hosted deployment. Setting STRIPE_SECRET_KEY switches the instance into hosted mode:
- every organization needs a plan (Hobby, Starter, Team, Business — defined in
src/lib/plans.ts), - the plan decides limits and features through
getEntitlements(orgId), - there are no shared inbox domains: every workspace receives and sends on its own domain(s), so one customer’s traffic never touches another’s domain reputation,
- every workspace sends through its own SES tenant (see below),
- instance setup (Amazon SES) is no longer possible from the dashboard — the operator does it once with two CLIs.
Without STRIPE_SECRET_KEY none of this exists: no plans, no paywall, no Stripe calls. Self-hosting stays lean.
1. Amazon SES
node --env-file=.env.production scripts/ses-setup.ts # pnpm ses:setup Needs AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, APP_URL and BETTER_AUTH_SECRET (same value as the Worker secret). It creates the S3 bucket, the SNS topic with its HTTPS subscription and the SES receipt rule. Re-running is safe.
Reputation isolation
One AWS account sends for every customer, so one bad sender must not get the whole account paused. Squadmail uses SES tenants:
- When a workspace adds a domain, Squadmail creates the tenant
sqm-<workspace id>with its own configuration set and its own suppression list, and links the domain to it. - Every message the workspace sends (sends, approved agent mail, forwards) carries that tenant, so SES tracks bounce and complaint rates per workspace.
- New tenants get SES’s standard reputation policy: on high-severity findings SES pauses only that tenant. Sends then fail with
403 sending_paused, the dashboard shows a banner under Settings → Domains, and receiving keeps working. - Domain reputation is isolated anyway — every workspace sends from its own DKIM-signed domain.
- Hobby’s daily send cap (20) limits how much damage a new account can do before SES has enough volume to judge it.
Review findings and resume a tenant in the SES console (Tenants) or with aws sesv2 update-reputation-entity-customer-managed-status. SES also emits Sending Status Disabled and Advisor Recommendation Status Open events to EventBridge — route them to email/Slack for alerting. Tenants cost nothing for the first 1,000 per region, then $0.005 per tenant and month.
Tenants isolate enforcement, not the account entirely: their combined bounce and complaint rates still count towards your account’s standing. Keep an eye on the SES account dashboard.
2. Stripe
STRIPE_SECRET_KEY=sk_live_… APP_URL=https://app.squadmail.dev pnpm stripe:setup Idempotent. It creates one product per plan (squadmail_<plan>) with monthly and yearly prices (lookup keys squadmail_<plan>_<monthly|yearly>), the webhook endpoint APP_URL/hooks/stripe and the customer-portal configuration (switch plans with proration, cancel at period end, invoices, payment methods). When a price changes in plans.ts, run it again — the lookup key moves to the new price and existing subscriptions keep theirs until changed.
Then set the Worker secrets:
wrangler secret put STRIPE_SECRET_KEY
wrangler secret put STRIPE_WEBHOOK_SECRET # printed by stripe:setup on first run Optional vars: STRIPE_AUTOMATIC_TAX=true once Stripe Tax is configured, STRIPE_PORTAL_CONFIGURATION if the script asks for it.
How billing works
- Sign-up from the pricing page carries
?plan=…&interval=…straight into Stripe Checkout; afterwards the dashboard asks for the workspace’s domain. - Stripe webhooks (
checkout.session.completed,customer.subscription.*) mirror the subscription into D1.active,trialingandpast_due(grace while Stripe retries) keep the plan. - Without a live plan the dashboard only shows Settings → Plan & billing and the API answers
402 plan_required. - Monthly quotas count from the start of the billing period; Hobby additionally has daily caps. Hitting a quota returns
429 limit_reachedwith the limit. - Retention follows the plan (3 / 30 / 90 / 365 days).
When a plan ends
| When | What happens |
|---|---|
| Day 0 | The workspace’s domains go onto squadmail-suspended-N receipt rules (Stop action) at the top of the SES rule set — no more mail, no S3 object, no Worker run. Sending is blocked. Owners and admins get an email. |
| Day 23 | Final reminder with the deletion date. |
| Day 30 | Cleanup: SES identities, tenant and configuration set, inboxes, emails, attachments and raw mail in R2, API keys, webhooks, outbox, events and usage counters are deleted. Accounts, the workspace, labels and settings stay. A last email confirms it. |
Choosing a plan again before day 30 lifts the block immediately and everything continues where it stopped. The cron (*/15) drives all steps; reminders need MAIL_FROM.
Suggested variables
{
"SIGNUP_MODE": "open",
"REQUIRE_EMAIL_VERIFICATION": "true",
"MAIL_FROM": "Squadmail <noreply@squadmail.dev>",
"TURNSTILE_SITE_KEY": "0x4AAAA…", // + secret TURNSTILE_SECRET
"MAX_ATTACHMENT_MB": "10",
"API_RATE_PER_MIN": "300",
"LABEL_GLOBAL_DAILY_BUDGET": "200000", // kill switch for Workers AI spend
"AI_GATEWAY_ID": "squadmail"
} Monitoring
- Workers Observability is enabled in
wrangler.jsonc; rejected mail, failed jobs and billing changes are written to the event log. - Route Clef through AI Gateway to see spend per day and add caching or rate limits without redeploying.