Seed & Source β Deployment Runbook
Seed & Source β Deployment Runbook
Audience: Technical founder / developer
Last updated: 2026-03-14
Service ID: srv-d6mskc6a2pns73ddjlo0
Table of Contents
- Architecture Overview
- Deploy Process
- Manual Deploy Override
- Environment Variables Reference
- Database Migrations
- First Deploy Checklist
- Rollback Procedure
- Health Verification Protocol
- Stripe Webhook Verification
- Common Failure Modes + Remediation
1. Architecture Overview
The entire production stack runs as a single Render web service (AIO β All-in-One) built from Dockerfile.combined. nginx proxies internal ports and serves the React frontend.
Hard rule: The active AIO blueprint must contain exactly one paid web service (
internal-admin). If a secondtype: webentry exists ininternal-projects/internal-admin/render.yaml, Render will create a second billable service.
Service Map
| Component | Internal Port | Public URL | Technology |
|---|---|---|---|
| license-server | 8001 | https://auth.seedsource.dev | FastAPI (Python) |
| admin-backend | 8002 | https://internal-admin.onrender.com | FastAPI (Python) |
| React frontend | static | served via nginx on port 80 | Vite / React |
| nginx | 80 | (Render terminates TLS externally) | nginx |
Request Flow
Client β βΌRender TLS termination β βΌnginx (port 80) βββ /api/license/* β license-server (127.0.0.1:8001) βββ /api/admin/* β admin-backend (127.0.0.1:8002) βββ /webhooks/* β license-server (127.0.0.1:8001) βββ /health β license-server (127.0.0.1:8001) βββ /* β React static buildDNS
| Domain | Target | Manager |
|---|---|---|
seedsource.dev | Static landing (external) | External registrar |
auth.seedsource.dev | CNAME β Render AIO service | External registrar |
Key Files
| File | Purpose |
|---|---|
internal-projects/internal-admin/Dockerfile.combined | Multi-stage build for the AIO service |
internal-projects/internal-admin/render.yaml | Render service definition + preDeployCommand |
tooling/services/license-server/alembic/ | Migration scripts |
2. Deploy Process
Every push to main on the foundry-meta root triggers an automatic Render deploy. The full sequence is:
- Push to main β Render detects the change via GitHub webhook.
- Docker build β Render pulls the repo and runs
docker buildusingDockerfile.combined. preDeployCommandruns β Before the new container serves traffic, Render executes:This applies all pending migrations against the Neon production database. If this command fails, the deploy is aborted and the current version keeps serving.cd /app/license_server && alembic upgrade head- Container swap β Render replaces the running container with zero downtime (rolling deploy).
- Health check β Render hits the configured health check endpoint. If it fails, the deploy is marked failed and the previous container is preserved.
The migration step is automatic. You do not need to manually run
alembic upgrade headafter pushing.
3. Manual Deploy Override
Trigger a redeploy from the Render dashboard
- Go to dashboard.render.com.
- Select the internal-admin service (
srv-d6mskc6a2pns73ddjlo0). - Click Manual Deploy β choose Deploy latest commit or Deploy specific commit.
- Confirm β the same
preDeployCommandflow runs.
Trigger via Render API
curl -X POST "https://api.render.com/v1/services/srv-d6mskc6a2pns73ddjlo0/deploys" \ -H "Authorization: Bearer $RENDER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"clearCache": false}'Deploy a specific commit
COMMIT_SHA="<full-sha>"curl -X POST "https://api.render.com/v1/services/srv-d6mskc6a2pns73ddjlo0/deploys" \ -H "Authorization: Bearer $RENDER_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"commitId\": \"$COMMIT_SHA\", \"clearCache\": false}"4. Environment Variables Reference
All variables are set in the Render dashboard under Environment for service srv-d6mskc6a2pns73ddjlo0. None of these are committed to the repository.
| Variable | Description | Example / Format | Source |
|---|---|---|---|
DATABASE_URL | Neon PostgreSQL connection string. Must use neondb_owner credential β app_user lacks DDL rights. | postgresql://neondb_owner:<pass>@<host>/neondb?sslmode=require | Neon dashboard β Connection Details |
SECRET_KEY | Shared app/signing secret for license-server and shared flows. Min 32 chars. | openssl rand -hex 32 | Generate locally |
JWT_SECRET | Admin-backend JWT signing secret. Min 32 chars. | openssl rand -hex 32 | Generate locally |
GITHUB_CLIENT_ID | GitHub OAuth App client ID | Iv1.abc123... | GitHub β Settings β OAuth Apps |
GITHUB_CLIENT_SECRET | GitHub OAuth App client secret | abc123... | GitHub β Settings β OAuth Apps |
STRIPE_SECRET_KEY | Stripe live secret key | sk_live_... | Stripe dashboard β Developers β API Keys |
STRIPE_WEBHOOK_SECRET | Webhook signing secret | whsec_... | Stripe dashboard β Webhooks β endpoint detail |
STRIPE_PUBLISHABLE_KEY | Stripe live publishable key | pk_live_... | Stripe dashboard β Developers β API Keys |
STRIPE_PRICE_ID_PRO_ALPHA | Price ID for Pro Alpha (MXN 580/mo) | price_... | Stripe dashboard β Products |
STRIPE_PRICE_ID_PRO_MONTHLY | Price ID for Pro Monthly (MXN 980/mo) | price_... | Stripe dashboard β Products |
STRIPE_PRICE_ID_PRO_ANNUAL | Price ID for Pro Annual (MXN 9,800/yr) | price_... | Stripe dashboard β Products |
STRIPE_PRICE_ID_FEATURE_UNLOCK | Price ID for Feature Unlock (MXN 980 one-time) | price_... | Stripe dashboard β Products |
LICENSE_SERVER_URL | Public auth host used by clients and checkout redirects. | https://auth.seedsource.dev | Render env |
PAYMENT_SUCCESS_URL | Redirect after successful Stripe checkout | https://seedsource.dev/payment/success | Configure to match frontend route |
PAYMENT_CANCEL_URL | Redirect after cancelled Stripe checkout | https://seedsource.dev/payment/cancel | Configure to match frontend route |
CORS_ORIGINS | Comma-separated allowlist for browser origins. | https://internal-admin.onrender.com,https://auth.seedsource.dev | Render env |
5. Database Migrations
How it works
The license-server uses Alembic for schema migrations. The migration chain lives at:
tooling/services/license-server/alembic/versions/Current production head: 005_add_payment_feature_unlocked
Migrations run automatically via preDeployCommand in render.yaml before every deploy. The command is:
cd /app/license_server && alembic upgrade headCheck the current migration state
From the Render shell (service β Shell tab):
cd /app/license_serveralembic currentFrom a local machine with DATABASE_URL set:
cd tooling/services/license-serverDATABASE_URL="<neon_connection_string>" alembic currentCheck migration history
cd /app/license_serveralembic history --verboseManually apply migrations (Render shell)
Only needed if preDeployCommand was bypassed or failed mid-run:
cd /app/license_serveralembic upgrade headTo apply a specific revision:
alembic upgrade 005_add_payment_feature_unlockedRollback a migration
β οΈ Irreversible on data. Downgrading migrations that drop columns or tables will destroy data. Take a Neon branch snapshot first.
# Roll back one stepalembic downgrade -1
# Roll back to a specific revisionalembic downgrade 004_<previous_revision_name>
# Roll back all migrations (empty schema)alembic downgrade baseCreate a new migration
cd tooling/services/license-serveralembic revision --autogenerate -m "describe_the_change"Review the generated file in alembic/versions/ before committing. Autogenerate misses some changes (e.g., server defaults, check constraints) β always validate manually.
6. First Deploy Checklist
Use this for a net-new environment (e.g., staging, disaster recovery).
Pre-deploy
- Neon database created,
neondb_ownercredential copied -
DATABASE_URLset in Render withneondb_owner(notapp_user) -
SECRET_KEYgenerated (openssl rand -hex 32) and set in Render -
JWT_SECRETgenerated (openssl rand -hex 32) and set in Render - GitHub OAuth App created at
https://github.com/settings/applications/new- Authorization callback URL:
https://auth.seedsource.dev/auth/github/callback GITHUB_CLIENT_IDandGITHUB_CLIENT_SECRETset in Render
- Authorization callback URL:
- All 4 Stripe products created in dashboard; all 4 price IDs copied to Render env vars
-
STRIPE_SECRET_KEYandSTRIPE_PUBLISHABLE_KEYset (use live keys for prod) -
PAYMENT_SUCCESS_URLandPAYMENT_CANCEL_URLset and matching deployed frontend routes
DNS
-
auth.seedsource.devCNAME record points to the Render serviceβs.onrender.comhostname - TLS certificate provisioned by Render (check service β Settings β Custom Domains β must show βVerifiedβ)
Post-deploy (first time)
- Run health check (see Β§8)
- Register Stripe webhook endpoint (see Β§9)
- Set
STRIPE_WEBHOOK_SECRETfrom the newly registered endpoint and redeploy - Verify migrations:
alembic currentshows005_add_payment_feature_unlocked (head) - Test GitHub OAuth login end-to-end
- Trigger a test Stripe checkout session and confirm webhook receipt
7. Rollback Procedure
Render retains the last N deploy builds. Rolling back redeploys the previous image β the preDeployCommand (migrations) will NOT run in reverse automatically.
Steps
- Go to Render dashboard β service
srv-d6mskc6a2pns73ddjlo0β Deploys. - Find the last known-good deploy.
- Click Rollback to this deploy.
- Confirm.
β οΈ Schema mismatch risk. If the current deploy added a migration, rolling back the code will leave the newer schema in Neon while running older code. Assess whether a manual
alembic downgrade -1is needed before rolling back β or ensure the old code is forward-compatible with the new schema.
Safe rollback sequence
# 1. Identify the migration the old code expects# (check alembic history in the old git commit)git show <old-commit-sha>:tooling/services/license-server/alembic/versions/
# 2. If schema must be reverted, connect to Render shell on the CURRENT deploy# and downgrade BEFORE triggering the rollbackcd /app/license_serveralembic downgrade -1
# 3. Then trigger the rollback in the Render dashboard8. Health Verification Protocol
Run this sequence after every deploy to confirm all subsystems are operational.
1. License server health
curl -s https://auth.seedsource.dev/health | jq .# Expected: {"status":"healthy", ...}2. License server API reachable
curl -s -o /dev/null -w "%{http_code}" https://auth.seedsource.dev/# Expected: 200 or 404 (not 502/503)3. Stripe webhook endpoint reachable
curl -s -o /dev/null -w "%{http_code}" \ -X POST https://auth.seedsource.dev/webhooks/stripe# Expected: 400 (missing signature β correct behavior, not 502/503/404)4. React frontend served
curl -s -o /dev/null -w "%{http_code}" https://internal-admin.onrender.com/# Expected: 2005. Database connectivity (requires Render shell or local env)
cd /app/license_serveralembic current# Expected: 005_add_payment_feature_unlocked (head)6. Neon branch status
neonctl branches list# Expected: main branch shows "ready"Full one-liner health sweep
for URL in \ "https://auth.seedsource.dev/health" \ "https://internal-admin.onrender.com/"; do CODE=$(curl -s -o /dev/null -w "%{http_code}" "$URL") echo "$CODE $URL"done9. Stripe Webhook Verification
Registered endpoint
https://auth.seedsource.dev/webhooks/stripeEvents subscribed (minimum required):
checkout.session.completedcustomer.subscription.updatedcustomer.subscription.deletedinvoice.payment_failed
Verify endpoint is reachable
curl -s -o /dev/null -w "%{http_code}" \ -X POST https://auth.seedsource.dev/webhooks/stripe# Must return 400 β Stripe signature validation rejected (no sig header).# 404 = route not registered. 502/503 = service down.Register or update the webhook endpoint
- Go to Stripe Dashboard β Developers β Webhooks.
- Click Add endpoint.
- URL:
https://auth.seedsource.dev/webhooks/stripe - Select events listed above.
- After saving, copy the Signing secret (
whsec_...). - Set
STRIPE_WEBHOOK_SECRETin Render and redeploy.
Test with Stripe CLI (local only)
stripe listen --forward-to http://localhost:8000/webhooks/stripestripe trigger checkout.session.completed \ --override checkout_session:client_reference_id=1 \ --override checkout_session:customer_email=user@example.comVerify live webhook delivery in Stripe dashboard
Stripe Dashboard β Developers β Webhooks β select the endpoint β Recent deliveries. All checkout.session.completed events should show 200 responses.
10. Common Failure Modes + Remediation
10.1 β CORS 500 on all API responses
Symptom: Frontend receives 500 Internal Server Error on every API call. Stack trace mentions CORS middleware or missing env var accessed at startup.
Cause: An env var read at import-time (e.g., ALLOWED_ORIGINS, FRONTEND_URL) is missing, causing the CORS middleware to crash on every request.
Remediation:
- Check Render logs:
render logs srv-d6mskc6a2pns73ddjlo0 --tail 100 - Identify the missing variable name in the traceback.
- Add the variable in Render dashboard β Environment.
- Trigger a manual redeploy.
10.2 β Missing column crash (UndefinedColumn / ProgrammingError)
Symptom: API returns 500; logs show UndefinedColumn: column "feature_unlocked" does not exist (or similar).
Cause: Code was deployed with a migration that didnβt apply, or preDeployCommand failed silently.
Remediation:
- Open Render shell for the running service.
- Run:
cd /app/license_server && alembic current - If not at
head, run:alembic upgrade head - If migration fails, check
DATABASE_URLis usingneondb_ownercredential (see Β§10.6).
10.3 β Migration drift (alembic head doesnβt match DB)
Symptom: alembic current returns a revision behind head, or shows (detached).
Cause: Manual DB changes, a failed preDeployCommand, or conflicting migration branches.
Remediation:
# On Render shell:cd /app/license_serveralembic history --verbose # check the chainalembic current # see where DB isalembic upgrade head # apply missing revisionsIf the schema is ahead of alembic (manual DDL was run directly on Neon):
alembic stamp <revision-id> # mark DB as being at a specific revisionβ οΈ
alembic stampdoes not run DDL β it only updates thealembic_versiontable. Use only when you are certain the schema matches the target revision.
10.4 β JWT_SECRET not set (all auth requests return 500)
Symptom: Every authenticated route returns 500; logs show KeyError: 'JWT_SECRET' or ValidationError on startup.
Cause: JWT_SECRET env var is missing from Render.
Remediation:
- Generate a new secret locally:
openssl rand -hex 32 - Add to Render dashboard β Environment β
JWT_SECRET. - Trigger redeploy.
Note: Rotating
JWT_SECRETimmediately invalidates all existing sessions. All logged-in users will need to re-authenticate.
10.5 β Unexpected second paid service appears in Render
Symptom: Blueprint apply creates internal-admin plus another service (for example license-server-rails).
Cause: render.yaml contains more than one type: web entry. Render always creates one billable service per entry.
Remediation:
- Keep only one service block (
internal-admin) ininternal-projects/internal-admin/render.yaml. - Re-apply the blueprint in Render.
- Delete any extra service created by the prior blueprint apply.
- Verify both hosts still route correctly through the single AIO container:
https://auth.seedsource.dev/healthhttps://internal-admin.onrender.com/health
10.6 β Neon DDL permissions error (InsufficientPrivilege)
Symptom: alembic upgrade head fails with ERROR: permission denied for schema public or ERROR: must be owner of table.
Cause: DATABASE_URL is set to the app_user credential, which has read/write but NOT DDL (CREATE TABLE, ALTER TABLE, etc.) rights on Neon.
Remediation:
- Go to Neon dashboard β your project β Connection Details.
- Select the
neondb_ownerrole (notapp_user). - Copy the connection string.
- Update
DATABASE_URLin Render dashboard to useneondb_owner. - Trigger a redeploy β
preDeployCommandwill retry migrations.
Long-term note: Keep
app_userfor the applicationβs runtime queries andneondb_ownerexclusively for migrations (alembic). Do not useneondb_owneras the applicationβs runtime credential.
Appendix: Useful Commands Quick Reference
# View live Render logsrender logs srv-d6mskc6a2pns73ddjlo0 --tail 200
# Check migration state (from Render shell)cd /app/license_server && alembic current
# Apply all pending migrations (from Render shell)cd /app/license_server && alembic upgrade head
# Roll back one migration (from Render shell)cd /app/license_server && alembic downgrade -1
# Neon branch statusneonctl branches list
# Health checkcurl -s https://auth.seedsource.dev/health | jq .
# Stripe webhook smoke testcurl -s -o /dev/null -w "%{http_code}" -X POST https://auth.seedsource.dev/webhooks/stripe# β 400 is correct
# Trigger Render redeploy via APIcurl -X POST "https://api.render.com/v1/services/srv-d6mskc6a2pns73ddjlo0/deploys" \ -H "Authorization: Bearer $RENDER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"clearCache": false}'