Developers
Deploy to Render
What the blueprint makes
| Service | Type | Plan | What it runs |
|---|---|---|---|
resell-store | Web service (Node 24) | starter | The Next.js app: marketplace, stores, seller app, API, docs, MCP. Health check on / |
resell-sweeps | Workflow (Oregon) | Render's default | The timed jobs in workflows/main.ts |
resell-sweeps-cron | Cron job (Oregon) | starter | Every 15 minutes, starts the sweeps on the workflow |
resell-db | Postgres 17 | basic-256mb | Database resell, user resell. pgvector is available and the first migration turns it on |
Pick a region for the web service and database close to the workflow if you change it. Plans can be raised later in the dashboard; check Render's pricing page for what each costs.
Step by step
- Fork or push the repo to your own GitHub (or GitLab) account.
- In Render, choose New, then Blueprint, and pick the repo. Render reads
render.yamland lists the four resources. - Fill in the values marked
sync: false. You can leave any optional service blank and add it later; see Environment variables for what each does. At minimum:NEXT_PUBLIC_ROOT_DOMAINandBETTER_AUTH_URLon the web service and the workflow, andRESEND_API_KEY+EMAIL_FROMso people can sign in. - On the cron job, set
RENDER_API_KEYto an API key from your Render account settings.RENDER_WORKFLOW_SLUGis filled in from the workflow for you. - Apply.
BETTER_AUTH_SECRETandCRON_SECRETare generated, and the secret is copied to the workflow.
Domains
- On the web service, add two custom domains:
resell.storeand*.resell.store(your own domain, if different). The wildcard covers every store and theapi.,docs.andmcp.hosts. - Add the DNS records Render shows for each. Copy the exact targets from the dashboard, and remove any
AAAArecords: Render doesn't support IPv6 for custom domains. - Set
NEXT_PUBLIC_ROOT_DOMAIN=resell.storeandBETTER_AUTH_URL=https://resell.storeon the web service, and the same on the workflow. The root domain is baked in at build time, so trigger a new deploy after changing it.
| Type | Name | Value |
|---|---|---|
A | @ | 216.24.57.1, or an ALIAS/ANAME to your onrender.com address |
CNAME | www | Your onrender.com address. Render redirects www to the root |
CNAME | * | Your onrender.com address |
CNAME | _acme-challenge | <service-id>.verify.renderdns.com, for the wildcard certificate |
CNAME | _cf-custom-hostname | <service-id>.hostname.renderdns.com |
On Cloudflare, use a CNAME on @ instead of the A record, and keep every record on DNS only (grey cloud) until Render has verified the domains and issued certificates. If you turn the proxy on afterwards, set SSL/TLS to Full. The wildcard only works while the root domain also points to Render.
onrender.com address, but store subdomains, the session cookie and the api/docs/mcp hosts all need the real root domain.Outside services
- Resend: verify your domain in Resend and set
EMAIL_FROMto an address on it, on both the web service and the workflow (the workflow sends reminders and payout emails). - Cloudflare R2: create a bucket (default name
resell-store) and an API token with read and write on it. SetR2_URLtohttps://{account_id}.r2.cloudflarestorage.complus the two keys. For speed, connect a custom domain to the bucket (likefiles.resell.store) and setR2_PUBLIC_URL; the r2.dev address is rate-limited. - PayPal: set the client id (also as
NEXT_PUBLIC_PAYPAL_CLIENT_ID), secret, partner merchant id and BN code, on the web service and the workflow. Then, in the developer dashboard under your app's Webhooks, addhttps://resell.store/api/paypal/webhookwith these events, and paste its id intoPAYPAL_WEBHOOK_ID:
PAYMENT.CAPTURE.COMPLETED
PAYMENT.CAPTURE.DENIED
PAYMENT.CAPTURE.DECLINED
PAYMENT.CAPTURE.REFUNDED
PAYMENT.CAPTURE.REVERSED
CUSTOMER.DISPUTE.CREATED
CUSTOMER.DISPUTE.UPDATED
CUSTOMER.DISPUTE.RESOLVED
MERCHANT.ONBOARDING.COMPLETED
MERCHANT.PARTNER-CONSENT.REVOKED
PAYMENT.REFERENCED-PAYOUT-ITEM.COMPLETED
PAYMENT.REFERENCED-PAYOUT-ITEM.FAILED- The demo seller (sandbox): from your machine with the same PayPal keys, run
node --env-file=.env.local scripts/paypal-demo-seller.mjsinapps/app, follow the link it prints, run it again, and set the printedPAYPAL_DEMO_SELLER_IDon the web service and the workflow. Add the demo seller and buyer logins (PAYPAL_DEMO_*_EMAIL/_PASSWORD) to the web service if you want them shown in the app. - The rest are just keys:
ANTHROPIC_API_KEY,CHANNEL3_API_KEY,KERNEL_API_KEY,JINA_EMBEDDING_MODEL_KEY, andSUPPORT_EMAILfor escalations.
What happens on each deploy
- Build:
corepack pnpm install --frozen-lockfile && corepack pnpm --filter app build. pnpm runs through corepack becausecorepack enablecan't write to Render's read-only/usr/bin. TheNEXT_PUBLIC_values are baked in here. - Pre-deploy:
cd packages/db && npm run db:migrateapplies any new migrations. If it fails, the deploy stops and the old version keeps serving. - Start:
cd apps/app && npm start. Render switches traffic once the health check on/passes.
The workflow and cron job install dependencies and run from source with tsx; they have no build step and no migrations of their own.
Seeding a deployed database
To fill a fresh deploy with the ten demo stores, run the seed from your machine against the database's External Database URL (on the database's page in Render). It refuses a remote database unless you say --remote. Render only accepts encrypted connections from outside, which PGSSLMODE=require turns on:
cd apps/app
PGSSLMODE=require DATABASE_URL="postgres://resell:...@....render.com/resell" pnpm seed --remoteIt uses the rest of your local .env.local: with R2 keys set there, the photos go to that bucket (use the same one as production); without, they go into Postgres. With a Jina key, the listings get embeddings. Running it again replaces only the seed's own rows.
Check it works
https://resell.storeloads, andhttps://docs.resell.storeshows these docs.https://api.resell.store/v1/openapi.jsonreturns the API description.- A store loads on its subdomain, e.g. one from the seed.
- Signing in by email works: the link arrives, and you stay signed in when you open your store.
- A checkout with the sandbox buyer completes, and the order shows on both sides.
- The cron job's log shows
Started resell-sweeps/offerSweep: …every 15 minutes, and the workflow's runs succeed. - In the PayPal dashboard, webhook deliveries to your site come back 200.
Troubleshooting
| You see | Check |
|---|---|
| Checkout says "Test checkout: no money moves yet" | PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET and PAYPAL_PARTNER_MERCHANT_ID must all be set on the web service. If only the seller screens say it, NEXT_PUBLIC_PAYPAL_CLIENT_ID is missing or the app wasn't rebuilt |
| "This seller hasn't connected PayPal yet" at checkout | The seller needs to connect PayPal on Connections, or set PAYPAL_DEMO_SELLER_ID in the sandbox |
| The Shopping sidekick says Coming soon | KERNEL_API_KEY and ANTHROPIC_API_KEY are both needed |
| No emails arrive | RESEND_API_KEY is set, the domain is verified in Resend, and EMAIL_FROM uses it. Without a key, emails only go to the log |
| Stores 404 or don't resolve | The *.resell.store custom domain and its DNS, and NEXT_PUBLIC_ROOT_DOMAIN matching the domain (then redeploy) |
| Signed in on the marketplace but not on a store | BETTER_AUTH_URL and NEXT_PUBLIC_ROOT_DOMAIN use the real domain, so the cookie is set on .resell.store |
| PayPal webhooks get 401 | PAYPAL_WEBHOOK_ID matches the webhook in the same PayPal app and environment |
| Offers never expire, payouts never happen | The cron job's RENDER_API_KEY, and the workflow's logs. As a stopgap, POST /api/cron/sweep with CRON_SECRET runs one pass |
| The deploy stops at pre-deploy | A migration failed: read the log, fix it in a new migration and push |
More on the timed side in Background jobs.