feat(ops): migrate Postgres to Neon.tech — verified end-to-end (#50) - #56
Merged
Conversation
Verifies get_database_url() passes a Neon-style pooled DSN through unchanged (sslmode + -pooler host preserved), normalizes bare postgres:// scheme with query string intact, and that sqlalchemy.create_engine() constructs (lazily, no connection) for the resulting URL. DSN handling only -- does NOT verify live Neon connectivity. That is verified by the owner during cutover per _docs/RUNBOOK-neon-migration.md. Assisted-by: Claude Code (sonnet subagent) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#50) render.yaml: target end-state after migrating off Render Postgres to Neon.tech. Removes the databases: block (elevareai-db) and changes elevareai-api's DATABASE_URL from a fromDatabase: binding to sync: false (manual secret, set to the Neon pooled connection string with ?sslmode=require in the Render dashboard). Updates the FREE TIER NOTES comment: the Render-Postgres 30-day-expiry note no longer applies; notes Neon's free tier is persistent with autosuspend/cold-start absorbed by the existing pool_pre_ping=True. No application code change needed: get_database_url() in src/config/settings.py already passes DATABASE_URL through verbatim (only normalizing postgres:// to postgresql://), and src/config/database.py's create_engine() is lazy with pool_pre_ping=True already set, so a Neon pooled DSN works unchanged. Adds _docs/RUNBOOK-neon-migration.md: the ordered cutover plan, emphasizing that removing the Render database (step 6) is destructive and must only happen after Neon is confirmed serving traffic (steps 4-5). Documents the DB_* vs DATABASE_URL nuance in scripts/setup_db.py. Marked as PLAN ONLY -- not yet executed against a live Neon instance. Adds a pointer at the top of _docs/RUNBOOK-db-expiry-recovery.md (#34) noting it no longer applies once migrated to Neon. .env.example: adds a commented Neon DATABASE_URL example (format only, no real value); existing DB_* local-dev defaults unchanged. Assisted-by: Claude Code (sonnet subagent) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Assisted-by: Claude Code (sonnet subagent) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Assisted-by: Claude Code (sonnet subagent) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add test_psycopg2_params_from_url_carries_sslmode_and_channel_binding, asserting that psycopg2 connect() params built from a Neon-style DSN (?sslmode=require&channel_binding=require) include those query params. Currently red: run_migration() builds conn_params directly from url.host/port/database/user/password, dropping the DSN query string. Assisted-by: Claude Code (sonnet subagent) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…#50) run_migration() previously built psycopg2 conn_params from engine.url.host/port/database/user/password only, dropping the DSN query string (sslmode, channel_binding). It worked against Neon only because psycopg2 defaults to sslmode=prefer. Extract the param-building into _psycopg2_params_from_url(), which merges url.query into the conn_params dict so a Neon DSN's SSL requirements are honored explicitly. Also note in create_database_if_not_exists() that it carries no SSL/DSN params and should be skipped (--skip-create-db) for managed DBs like Neon. Assisted-by: Claude Code (sonnet subagent) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tes (#50) - RUNBOOK-neon-migration.md: banner now reflects that migration mechanics (connect via pooled SSL endpoint, migrate 18 tables via setup_db.py, seed demo data, demo@elevare.ai bcrypt login) were verified against a live Neon instance; remaining work is the Render-dashboard cutover. Fix step-3 over-claim: run_migration now explicitly carries the DSN's SSL params via _psycopg2_params_from_url(). Add a GOTCHA section on never adding non-schema keys (e.g. NEON_EXTERNAL_DATABASE_URL) to .env, since Settings uses extra="forbid". Note that cutover is a connection-string change, not a re-seed, since data already exists in the verified Neon DB. - README.md / _docs/ARCHITECTURE.md: update the Render Blueprint description -- Postgres is now on Neon (persistent free tier), Render hosts only the web service + static site. Point to the Neon runbook. Assisted-by: Claude Code (sonnet subagent) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ording; guard query tuple (#50)
franciszver
marked this pull request as ready for review
July 24, 2026 11:58
Owner
Author
|
VERIFIED against live Neon (2026-07-24): connect via pooled SSL endpoint, 18 tables migrated via |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Prep for #50 — DRAFT, do NOT merge until the owner verifies against a real Neon instance.
Status: PLAN ONLY, unverified against live Neon
This branch prepares the migration; nothing here has been run against a real Neon database. Merge only after the cutover succeeds (see runbook).
What's here
databases:block removed,DATABASE_URLswitched fromfromDatabase:to async:falsemanual secret (Neon pooled string +?sslmode=require), free-tier notes updated (Render 30-day expiry no longer applies; Neon persistent + autosuspend).get_database_url()already passes a Neonpostgresql://...?sslmode=requireDSN through verbatim;create_engineis lazy withpool_pre_ping=True. Confirmed by reading both.tests/test_neon_dsn_handling.py— validates DSN handling only (Neon DSN preserved,postgres://→postgresql://normalization keeps query string, SQLAlchemy accepts the URL lazily). Does NOT verify live Neon connectivity (owner does, per runbook)._docs/RUNBOOK-neon-migration.md— ordered cutover with a.env.example— commented Neon DSN example.RUNBOOK-db-expiry-recovery.md— pointer noting Neon supersedes it.scripts/setup_db.py::get_db_connection_string()builds its connection fromDB_*parts and does NOT appendsslmode— but Neon requires SSL. The migration/seed step will fail against Neon unless setup_db.py is taught to honorDATABASE_URLor appendsslmode=require. Documented in the runbook; needs a decision (fix in this PR vs. handle at cutover).Before eventual merge
Full three-gate review + owner's live-Neon verification. Suite currently: 528 passed, 1 skipped, 2 xfailed.
🤖 Generated with Claude Code
https://claude.ai/code/session_016v4C1daTwNakL6QDbmy3MJ