Skip to main content
Bisibility sends transactional email for sign-in codes, team invitations, ranking alerts, and weekly digests. Every message goes through one shared sender that supports two interchangeable providers: Without any email configuration, production refuses to start email-dependent flows, while development and demo instances log the message (including sign-in codes and invite links) to the server console instead.

Selecting a provider

EMAIL_PROVIDER is the single switch: without it no email is sent, and ambient credentials (a RESEND_API_KEY, AWS configuration) never select a provider on their own. Choosing or changing the provider is always an explicit decision.

Resend

  1. Create an API key in the Resend dashboard.
  2. Verify your sending domain (Resend guides you through the DNS records).
  3. Configure:
The free tier is limited (100 emails/day at the time of writing). Alerting and digests across an active team can exceed it quickly; check your expected volume against your plan.

Amazon SES

SES pricing depends on the selected plan and enabled deliverability features. Production SES accounts also have account-specific sending quotas. Bisibility applies its own independent daily budgets before calling the provider, but those budgets do not replace provider quotas or a provider-side spend cap.

1. Verify your domain

In the SES console, create a verified identity for your sending domain and publish the DKIM records it generates. Also add an SPF record if the domain does not have one. Unverified senders are rejected.

2. Leave the sandbox

New SES accounts start in the sandbox: at most 200 emails per 24 hours, and only to verified recipient addresses. Request production access in the SES console (“Request production access”); approval normally takes up to 24 hours and lifts the default quota to 50,000 emails per day.

3. Grant credentials

The SES provider uses the standard AWS SDK credential chain: environment keys, the shared config file, or an IAM role (recommended on EC2/ECS/EKS). The attached policy only needs:
For Docker Compose, pass AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY through your .env; the stack forwards them (plus AWS_REGION) to the app and worker containers.

4. Configure

Watch your bounce and complaint rates in the SES console: AWS pauses accounts whose rates stay high, so keep recipient lists clean.

Behavior shared by both providers

  • Every provider send reserves one recipient from either the transactional budget (sign-in codes, invitations, and waitlist notifications) or the bulk budget (alert email and weekly reports). The default is 1,000 per category per UTC day. Development console logging and unconfigured transports do not consume budget.
  • At a provider price of 0.10per1,000recipients,fullyconsumingbothdefaultseverydaycostsabout0.10 per 1,000 recipients, fully consuming both defaults every day costs about 6 per month. Actual cost depends on the provider, plan, region, and enabled features. These are anomaly brakes, not cost-fitting controls; the provider spend cap remains the outer wall. OTP resend requests are also throttled per email address.
  • Requests time out after 10 seconds and are never retried inside the provider; alert delivery retries centrally with backoff and honors rate-limit responses.
  • Delivery failures carry the HTTP status of the provider response, so rate-limited alert email is requeued instead of dropped.
  • The waitlist marketing audience sync is Resend-only: it always requires RESEND_API_KEY, even when transactional email goes through SES. Without the key it logs and skips, which is fine for most self-hosted installs.

Adding another provider

The transactional sender is deliberately pluggable, so a self-hosted instance can carry its own provider (for example SMTP or Postmark). To get a provider into bisibility itself, do not send a pull request - the project does not accept them. Instead, file a feature spec describing the provider, its authentication, and its failure modes; accepted specs are implemented by the core team and credited in the release notes. Wiring a provider into your own instance takes four steps:
  1. Implement the EmailProvider interface from lib/email/types.ts in a new file under lib/email/providers/: an id, a label, isConfigured(), and a send() that maps failures to EmailSendError with the provider’s HTTP status (report throttling as status 429 so retries keep working).
  2. Register it in lib/email/registry.ts and extend the EmailProviderId union in lib/email/types.ts.
  3. Add colocated tests mirroring lib/email/providers/ses.test.ts: message shape, configuration detection, and error mapping, plus a routing case in lib/email/send.test.ts.
  4. Forward any new environment variables in docker-compose.yml and scripts/deploy/bake-runtime-env.mjs, and document them in .env.example.