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. For delivery failure
diagnosis, see the canonical
Email failures runbook.
Selecting a provider
EMAIL_PROVIDER is the single switch: without it no email is sent, and ambient
credentials (RESEND_API_KEY, AWS configuration, or SMTP_URL) never select a
provider on their own. Choosing or changing the provider is always an explicit
decision.
Weekly reports
The Temporal worker attempts weekly reports for every project on Monday at 06:15 UTC by default. SetSCHEDULED_MAINTENANCE_ENABLED=1 on the worker to
enable the schedule. Use WEEKLY_DIGEST_CRON to override its timing.
A project report is sent only when:
- the project has at least one completed or failed rank check from the preceding seven days;
- at least one owner or member has a verified email address and keeps Weekly report email enabled in notification settings;
- a report has not already been sent for the project during the preceding six days; and
- the configured email provider is available and the
bulkemail budget has not been exhausted.
Resend
- Create an API key in the Resend dashboard.
- Verify your sending domain (Resend guides you through the DNS records).
- Configure:
SMTP
The built-in SMTP transport uses Nodemailer and acceptssmtp:// and smtps://
connection URLs. Configure:
SMTP_URL as a secret because it normally contains relay credentials. If
the username or password contains URL-reserved characters, percent-encode those
characters in the connection URL.
The transport opens one connection per send and applies a 10-second connection
and socket timeout. TLS mode, authentication, relay limits, and sender
verification are controlled by the SMTP service.
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 all 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 preserve the available provider status for logging and retry classification.
- The waitlist marketing audience sync is Resend-only: it always requires
RESEND_API_KEY, even when transactional email goes through SES or SMTP. 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. To get another provider into bisibility itself, do not send a pull request - the project does not accept them. Instead, file a feature request describing the provider, its authentication, and its failure modes; accepted requests are implemented by the core team, and requests that materially shape what ships may receive changelog credit. 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
compose.yaml,compose.worker.yaml, andscripts/deploy/bake-runtime-env.mjs, and document them in.env.example.