Wraps Logo
Guide

Self-Hosted Deployment

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.

30–45 min

Overview

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-based deployment

    Fork the Wraps repo, run pnpm selfhost:deploy, and receive upstream updates on your schedule via a weekly sync PR

  • Control plane API on Lambda

    Deployed into your AWS account via SST — you own the function, logs, and billing

  • Your Postgres database

    Bring any Postgres-compatible DB (Neon, Supabase, Railway, self-hosted)

  • Dashboard on CloudFront

    The apps/web dashboard is deployed automatically via SST into your AWS account — no separate hosting needed

  • GitHub Actions OIDC for upgrades

    Automated upgrades use GitHub Actions OIDC to assume a short-lived AWS role — no stored keys required

Prerequisites

  • Node.js 20+ and pnpm 10+ installed locally
  • AWS CLI configured with credentials that have IAM, Lambda, S3, CloudFormation, SSM, CloudFront, SQS, DynamoDB, EventBridge Scheduler, and CloudWatch Logs permissions
  • Postgres database connection string — Neon, Supabase, Railway, or self-hosted Postgres all work
  • Wraps enterprise license key — contact wraps.dev/contact to get one
  • GitHub account for forking the repository

1
Fork the Repository

The self-hosted deployment scripts live in the Wraps monorepo. Fork it so you can receive upstream updates on your own schedule.

  1. Fork on GitHub — go to github.com/wraps-team/wraps and click Fork. Keep the default branch name main.
  2. Clone your fork and install:
    Terminal
    git clone https://github.com/YOUR_ORG/wraps.gitcd wrapspnpm install

2
Deploy the Control Plane

Run 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.

Terminal
# 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:deploy

The script will:

  1. Check your AWS credentials
  2. Prompt for any missing values (database URL and license key) if not passed as flags
  3. Write .env.selfhost with auto-generated secrets
  4. Run sst install then deploy the Lambda, dashboard, and supporting AWS resources via SST
  5. Append the Lambda URL, app URL, and any email variables to .env.selfhost

Run migrations after initial deploy

selfhost:deploy provisions infrastructure but does not run database migrations. After your first deploy, run pnpm selfhost:upgrade to apply all pending Drizzle migrations against your database. Upgrades always run migrations automatically.

After the deploy completes, your .env.selfhost will look like this:

.env.selfhost
# 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_...# 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.com

Optional flags

Pass --web-domain to set a custom domain for the dashboard (e.g. dashboard.yourdomain.com) and --ai-gateway-api-key to enable AI email generation features. 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.

The SST stack always deploys to us-east-1. To target a different region, edit the region field in infra/selfhost.sst.config.ts before deploying.

3
Deploy Email Infrastructure

Initialise email sending with AWS SES. This creates IAM roles, configures SES, and sets up event processing for open and click tracking.

GNU Bashterminal.sh
wraps email init

If you only need SMS, skip this step and run wraps sms init instead.

4
Create Your Account and Connect

With the dashboard running, create your account, then authenticate the CLI against your instance and register the AWS connection.

  1. Open your dashboard URL and sign up with email and password.
  2. Create your organization — you will be prompted during onboarding. The CLI requires an organization to exist before it can connect.
  3. Sign in the CLI — reads your deployment metadata to find the dashboard URL, then opens a browser for device authorization against your own instance:
GNU Bashterminal.sh
wraps selfhost login

Why 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-console-access-role IAM role and enables event streaming from your email infrastructure to the dashboard:

GNU Bashterminal.sh
wraps selfhost connect

Single-account deployments

The CLI automatically configures the trust policy to trust your own AWS account — not the Wraps SaaS platform account.

Two additional commands are available once you are signed in:

  • 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.

5
Verify Your Deployment

Check the health of your self-hosted control plane from the CLI:

GNU Bashterminal.sh
wraps selfhost status

A 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.

Keeping Up to Date

New Wraps releases include Lambda updates, database migrations, and dashboard improvements. Choose the upgrade path that fits your workflow.

Manual upgrade

Pull upstream changes into your fork, then run the upgrade script. It re-deploys the SST stack and runs any pending database migrations automatically.

Terminal
# Manual upgrade from your fork:pnpm selfhost:upgrade# Runs SST deploy + database migrations automatically

The upgrade script redeploys both the Lambda and the dashboard in a single run — no separate steps required.

Automated upgrades with GitHub Actions

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.

Actions setup 1 — Enable workflows 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.

Actions setup 2 — Create an AWS OIDC identity provider for GitHub

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:

  • Provider type: OpenID Connect
  • Provider URL: https://token.actions.githubusercontent.com
  • Audience: sts.amazonaws.com

Then create an IAM role named wraps-github-deploy-role (or similar) with the following trust policy. Replace YOUR_ACCOUNT_ID and YOUR_GITHUB_ORG with your AWS account ID and the GitHub username or org that owns your fork:

JSONgithub-trust-policy.json
{  "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/wraps:*"        }      }    }  ]}

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.

Actions setup 3 — Configure secrets and variables

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

SecretRequiredDescription
AWS_DEPLOY_ROLE_ARNAlwaysARN of the GitHub deploy role from step 2
DATABASE_URLAlwaysPostgres connection string — needed by SST on every deploy to set the Lambda env var
WRAPS_LICENSE_KEYAlwaysYour Wraps license key — needed by SST on every deploy
BETTER_AUTH_SECRETAlwaysSession signing secret — auto-generated on first deploy. Copy from .env.selfhost after initial deploy. Rotating this invalidates all active sessions
UNSUBSCRIBE_SECRETAlwaysHMAC secret for unsubscribe links — auto-generated on first deploy. Copy from .env.selfhost after initial deploy
WRAPS_EMAIL_ROLE_ARNOptionalIAM role ARN for SES sending. Copy from .env.selfhost after running wraps email init
AUTH_EMAIL_FROMOptionalFrom address for transactional auth emails (password reset, magic links)
AUTH_EMAIL_CONFIGURATION_SETOptionalSES configuration set for auth emails. Copy from .env.selfhost after running wraps email init
AI_GATEWAY_API_KEYOptionalAPI key for the AI gateway — enables AI email template generation

Repository variables (not secrets)

VariableDefaultDescription
AWS_REGIONus-east-1AWS region for deployments (note: must also match infra/selfhost.sst.config.ts)
SELFHOST_WEB_DOMAINCustom domain for the dashboard (e.g. dashboard.yourdomain.com). When set, SST configures CloudFront with this domain
NEXT_PUBLIC_APP_URLFull 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
Actions setup 4 — Create a production environment (optional)

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".

Actions setup 5 — How the workflows run
  1. 1.Every Monday, Sync from upstream opens a PR with the latest commits from wraps-team/wraps. Review the changes and merge when you are ready.
  2. 2.After merging, go to Actions → Deploy self-hosted → Run workflow. Select upgrade (or deploy for a first-time setup) and click Run.
  3. 3.If you configured a production environment with required reviewers, approve the pending deployment. The job then assumes the OIDC role, runs SST deploy, and applies any pending migrations.
  4. 4.The workflow redeploys the Lambda and dashboard together — once the job completes, your update is live.

.env.selfhost Reference

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.

VariableRequiredDescription
DATABASE_URLYesPostgres connection string for the control plane database
LICENSE_KEYYesYour Wraps enterprise license key (wraps_lic_...)
BETTER_AUTH_SECRETYesSecret for session signing — auto-generated on first deploy, do not rotate without invalidating all active sessions
UNSUBSCRIBE_SECRETYesHMAC secret used to sign unsubscribe links in outbound emails — auto-generated on first deploy
NEXT_PUBLIC_APP_URLYesFull URL of your dashboard (e.g. https://dashboard.yourdomain.com) — used for CORS and auth redirects. Set automatically after deploy
WRAPS_API_URLYesLambda function URL — set automatically after first deploy
BETTER_AUTH_URLYesSame as NEXT_PUBLIC_APP_URL — set automatically after first deploy
SELFHOST_WEB_DOMAINOptionalCustom domain for the dashboard — sets AUTH_EMAIL_FROM to noreply@yourdomain.com automatically. Pass via --web-domain flag
WRAPS_EMAIL_ROLE_ARNOptionalIAM role ARN for SES sending — populated automatically when email infrastructure is present
AUTH_EMAIL_FROMOptionalFrom address for transactional auth emails (password reset, magic links). Must be a verified SES sender
AUTH_EMAIL_CONFIGURATION_SETOptionalSES configuration set name for auth emails — populated automatically when email infrastructure is present
AI_GATEWAY_API_KEYOptionalAPI key for the AI gateway — enables the AI email template generation feature. Pass via --ai-gateway-api-key flag
ANTHROPIC_API_KEYOptionalAnthropic API key — alternative to AI_GATEWAY_API_KEY for direct Anthropic access without a gateway
AI_MODELOptionalOverride the default Claude model used for AI features (e.g. claude-sonnet-4-6)

Next Steps

Verify your domain

Add DKIM, SPF, and DMARC records to start sending from your own domain.

Domain verification →

Request production access

SES starts in sandbox mode. Request production access to send to any recipient.

Production access →

Enable AI features

Add AI_GATEWAY_API_KEY to your .env.selfhost and run pnpm selfhost:upgrade to enable AI-powered email template generation.

Tear down

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