Skip to content

Seed & Source β€” Deployment Runbook

Seed & Source β€” Deployment Runbook

Audience: Technical founder / developer Last updated: 2026-03-14 Service ID: srv-d6mskc6a2pns73ddjlo0


Table of Contents

  1. Architecture Overview
  2. Deploy Process
  3. Manual Deploy Override
  4. Environment Variables Reference
  5. Database Migrations
  6. First Deploy Checklist
  7. Rollback Procedure
  8. Health Verification Protocol
  9. Stripe Webhook Verification
  10. 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 second type: web entry exists in internal-projects/internal-admin/render.yaml, Render will create a second billable service.

Service Map

ComponentInternal PortPublic URLTechnology
license-server8001https://auth.seedsource.devFastAPI (Python)
admin-backend8002https://internal-admin.onrender.comFastAPI (Python)
React frontendstaticserved via nginx on port 80Vite / React
nginx80(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 build

DNS

DomainTargetManager
seedsource.devStatic landing (external)External registrar
auth.seedsource.devCNAME β†’ Render AIO serviceExternal registrar

Key Files

FilePurpose
internal-projects/internal-admin/Dockerfile.combinedMulti-stage build for the AIO service
internal-projects/internal-admin/render.yamlRender 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:

  1. Push to main β€” Render detects the change via GitHub webhook.
  2. Docker build β€” Render pulls the repo and runs docker build using Dockerfile.combined.
  3. preDeployCommand runs β€” Before the new container serves traffic, Render executes:
    cd /app/license_server && alembic upgrade head
    This applies all pending migrations against the Neon production database. If this command fails, the deploy is aborted and the current version keeps serving.
  4. Container swap β€” Render replaces the running container with zero downtime (rolling deploy).
  5. 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 head after pushing.


3. Manual Deploy Override

Trigger a redeploy from the Render dashboard

  1. Go to dashboard.render.com.
  2. Select the internal-admin service (srv-d6mskc6a2pns73ddjlo0).
  3. Click Manual Deploy β†’ choose Deploy latest commit or Deploy specific commit.
  4. Confirm β€” the same preDeployCommand flow runs.

Trigger via Render API

Terminal window
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

Terminal window
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.

VariableDescriptionExample / FormatSource
DATABASE_URLNeon PostgreSQL connection string. Must use neondb_owner credential β€” app_user lacks DDL rights.postgresql://neondb_owner:<pass>@<host>/neondb?sslmode=requireNeon dashboard β†’ Connection Details
SECRET_KEYShared app/signing secret for license-server and shared flows. Min 32 chars.openssl rand -hex 32Generate locally
JWT_SECRETAdmin-backend JWT signing secret. Min 32 chars.openssl rand -hex 32Generate locally
GITHUB_CLIENT_IDGitHub OAuth App client IDIv1.abc123...GitHub β†’ Settings β†’ OAuth Apps
GITHUB_CLIENT_SECRETGitHub OAuth App client secretabc123...GitHub β†’ Settings β†’ OAuth Apps
STRIPE_SECRET_KEYStripe live secret keysk_live_...Stripe dashboard β†’ Developers β†’ API Keys
STRIPE_WEBHOOK_SECRETWebhook signing secretwhsec_...Stripe dashboard β†’ Webhooks β†’ endpoint detail
STRIPE_PUBLISHABLE_KEYStripe live publishable keypk_live_...Stripe dashboard β†’ Developers β†’ API Keys
STRIPE_PRICE_ID_PRO_ALPHAPrice ID for Pro Alpha (MXN 580/mo)price_...Stripe dashboard β†’ Products
STRIPE_PRICE_ID_PRO_MONTHLYPrice ID for Pro Monthly (MXN 980/mo)price_...Stripe dashboard β†’ Products
STRIPE_PRICE_ID_PRO_ANNUALPrice ID for Pro Annual (MXN 9,800/yr)price_...Stripe dashboard β†’ Products
STRIPE_PRICE_ID_FEATURE_UNLOCKPrice ID for Feature Unlock (MXN 980 one-time)price_...Stripe dashboard β†’ Products
LICENSE_SERVER_URLPublic auth host used by clients and checkout redirects.https://auth.seedsource.devRender env
PAYMENT_SUCCESS_URLRedirect after successful Stripe checkouthttps://seedsource.dev/payment/successConfigure to match frontend route
PAYMENT_CANCEL_URLRedirect after cancelled Stripe checkouthttps://seedsource.dev/payment/cancelConfigure to match frontend route
CORS_ORIGINSComma-separated allowlist for browser origins.https://internal-admin.onrender.com,https://auth.seedsource.devRender 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:

Terminal window
cd /app/license_server && alembic upgrade head

Check the current migration state

From the Render shell (service β†’ Shell tab):

Terminal window
cd /app/license_server
alembic current

From a local machine with DATABASE_URL set:

Terminal window
cd tooling/services/license-server
DATABASE_URL="<neon_connection_string>" alembic current

Check migration history

Terminal window
cd /app/license_server
alembic history --verbose

Manually apply migrations (Render shell)

Only needed if preDeployCommand was bypassed or failed mid-run:

Terminal window
cd /app/license_server
alembic upgrade head

To apply a specific revision:

Terminal window
alembic upgrade 005_add_payment_feature_unlocked

Rollback a migration

⚠️ Irreversible on data. Downgrading migrations that drop columns or tables will destroy data. Take a Neon branch snapshot first.

Terminal window
# Roll back one step
alembic downgrade -1
# Roll back to a specific revision
alembic downgrade 004_<previous_revision_name>
# Roll back all migrations (empty schema)
alembic downgrade base

Create a new migration

Terminal window
cd tooling/services/license-server
alembic 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_owner credential copied
  • DATABASE_URL set in Render with neondb_owner (not app_user)
  • SECRET_KEY generated (openssl rand -hex 32) and set in Render
  • JWT_SECRET generated (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_ID and GITHUB_CLIENT_SECRET set in Render
  • All 4 Stripe products created in dashboard; all 4 price IDs copied to Render env vars
  • STRIPE_SECRET_KEY and STRIPE_PUBLISHABLE_KEY set (use live keys for prod)
  • PAYMENT_SUCCESS_URL and PAYMENT_CANCEL_URL set and matching deployed frontend routes

DNS

  • auth.seedsource.dev CNAME record points to the Render service’s .onrender.com hostname
  • 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_SECRET from the newly registered endpoint and redeploy
  • Verify migrations: alembic current shows 005_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

  1. Go to Render dashboard β†’ service srv-d6mskc6a2pns73ddjlo0 β†’ Deploys.
  2. Find the last known-good deploy.
  3. Click Rollback to this deploy.
  4. 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 -1 is needed before rolling back β€” or ensure the old code is forward-compatible with the new schema.

Safe rollback sequence

Terminal window
# 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 rollback
cd /app/license_server
alembic downgrade -1
# 3. Then trigger the rollback in the Render dashboard

8. Health Verification Protocol

Run this sequence after every deploy to confirm all subsystems are operational.

1. License server health

Terminal window
curl -s https://auth.seedsource.dev/health | jq .
# Expected: {"status":"healthy", ...}

2. License server API reachable

Terminal window
curl -s -o /dev/null -w "%{http_code}" https://auth.seedsource.dev/
# Expected: 200 or 404 (not 502/503)

3. Stripe webhook endpoint reachable

Terminal window
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

Terminal window
curl -s -o /dev/null -w "%{http_code}" https://internal-admin.onrender.com/
# Expected: 200

5. Database connectivity (requires Render shell or local env)

Terminal window
cd /app/license_server
alembic current
# Expected: 005_add_payment_feature_unlocked (head)

6. Neon branch status

Terminal window
neonctl branches list
# Expected: main branch shows "ready"

Full one-liner health sweep

Terminal window
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"
done

9. Stripe Webhook Verification

Registered endpoint

https://auth.seedsource.dev/webhooks/stripe

Events subscribed (minimum required):

  • checkout.session.completed
  • customer.subscription.updated
  • customer.subscription.deleted
  • invoice.payment_failed

Verify endpoint is reachable

Terminal window
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

  1. Go to Stripe Dashboard β†’ Developers β†’ Webhooks.
  2. Click Add endpoint.
  3. URL: https://auth.seedsource.dev/webhooks/stripe
  4. Select events listed above.
  5. After saving, copy the Signing secret (whsec_...).
  6. Set STRIPE_WEBHOOK_SECRET in Render and redeploy.

Test with Stripe CLI (local only)

Terminal window
stripe listen --forward-to http://localhost:8000/webhooks/stripe
Terminal window
stripe trigger checkout.session.completed \
--override checkout_session:client_reference_id=1 \
--override checkout_session:customer_email=user@example.com

Verify 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:

  1. Check Render logs: render logs srv-d6mskc6a2pns73ddjlo0 --tail 100
  2. Identify the missing variable name in the traceback.
  3. Add the variable in Render dashboard β†’ Environment.
  4. 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:

  1. Open Render shell for the running service.
  2. Run: cd /app/license_server && alembic current
  3. If not at head, run: alembic upgrade head
  4. If migration fails, check DATABASE_URL is using neondb_owner credential (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:

Terminal window
# On Render shell:
cd /app/license_server
alembic history --verbose # check the chain
alembic current # see where DB is
alembic upgrade head # apply missing revisions

If the schema is ahead of alembic (manual DDL was run directly on Neon):

Terminal window
alembic stamp <revision-id> # mark DB as being at a specific revision

⚠️ alembic stamp does not run DDL β€” it only updates the alembic_version table. 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:

  1. Generate a new secret locally: openssl rand -hex 32
  2. Add to Render dashboard β†’ Environment β†’ JWT_SECRET.
  3. Trigger redeploy.

Note: Rotating JWT_SECRET immediately 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:

  1. Keep only one service block (internal-admin) in internal-projects/internal-admin/render.yaml.
  2. Re-apply the blueprint in Render.
  3. Delete any extra service created by the prior blueprint apply.
  4. Verify both hosts still route correctly through the single AIO container:
    • https://auth.seedsource.dev/health
    • https://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:

  1. Go to Neon dashboard β†’ your project β†’ Connection Details.
  2. Select the neondb_owner role (not app_user).
  3. Copy the connection string.
  4. Update DATABASE_URL in Render dashboard to use neondb_owner.
  5. Trigger a redeploy β€” preDeployCommand will retry migrations.

Long-term note: Keep app_user for the application’s runtime queries and neondb_owner exclusively for migrations (alembic). Do not use neondb_owner as the application’s runtime credential.


Appendix: Useful Commands Quick Reference

Terminal window
# View live Render logs
render 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 status
neonctl branches list
# Health check
curl -s https://auth.seedsource.dev/health | jq .
# Stripe webhook smoke test
curl -s -o /dev/null -w "%{http_code}" -X POST https://auth.seedsource.dev/webhooks/stripe
# β†’ 400 is correct
# Trigger Render redeploy via 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}'