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
- Create an API key in the Resend dashboard.
- Verify your sending domain (Resend guides you through the DNS records).
- Configure:
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: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
Behavior shared by both providers
- Every provider send reserves one recipient from either the
transactionalbudget (sign-in codes, invitations, and waitlist notifications) or thebulkbudget (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 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:- Implement the
EmailProviderinterface fromlib/email/types.tsin a new file underlib/email/providers/: anid, alabel,isConfigured(), and asend()that maps failures toEmailSendErrorwith the provider’s HTTP status (report throttling as status 429 so retries keep working). - Register it in
lib/email/registry.tsand extend theEmailProviderIdunion inlib/email/types.ts. - Add colocated tests mirroring
lib/email/providers/ses.test.ts: message shape, configuration detection, and error mapping, plus a routing case inlib/email/send.test.ts. - Forward any new environment variables in
docker-compose.ymlandscripts/deploy/bake-runtime-env.mjs, and document them in.env.example.