Deploy on Render

render.yaml at the root of the repo is a Blueprint for the whole stack. One deploy creates the API, the dashboard, the Workflows service and the database, and wires them together.

What gets created

NameTypeNotes
offersdk-apiWeb service, DockerBuilt from api/Dockerfile. Health check at /health. Runs migrations on boot.
offer-appWeb service, NodeThe dashboard. Health check at /api/health.
offersdk-workflowsWorkflow, NodePause and resume tasks for subscriptions. Region oregon.
offersdk-dbPostgres 17basic-256mb. Internal connections only.

The two web services start on Render's starter plan. Change plan in render.yaml before you deploy if you want something else.

Before you start

  • A Render account. Fork the repo first if you plan to change the code, and deploy your fork.
  • A long random value for the shared admin key, for example from openssl rand -hex 32.
  • Optional: an Anthropic API key for the agent and dynamic save offers, and a Render API key to run pauses on Workflows. You can add both later.

PayPal credentials aren't part of the deploy. Each app connects its own PayPal account in the dashboard, under App settings → Payments.

Deploy

  1. Start the Blueprint

    Use the one-click deploy link for the public repo, or in Render choose New → Blueprint and pick your repo. Render reads render.yaml and lists the services above.

  2. Fill in the values Render asks for

    Everything else is generated or copied between services. These are the values only you know:

    VariableServiceValue
    ADMIN_API_KEYoffersdk-apiYour random admin key.
    APP_URLoffersdk-apiThe dashboard's public URL, e.g. https://offer-app.onrender.com
    OFFER_API_URLoffer-appThe API's public URL, e.g. https://offersdk-api.onrender.com
    OFFER_API_URLoffersdk-workflowsThe same API URL.
    ANTHROPIC_API_KEYoffer-appOptional. Turns on the Agent page.
    ANTHROPIC_API_KEYoffersdk-apiOptional. Turns on dynamic save offers in cancel flows.
    RENDER_API_KEYoffersdk-apiOptional. Runs pauses on Render Workflows.
    You won't know the URLs for certain until the first deploy. Render names them after the service, https://<name>.onrender.com, and adds a suffix when that name is taken. Enter the expected URLs now. If Render picks different ones, update APP_URL and both OFFER_API_URL values to match.
  3. Apply and wait for the first deploy

    The services build in parallel. The API waits for the database, runs its migrations and passes its health check. The dashboard builds with pnpm and can go live before the API does, in which case its pages show errors until the API is up.

  4. Create the admin account

    Open the dashboard. A fresh install has no accounts, so it shows /setup: the first person to fill it in becomes the install's admin, and sign-up closes after that. Do this as soon as the deploy finishes. Then name your workspace and create your first app. The app's keys are under Developers → API keys. See Dashboard tour → First user: the admin.

  5. Try the demo workspace (optional)

    The demo is off by default. To turn it on, add DEMO_ENABLED=true under offer-app → Environment in the Render dashboard, and the sign-in page offers Explore the demo workspace. The first click builds it in your database, which takes a few seconds. The demo login is shared and read-only, so visitors can look around but can't change or delete anything. Send people to /demo-account to open sign-in with the login filled in. To rebuild the demo from scratch, run this from your machine with the API's ADMIN_API_KEY (in the Render dashboard under offersdk-api → Environment):

    APP_URL=https://offer-app.onrender.com ADMIN_API_KEY=… pnpm seed:demo --reset

How the services find each other

Shared admin key
You set ADMIN_API_KEY once on the API. The Blueprint copies it to the dashboard as OFFER_API_ADMIN_KEY and to the Workflows service as ADMIN_API_KEY. The API rejects workspace, sign-in and pause routes without it.
Private network
The dashboard calls the API over Render's private network at OFFER_API_INTERNAL_URL, which the Blueprint fills with the API's internal host and port. OFFER_API_URL stays the public address shown in code snippets.
Sign-in
Users, sessions and workspaces live in the API's Postgres. BETTER_AUTH_SECRET is generated on the API. Sessions are issued for APP_URL, and that is the only origin the API trusts for sign-in.
PayPal webhooks
When an app connects PayPal, the API registers a webhook at its own address. On Render that's the service's onrender.com URL, which Render provides as RENDER_EXTERNAL_URL, so webhooks work from the first deploy. Set PUBLIC_API_URL to register a different address.
MCP sign-in
The MCP server's OAuth metadata uses the same address. If you give clients a server URL on a custom domain, set PUBLIC_API_URL to that domain, or sign-in fails.
Database
The API reads DATABASE_URL from the database's internal connection string. The database accepts no outside connections.

Run pauses on Render Workflows

When a customer accepts a pause in a cancel flow, the API suspends their PayPal subscription and later resumes it. With RENDER_API_KEY set on the API, each pause and resume runs as a task on offersdk-workflows with its own retries. RENDER_WORKFLOW_SLUG is already set to offersdk-workflows. Without the key, the API does the same work in-process on a timer. See Render for the tasks.

Custom domains

Add the domain in Render, then update every variable that holds the old address:

  • New dashboard domain: APP_URL on offersdk-api.
  • New API domain: OFFER_API_URL on offer-app and offersdk-workflows. PayPal webhooks keep arriving at the onrender.com address, which stays live. To move them to the new domain, set PUBLIC_API_URL on offersdk-api, then disconnect and connect PayPal again in App settings → Payments for each app.

Updates and migrations

Each service has a build filter, so a push only rebuilds the services whose files changed. Pending migrations in api/migrations run when the API boots, behind a Postgres advisory lock, so several instances can start at once. Set MIGRATE_ON_BOOT=false to run them yourself with bun run migrate.

Troubleshooting

Dashboard fails to start
OFFER_API_ADMIN_KEY is required when OFFER_API_URL is set means the key wasn't copied from the API. Check that ADMIN_API_KEY is set on offersdk-api.
Sign-in fails
Check that APP_URL on the API matches the dashboard address in your browser exactly, including https://. Fix it and redeploy the API.
Setup sends me to sign-in
The install already has its admin, so /setup is closed. Sign in with that account. If nobody has it, deleting that row from auth_user in Postgres reopens /setup, but the new admin starts without the old workspaces. The database accepts no outside connections, so open offersdk-api → Shell in the Render dashboard and run this with the admin's email. The API image has Bun but not psql, and Bun.sql reads DATABASE_URL:
bun -e 'await Bun.sql.unsafe("delete from auth_user where email = $1", ["admin@example.com"])'
PayPal shows the webhook as not registered
The API couldn't find a public HTTPS address for itself, or PayPal refused it. Outside Render, set PUBLIC_API_URL. If the URL was already added by hand in PayPal, delete it there first, since PayPal won't register it twice. Then connect PayPal again under App settings → Payments.
The Agent page asks for a key
Set ANTHROPIC_API_KEY on offer-app. The API's own key is only used for cancel flows.

Every variable each service reads is listed in Environment variables.