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
| Name | Type | Notes |
|---|---|---|
| offersdk-api | Web service, Docker | Built from api/Dockerfile. Health check at /health. Runs migrations on boot. |
| offer-app | Web service, Node | The dashboard. Health check at /api/health. |
| offersdk-workflows | Workflow, Node | Pause and resume tasks for subscriptions. Region oregon. |
| offersdk-db | Postgres 17 | basic-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
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.yamland lists the services above.Fill in the values Render asks for
Everything else is generated or copied between services. These are the values only you know:
Variable Service Value ADMIN_API_KEY offersdk-api Your random admin key. APP_URL offersdk-api The dashboard's public URL, e.g. https://offer-app.onrender.com OFFER_API_URL offer-app The API's public URL, e.g. https://offersdk-api.onrender.com OFFER_API_URL offersdk-workflows The same API URL. ANTHROPIC_API_KEY offer-app Optional. Turns on the Agent page. ANTHROPIC_API_KEY offersdk-api Optional. Turns on dynamic save offers in cancel flows. RENDER_API_KEY offersdk-api Optional. 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, updateAPP_URLand bothOFFER_API_URLvalues to match.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.
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.Try the demo workspace (optional)
The demo is off by default. To turn it on, add
DEMO_ENABLED=trueunder 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-accountto open sign-in with the login filled in. To rebuild the demo from scratch, run this from your machine with the API'sADMIN_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_KEYonce on the API. The Blueprint copies it to the dashboard asOFFER_API_ADMIN_KEYand to the Workflows service asADMIN_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_URLstays the public address shown in code snippets. - Sign-in
- Users, sessions and workspaces live in the API's Postgres.
BETTER_AUTH_SECRETis generated on the API. Sessions are issued forAPP_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.comURL, which Render provides asRENDER_EXTERNAL_URL, so webhooks work from the first deploy. SetPUBLIC_API_URLto 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_URLto that domain, or sign-in fails. - Database
- The API reads
DATABASE_URLfrom 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_URLon offersdk-api. - New API domain:
OFFER_API_URLon offer-app and offersdk-workflows. PayPal webhooks keep arriving at theonrender.comaddress, which stays live. To move them to the new domain, setPUBLIC_API_URLon 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 setmeans the key wasn't copied from the API. Check thatADMIN_API_KEYis set on offersdk-api.- Sign-in fails
- Check that
APP_URLon the API matches the dashboard address in your browser exactly, includinghttps://. Fix it and redeploy the API. - Setup sends me to sign-in
- The install already has its admin, so
/setupis closed. Sign in with that account. If nobody has it, deleting that row fromauth_userin 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 notpsql, andBun.sqlreadsDATABASE_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_KEYon offer-app. The API's own key is only used for cancel flows.
Every variable each service reads is listed in Environment variables.