configuration

Cloudflare Resources

Provision the Cloudflare resources Spinupmail expects and wire them into the backend Worker.

Spinupmail depends on a small set of Cloudflare primitives: D1 for relational storage, KV for supporting state, R2 for attachments, and a Durable Object namespace for abuse tracking. The README is the source of truth here: create the resources first, then copy their IDs and names into the backend Worker config.

Provision the required resources

Run the setup commands from packages/backend.

BashCLI command example
cd packages/backend
pnpm exec wrangler d1 create SUM_DB
pnpm exec wrangler kv namespace create SUM_KV
pnpm exec wrangler r2 bucket create spinupmail-attachments
pnpm exec wrangler r2 bucket create spinupmail-attachments-preview
pnpm wrangler queues create spinupmail-integration-dispatches

Record the values each command returns. You will need them when you edit wrangler.toml.

ResourceCreate commandSave these values
D1 databasepnpm exec wrangler d1 create SUM_DBbinding, database_name, database_id
KV namespacepnpm exec wrangler kv namespace create SUM_KVbinding, id
R2 bucketpnpm exec wrangler r2 bucket create spinupmail-attachmentsbucket_name
R2 preview bucketpnpm exec wrangler r2 bucket create spinupmail-attachments-previewbucket_name
Integration dispatch queuepnpm wrangler queues create spinupmail-integration-dispatchesqueue_name

Copy the backend Wrangler config

Start from the example config instead of building the file by hand.

BashCLI command example
cp wrangler.toml.example wrangler.toml

Then update the generated file with the resource values you just created.

TOMLConfiguration example
[[d1_databases]]
binding = "SUM_DB"
database_name = "SUM_DB"
database_id = "YOUR_D1_DATABASE_ID"
 
[[kv_namespaces]]
binding = "SUM_KV"
id = "YOUR_KV_NAMESPACE_ID"
 
[[r2_buckets]]
binding = "R2_BUCKET"
bucket_name = "spinupmail-attachments"
preview_bucket_name = "spinupmail-attachments-preview"

The example config already includes the Durable Object binding and migration:

TOMLConfiguration example
[[migrations]]
tag = "v1"
new_sqlite_classes = ["InboundAbuseCounterDurableObject"]
 
[[durable_objects.bindings]]
name = "ABUSE_COUNTERS"
class_name = "InboundAbuseCounterDurableObject"

Fill the Worker variables

Some runtime configuration lives in [vars] instead of secrets. These values control routing, email limits, and optional product behavior.

Required variables

VariablePurpose
EMAIL_DOMAINSComma-separated inbound domains handled by Email Routing, such as spinupmail.com or spinupmail.com,spinupmail.dev
RESEND_FROM_EMAILVerified sender used for Better Auth verification and password-reset email

Common optional variables

VariableDefault or usage
AUTH_ALLOWED_EMAIL_DOMAINRestricts sign-up and sign-in to one email domain for internal deployments
FORCED_MAIL_PREFIXForces every created or renamed inbox local part to start with this prefix plus -; unset disables the feature
EMAIL_MAX_BYTESMax raw email bytes parsed by the Worker
EMAIL_BODY_MAX_BYTESMax HTML and text body bytes persisted in D1
EMAIL_FORWARD_TOOptional forward target
EMAIL_ATTACHMENT_MAX_BYTESMax size per attachment uploaded to R2
EMAIL_ATTACHMENT_MAX_TOTAL_BYTES_PER_ORGANIZATIONTotal attachment quota per organization, default 104857600
EMAIL_ATTACHMENTS_ENABLEDToggle attachment processing, default true
MAX_ADDRESSES_PER_ORGANIZATIONAddress cap per org, default 10
MAX_RECEIVED_EMAILS_PER_ADDRESSDefault and hard cap for stored emails per inbox, default 100
MAX_RECEIVED_EMAILS_PER_ORGANIZATIONTotal stored-email cap across one org, default 1000
MAX_INTEGRATIONS_PER_ORGANIZATIONIntegration cap per org, default 3
MAX_INTEGRATION_DISPATCHES_PER_ORGANIZATION_PER_DAYDaily integration dispatch cap per org, default 100
INTEGRATION_QUEUE_RETRY_WINDOW_SECONDSRetry window for integration dispatch failures, default 21600
INTEGRATION_QUEUE_BASE_DELAY_SECONDSInitial retry delay for integration dispatches, default 30
INTEGRATION_QUEUE_MAX_DELAY_SECONDSMaximum retry delay for integration dispatches, default 1800
INTEGRATION_QUEUE_JITTER_SECONDSRetry jitter to spread retries, default 10
API_KEY_RATE_LIMIT_WINDOW and API_KEY_RATE_LIMIT_MAXRate limit window and max for x-api-key traffic
AUTH_RATE_LIMIT_WINDOW and AUTH_RATE_LIMIT_MAXBetter Auth global rate limiting
AUTH_CHANGE_EMAIL_RATE_LIMIT_WINDOW and AUTH_CHANGE_EMAIL_RATE_LIMIT_MAXChange-email rate limiting
EMAIL_STORE_HEADERS_IN_DBPersist full headers in D1
EMAIL_STORE_RAW_IN_DBPersist raw MIME in D1
EMAIL_STORE_RAW_IN_R2Persist raw MIME in private R2 for debugging

Domain strategy and routing prep

EMAIL_DOMAINS is the inbound source of truth. If you configure multiple domains, Spinupmail treats them as valid receiving domains and the UI will show a domain selector during address creation. If you configure only one domain, the app uses it automatically.

If you also set FORCED_MAIL_PREFIX, Spinupmail displays that enforced prefix in the create/edit address forms and always applies it on the backend when an address is created or renamed.

If you want Spinupmail to operate as an internal tool, also set AUTH_ALLOWED_EMAIL_DOMAIN. That keeps email/password auth and Google OAuth aligned with one company domain while still letting EMAIL_DOMAINS define where inbound mail is received.