Self-Hosted Deployment Guide
Deploy the full Wraps control plane to your own AWS account. API, dashboard, database, and email infrastructure — everything in your infrastructure.
Deploy the full Wraps control plane to your own AWS account. Your API, your dashboard, your database — everything in your infrastructure with no Wraps servers in the critical path.
Self-hosting deploys the full Wraps control plane — API Lambda, dashboard, SQS workers, DynamoDB, and EventBridge Scheduler — into your AWS account using SST. You fork the Wraps repository, configure a handful of secrets, and run a single command. Everything runs in your account; no Wraps SaaS servers are in the critical path.
Fork the Wraps repo, run pnpm selfhost:deploy, and receive upstream updates on your schedule via a weekly sync PR
Deployed into your AWS account via SST — you own the function, logs, and billing
Bring any Postgres-compatible DB (Neon, Supabase, Railway, self-hosted)
The apps/web dashboard is deployed automatically via SST into your AWS account — no separate hosting needed
Automated upgrades use GitHub Actions OIDC to assume a short-lived AWS role — no stored keys required
One deploy path — from a fork, via SST
pnpm selfhost:deploy from a fork (this guide) deploys the full platform via SST: API Lambda, the dashboard on CloudFront, workers, DynamoDB, and EventBridge Scheduler.
An earlier API-only variant deployed by wraps selfhost deploy was removed in July 2026. The wraps selfhost CLI now only operates on an existing deployment — selfhost login, selfhost connect, selfhost status, selfhost env, selfhost logs.
One self-hosted plane per AWS account — the deploy creates the account-global wraps-selfhost-scheduler-role. If a previous deploy left that role behind, a fresh pnpm selfhost:deploy fails fast rather than colliding partway through.
postgresql:// form, and the role must be able to create schemas — migrations create a drizzle schema to track their own state, so a least-privilege application role usually needs GRANT CREATE ON DATABASE firstThe self-hosted deployment scripts live in the Wraps monorepo. Fork it so you can receive upstream updates on your own schedule.
github.com/wraps-team/wraps and click Fork. Keep the default branch name main.git clone https://github.com/YOUR_ORG/wraps.gitcd wrapspnpm installRun the deploy script from the root of your fork. It provisions all AWS infrastructure via SST, runs database migrations, and writes an .env.selfhost file with the outputs.
# Pass all required values as flags (non-interactive):pnpm selfhost:deploy \ --database-url "postgres://user:pass@your-db.neon.tech/wraps" \ --license-key "wraps_lic_..." \ --yes# Or run without flags — the script will prompt you:pnpm selfhost:deployThe script will:
.env.selfhost with auto-generated secretssst install then deploy the Lambda, dashboard, and supporting AWS resources via SST.env.selfhostIf migrations fail, the infrastructure is still deployed
The deploy prints your API and app URLs, then reports what failed and why (rejected credentials, missing CREATE privilege, TLS, unreachable host). Fix the database and run pnpm selfhost:upgrade to retry — until migrations apply, creating an account will fail.
After the deploy completes, your .env.selfhost will look like this:
# Created automatically by pnpm selfhost:deployDATABASE_URL=postgres://user:pass@your-db.neon.tech/wrapsBETTER_AUTH_SECRET=<auto-generated>UNSUBSCRIBE_SECRET=<auto-generated>LICENSE_KEY=wraps_lic_...SELFHOST_AWS_REGION=us-east-1# Set when --web-domain flag is provided:SELFHOST_WEB_DOMAIN=dashboard.yourdomain.com# Populated after SST deploy completes:# Without --web-domain, NEXT_PUBLIC_APP_URL is a *.cloudfront.net URL:NEXT_PUBLIC_APP_URL=https://d1abc123xyz.cloudfront.netWRAPS_API_URL=https://abc123.lambda-url.us-east-1.on.awsBETTER_AUTH_URL=https://d1abc123xyz.cloudfront.net# Populated if email infrastructure was found:WRAPS_EMAIL_ROLE_ARN=arn:aws:iam::123456789012:role/wraps-email-roleAUTH_EMAIL_CONFIGURATION_SET=wraps-email-eventsAUTH_EMAIL_FROM=noreply@yourdomain.comOptional flags
Pass --web-domain to set a custom domain for the dashboard (e.g. dashboard.yourdomain.com) and --ai-provider to enable AI email generation — with the Vercel AI Gateway, OpenAI, Anthropic, or AWS Bedrock, none of which is required for the rest of Wraps. See Configuring AI for the flags each one takes. Both can be added later by re-running pnpm selfhost:upgrade. Without --web-domain, NEXT_PUBLIC_APP_URL will be a *.cloudfront.net URL — a custom domain can be configured any time by re-running with --web-domain.
Pass --sentry-dsn to send this deployment's errors to your own Sentry project. Without it, a self-hosted stack reports errors nowhere — the dashboard's error page still shows an Error ID, but that ID only resolves in your own CloudWatch logs. Nothing is ever reported to Wraps either way. If you deploy or upgrade from GitHub Actions, add the same DSN as a SENTRY_DSN repository secret — the workflow rebuilds .env.selfhost from secrets on every run.
The DSN reaches the background workers too — the broadcast sender, the automation processor, the dead-letter consumers and the hourly sweeps. Those are the ones worth having: they swallow their own failures on purpose, so that one bad recipient cannot abort a broadcast and a dead-letter consumer never throws. A broken cron chain, a bookkeeping write that never landed, or an account whose IAM role has drifted is invisible without a DSN configured.
--reroute-events still works on both selfhost:deploy and selfhost:upgrade, but it is the legacy way to get SES events into a self-hosted control plane. New deployments should skip it — see SES event routing below.
Pass --region to deploy somewhere other than us-east-1. The value is persisted to .env.selfhost as SELFHOST_AWS_REGION, and every later selfhost:upgrade reads it back — so the region is set once, not re-passed. No file editing required.
Registering this AWS account with your control plane (step 4 below) is what gets SES events to your self-hosted API. It adds a dedicated EventBridge target rather than repointing the existing one, so both control planes can receive the same events — run the plain wraps platform connect as well and app.wraps.dev stays live alongside your own dashboard. The full sequence:
wraps selfhost connectwraps email upgradewraps platform connectMigrating off --reroute-events
The reroute is the legacy path, kept working for deployments that already use it. An account that went through it is migrated automatically the next time you run wraps selfhost connect: the CLI clears the old reroute and prints a warning, because two targets pointing at the same API would deliver every event twice.
The rebuild takes effect on the next wraps email upgrade, and it replaces rather than renames the EventBridge resources — expect a short gap in event delivery during that step.
Initialise email sending with AWS SES. This creates IAM roles, configures SES, and sets up event processing for open and click tracking.
wraps email initIf you only need SMS, skip this step and run wraps sms init instead.
With the dashboard running, create your account, then authenticate the CLI against your instance and register the AWS connection.
wraps selfhost loginWhy not wraps auth login?
The standard login command authenticates against the Wraps SaaS platform. wraps selfhost login reads your deployment metadata and authenticates against your own dashboard URL instead.
Once signed in, register this AWS account with your self-hosted control plane. This creates the wraps-selfhost-console-access-role IAM role — distinct from the platform's wraps-console-access-role because an IAM trust policy names one principal and your control plane is not the Wraps platform — and enables event streaming from your email infrastructure to the dashboard:
wraps selfhost connectSingle-account deployments
The CLI automatically configures the trust policy to trust your own AWS account — not the Wraps SaaS platform account.
Three additional commands are available once you are signed in:
wraps selfhost update-role — refreshes the permissions on wraps-selfhost-console-access-role after an upgrade adds new ones. It only ever writes the self-hosted role; wraps platform update-role is the equivalent for the platform role and the two leave each other alone.wraps selfhost env — prints the environment variables needed to deploy apps/web against your self-hosted control plane (useful for Vercel or any external hosting).wraps selfhost logout — removes the local session token for your self-hosted instance.Check the health of your self-hosted control plane from the CLI:
wraps selfhost statusA healthy deployment reports the API URL, app URL, AWS region, deployment timestamp, and license key prefix. Open your dashboard URL and confirm your connected AWS account and any email or SMS infrastructure are visible.
You are fully self-hosted
All API calls from the dashboard go directly to your Lambda. No Wraps SaaS servers are involved at runtime. Your license is verified offline — no phone-home required.
New Wraps releases include Lambda updates, database migrations, and dashboard improvements. Choose the upgrade path that fits your workflow.
Pull upstream changes into your fork, then run the upgrade script. It re-deploys the SST stack and runs any pending database migrations automatically.
# Manual upgrade from your fork:pnpm selfhost:upgrade# Runs SST deploy + database migrations automaticallyThe upgrade script redeploys both the Lambda and the dashboard in a single run — no separate steps required.
Your fork includes two pre-built workflows that handle the full update loop hands-free:
selfhost-sync.yml
Runs every Monday at 09:00 UTC (also manually triggerable). Fetches upstream main, opens a PR with up to 20 included commits. Any existing sync PR is automatically closed and superseded. You review and merge when ready.
selfhost-deploy.yml
Manual trigger only. Runs pnpm selfhost:upgrade (or deploy for first-time) with OIDC credentials — no stored AWS keys.
Follow these steps to enable them in your fork.
GitHub disables Actions on new forks by default. Go to your fork's Settings → Actions → General and set "Allow all actions and reusable workflows", then save.
The sync workflow needs one extra step: GitHub marks workflows with cron triggers as disabled in forks even after Actions is enabled, which blocks both the Monday schedule and manual "Run workflow" triggers. Open Actions → Sync from upstream and click Enable workflow on the banner — even if you only ever plan to sync manually. GitHub also suspends cron triggers after 60 days without repository activity; merging the sync PRs counts as activity and keeps it alive.
If a run sits at "Waiting for a runner to pick up this job", the workflows aren't the cause — both use standard GitHub-hosted runners. Check that your organization allows GitHub-hosted runners for this repository (org Settings → Actions) and, for private repositories, that your Actions minutes and spending limit under Settings → Billing aren't exhausted.
See the GitHub OIDC with AWS guide and the AWS IAM OIDC provider docs for full context. In the AWS Console, go to IAM → Identity providers → Add provider:
https://token.actions.githubusercontent.comsts.amazonaws.comThen create an IAM role named wraps-github-deploy-role (or similar) with the following trust policy. Replace YOUR_ACCOUNT_ID, YOUR_GITHUB_ORG and YOUR_FORK_REPO with your AWS account ID, the GitHub username or org that owns your fork, and your fork's repository name — which is not necessarily wraps if you renamed the fork:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::YOUR_ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" }, "StringLike": { "token.actions.githubusercontent.com:sub": "repo:YOUR_GITHUB_ORG/YOUR_FORK_REPO:*" } } } ]}Attach an IAM permissions policy to this role with the permissions SST needs: Lambda, S3, CloudFormation, IAM, SSM Parameter Store, CloudWatch Logs, STS, CloudFront, SQS, DynamoDB, and EventBridge Scheduler. Copy the role ARN — you will need it in the next step.
The deploy workflow already sets permissions: id-token: write at the workflow level, which is required for OIDC token exchange. You do not need to add this manually.
If the deploy fails with "Could not assume role with OIDC: Not authorized to perform sts:AssumeRoleWithWebIdentity"
The role exists but its trust policy does not match the token GitHub sent. The two usual causes: the sub condition still says /wraps while your fork is renamed, or it pins a branch like :ref:refs/heads/main. The deploy job runs in the production environment, so GitHub issues the token with repo:ORG/REPO:environment:production as the subject — not the branch form. The :* suffix above matches both.
In your fork, go to Settings → Secrets and variables → Actions and add the following:
After your first deploy
Open .env.selfhost and copy BETTER_AUTH_SECRET and UNSUBSCRIBE_SECRET into repository secrets. The upgrade workflow reconstructs .env.selfhost from secrets on every run — without these, SST will overwrite your Lambda with empty env vars.
Repository secrets
| Secret | Required | Description |
|---|---|---|
AWS_DEPLOY_ROLE_ARN | Always | ARN of the GitHub deploy role from step 2 |
DATABASE_URL | Always | Postgres connection string — needed by SST on every deploy to set the Lambda env var |
WRAPS_LICENSE_KEY | Always | Your Wraps license key — needed by SST on every deploy |
BETTER_AUTH_SECRET | Always | Session signing secret — auto-generated on first deploy. Copy from .env.selfhost after initial deploy. Rotating this invalidates all active sessions |
UNSUBSCRIBE_SECRET | Always | HMAC secret for unsubscribe links — auto-generated on first deploy. Copy from .env.selfhost after initial deploy |
WRAPS_EMAIL_ROLE_ARN | Optional | IAM role ARN for SES sending. Copy from .env.selfhost after running wraps email init |
AUTH_EMAIL_FROM | Optional | From address for transactional auth emails (password reset, magic links) |
AUTH_EMAIL_CONFIGURATION_SET | Optional | SES configuration set for auth emails. Copy from .env.selfhost after running wraps email init |
AI_GATEWAY_API_KEY | Optional | API key for the AI gateway — enables AI email template generation |
SENTRY_DSN | Optional | DSN for your Sentry project — turns on error reporting for the API, dashboard, browser and background workers. Set it here even if you passed --sentry-dsn locally: the upgrade rebuilds .env.selfhost from these secrets, so a DSN that is missing here is dropped on the next run |
DATABASE_POOL_MAX | Optional | Database connections each function may hold. Defaults to 2. Every function runs on Lambda with its own pool, so this is multiplied by concurrent containers and by the six functions in the stack — raising it is the fastest way to exhaust your database's connection slots. Only raise it if you have measured a reason to |
Repository variables (not secrets)
| Variable | Default | Description |
|---|---|---|
AWS_REGION | us-east-1 | AWS region for deployments. The workflow writes it into the reconstructed .env.selfhost as SELFHOST_AWS_REGION — set it to the region you deployed into locally, or CI will target a different one |
SELFHOST_WEB_DOMAIN | — | Custom domain for the dashboard (e.g. dashboard.yourdomain.com). When set, SST configures CloudFront with this domain |
NEXT_PUBLIC_APP_URL | — | Full URL of the dashboard. Set this to your CloudFront URL (or custom domain) after first deploy — required for correct auth redirects on upgrades if not using SELFHOST_WEB_DOMAIN. Uploaded organization logos store this URL, so changing it later leaves existing logos pointing at the old domain — re-upload them, or keep the old domain resolving |
Go to Settings → Environments → New environment and create one named production. Enable Required reviewers and add yourself — this adds a manual approval step before any deploy job runs, giving you a second confirmation gate even after you click "Run workflow".
wraps-team/wraps. Review the changes and merge when you are ready.upgrade (or deploy for a first-time setup) and click Run.production environment with required reviewers, approve the pending deployment. The job then assumes the OIDC role, runs SST deploy, and applies any pending migrations.Two dashboard features call a language model: AI email template generation and AI workflow generation. Everything else in Wraps works without any AI configuration — if you skip this section, those two features return an error and the rest of the dashboard is unaffected.
By default inference is routed through the Vercel AI Gateway, which needs a gateway account. Set WRAPS_AI_PROVIDER to call OpenAI, Anthropic, or AWS Bedrock directly instead. Every flag below works on both pnpm selfhost:deploy and pnpm selfhost:upgrade, so AI can be added, switched, or rotated at any time by re-running the upgrade.
# Vercel AI Gateway (the default) — one key, routes to any upstream model.pnpm selfhost:upgrade --ai-gateway-api-key "vck_..."# OpenAI directly — no gateway account needed.pnpm selfhost:upgrade --ai-provider openai --openai-api-key "sk-..."# Anthropic directly.pnpm selfhost:upgrade --ai-provider anthropic --anthropic-api-key "sk-ant-..."# AWS Bedrock — no API key at all. Uses this deployment's own IAM role,# so inference bills to the AWS account already running the dashboard.pnpm selfhost:upgrade --ai-provider bedrock# Any OpenAI-compatible endpoint (LiteLLM, vLLM, a corporate proxy).pnpm selfhost:upgrade \ --ai-provider openai \ --openai-api-key "sk-..." \ --openai-base-url "https://llm.internal.example.com/v1"The values are written to .env.selfhost and injected as Lambda environment variables, so you can also edit that file directly and re-run pnpm selfhost:upgrade.
You do not have to pick one. AI_MODEL is optional for every provider, and each one has a sensible default: Claude Sonnet 4 for the gateway, Anthropic, and Bedrock; GPT-5 for OpenAI.
To override it, pass --ai-model with either a catalog key — claude-sonnet-4 or gpt-5, translated automatically to whatever id your provider expects — or a raw provider-native id, which is passed through untouched. Raw ids are the escape hatch for models newer than this release: claude-opus-4-1-20250805 for Anthropic, anthropic/claude-sonnet-4 for the gateway (gateway ids must be namespaced), a full inference profile ARN for Bedrock.
A model your provider cannot serve is caught at boot
Setting AI_MODEL=gpt-5 while WRAPS_AI_PROVIDER=anthropic is a contradiction, and the dashboard reports it in its startup logs — naming the variable to fix — instead of leaving you to discover it when someone clicks Generate. The rest of the dashboard keeps working either way.
Bedrock is available on self-hosted deployments only, and it is the one option with no API key to manage. Inference is called with this deployment's own credentials — the dashboard's Lambda execution role, which the deploy already grants bedrock:InvokeModel. It never assumes a role into the AWS account you connected for SES, because that account is scoped for email and model spend does not belong on it.
Region resolution is WRAPS_AI_REGION first, then AWS_REGION, so you only need --ai-region when you want inference in a different region from the rest of the stack. Most Claude models on Bedrock are only reachable through a cross-region inference profile, so the region prefix (us., eu., apac.) is applied for you.
One step the deploy cannot do for you
Model access on Bedrock is a separate per-account, per-region opt-in in the AWS console under Bedrock → Model access. Until you enable the model there, calls fail with AccessDeniedException — which reads exactly like a credentials problem and sends you off to re-check IAM that was never wrong. IAM is already granted; if you see that error, it is almost certainly model access.
The deploy script creates and maintains .env.selfhost in your repo root. These values are read by SST at deploy time and injected as Lambda environment variables. If you upgrade via the GitHub Actions workflow, every value here must also be set as a repository secret or variable — the upgrade job rebuilds .env.selfhost from scratch on a fresh runner, so anything that only lives in your local file is dropped on the next automated upgrade.
| Variable | Required | Description |
|---|---|---|
DATABASE_URL | Yes | Postgres connection string for the control plane database. If your provider offers a connection pooler, use its pooled endpoint here — PlanetScale exposes PgBouncer on port 6432 and Neon offers a -pooler host. The same URL is used for migrations, and a transaction-mode pooler is fine for them: Drizzle takes no advisory locks and applies each run inside a single transaction |
LICENSE_KEY | Yes | Your Wraps enterprise license key (wraps_lic_...) |
BETTER_AUTH_SECRET | Yes | Secret for session signing — auto-generated on first deploy, do not rotate without invalidating all active sessions |
UNSUBSCRIBE_SECRET | Yes | HMAC secret used to sign unsubscribe links in outbound emails — auto-generated on first deploy |
NEXT_PUBLIC_APP_URL | Yes | Full URL of your dashboard (e.g. https://dashboard.yourdomain.com) — used for CORS and auth redirects. Set automatically after deploy |
WRAPS_API_URL | Yes | Lambda function URL — set automatically after first deploy |
BETTER_AUTH_URL | Yes | Same as NEXT_PUBLIC_APP_URL — set automatically after first deploy |
SELFHOST_AWS_REGION | Yes | AWS region the SST stack deploys into — written on first deploy from --region (default us-east-1) and read back by every later upgrade. Changing it after deploy strands the existing stack in the old region |
SELFHOST_WEB_DOMAIN | Optional | Custom domain for the dashboard — sets AUTH_EMAIL_FROM to noreply@yourdomain.com automatically. Pass via --web-domain flag |
WRAPS_EMAIL_ROLE_ARN | Optional | IAM role ARN for SES sending — populated automatically when email infrastructure is present |
AUTH_EMAIL_FROM | Optional | From address for transactional auth emails (password reset, magic links). Must be a verified SES sender |
AUTH_EMAIL_CONFIGURATION_SET | Optional | SES configuration set name for auth emails — populated automatically when email infrastructure is present |
WRAPS_SSO_TRUSTED_ORIGINS | Optional | Comma-separated identity-provider origins the SSO plugin is allowed to reach for OIDC discovery, e.g. https://login.yourcompany.com,https://*.internal.yourcompany.com. Okta, Entra ID, Google Workspace, Auth0, OneLogin, JumpCloud, Ping and WorkOS are trusted out of the box — set this for an Okta custom domain, a self-hosted Keycloak, or any other IdP. Saving an SSO provider whose issuer is not trusted fails with an untrusted-discovery-URL error |
AI_GATEWAY_API_KEY | Optional | API key for the AI gateway — enables the AI email template generation feature. Pass via --ai-gateway-api-key flag |
WRAPS_AI_PROVIDER | Optional | Which inference backend serves the template and workflow AI. Defaults to gateway (Vercel AI Gateway). Set openai to call OpenAI directly instead — no gateway account needed. Pass via --ai-provider on deploy or upgrade |
OPENAI_API_KEY | Required with openai | Your OpenAI API key. Only read when WRAPS_AI_PROVIDER=openai; the gateway provider does not use it. Pair with OPENAI_BASE_URL to target an OpenAI-compatible endpoint such as a corporate proxy, LiteLLM, or vLLM |
ANTHROPIC_API_KEY | Required with anthropic | Your Anthropic API key, for calling Anthropic directly without a gateway. Only read when WRAPS_AI_PROVIDER=anthropic; the gateway provider does not use it |
WRAPS_AI_REGION | Optional | Region for WRAPS_AI_PROVIDER=bedrock, which is available on self-hosted deployments only. Falls back to AWS_REGION. Bedrock uses this deployment's own credentials, never a role in your SES-connected account, so inference bills to the account running the dashboard. One step the deploy cannot do for you: enable each model under Bedrock → Model access for that account and region. IAM is already granted, so an AccessDeniedException almost always means the model was never enabled |
SENTRY_DSN | Optional | DSN for your Sentry project — turns on error reporting for the API Lambda, the dashboard, and the background workers that send broadcasts and run automations (the browser copy, NEXT_PUBLIC_SENTRY_DSN, is derived from it at build time). Pass via --sentry-dsn on deploy or upgrade, or set a SENTRY_DSN repository secret for the Actions workflow. Unset means errors are reported nowhere |
AI_MODEL | Optional | Override the model used for AI features. Accepts a catalog key, which is translated for whichever provider is active — claude-sonnet-4 or gpt-5 — or a raw provider-native id, which is passed through unchanged. Gateway ids must be namespaced (anthropic/claude-sonnet-4); a bare model name is only valid as a catalog key |
DATABASE_POOL_MAX | Optional | Database connections each function may hold. Defaults to 2. The stack runs six Lambda functions and each warm container keeps its own pool, so the real connection count is this number multiplied by containers and by functions. If you see too many connections or remaining connection slots are reserved, move DATABASE_URL to your provider's pooled endpoint before raising this |
DATABASE_DIRECT_URL | Required if your database is behind a pooler | A direct (non-pooled) Postgres URL, used only by the index-creation scripts in packages/db/scripts/. Those run CREATE INDEX CONCURRENTLY, which a transaction-mode pooler cannot execute — PlanetScale PSBouncer on port 6432 and PgBouncer generally. Usually the same host on port 5432. Leave unset if DATABASE_URL is already direct. Without it those scripts fail with cannot run inside a transaction block, and your database silently runs without the indexes that keep large broadcasts and the email log fast |
ALERT_EMAIL | Optional | Email address for infrastructure alerts — subscribes to an SNS topic backing three CloudWatch alarms: one on messages landing in the batch dead-letter queue, one on the oldest message on the batch queue sitting for 15+ minutes (a broadcast is a self-propagating chain of one message at a time, so either signals a stuck send), and one on messages landing in the workflow dead-letter queue. AWS emails a subscription confirmation link to this address after deploy — accept it, or alarms fire with nowhere to deliver. Set it as a repository variable for Actions-driven upgrades, or the subscription is deleted on the next one and the alarms fire into an empty topic |
SELFHOST_ACCOUNT_HEALTH_ENABLED | Optional | Set to false to disable the hourly SES account-health check. On by default: it assumes each connected AWS account's role and checks for sending paused, an SES enforcement status other than healthy, and bounce/complaint rates approaching AWS's thresholds — the kind of problem that otherwise surfaces only when AWS pauses the account mid-send |
SELFHOST_EVENT_FEED_STALENESS_ENABLED | Optional | Set to false to disable the hourly check for SES event feeds that have gone silent while sends are still happening. On by default: without it, a broken event pipeline is invisible — sends keep going, the dashboard timeline freezes, and bounce/complaint handling goes blind, with no alert. The in-dashboard flag on the AWS account still appears either way. The check emails the org owner, and this requires AUTH_EMAIL_FROM to deliver: with it unset, the send falls back to an unverified hello@wraps.dev sender, SES rejects it, and the check silently retries every hour without ever reaching anyone |
SELFHOST_WORKFLOW_REAPER_ENABLED | Optional | Set to false to disable the hourly sweep that fails workflow executions stuck past their scheduled resume. On by default: it is a backstop for lost EventBridge Scheduler deliveries — disabling it means a stuck execution stays stuck indefinitely |
Add DKIM, SPF, and DMARC records to start sending from your own domain.
Domain verification →SES starts in sandbox mode. Request production access to send to any recipient.
Production access →Point Wraps at the Vercel AI Gateway, OpenAI, Anthropic, or AWS Bedrock, then run pnpm selfhost:upgrade to enable AI-powered email template and workflow generation.
Remove the SST-managed control plane (Lambda, CloudFront, S3, SQS, DynamoDB, EventBridge Scheduler) and clears local metadata. Email infrastructure created by wraps email init (SES config sets, IAM roles, EventBridge rules) is managed separately and must be removed via the CLI or AWS Console.
pnpm selfhost:destroy