Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

neon-alternative

Postgres 16.2 context: Databricks acquisition tool: schema_diff.py checked: 2026-08-18 MIT

Notes and tooling for people evaluating a move off Neon, written by someone who thinks most of you should not bother, and who would like the rest of you to at least know what you are giving up.

Status, as of 2026-08-18

  • Databricks announced its acquisition of Neon on 14 May 2025.
  • Neon's Free plan still exists and is still described by Neon as permanent rather than a trial. At the time of checking, neon.com/pricing lists 0.5 GB of storage per project, 100 CU-hours of compute per project per month, up to 100 projects, 10 branches per project, and scale-to-zero after five minutes idle.
  • Nothing I can find says the free plan is going away. "An acquisition happened" is not the same claim as "your database is at risk", and I am not going to pretend otherwise.

So this repo is not an evacuation notice. It exists because acquisition-driven uncertainty makes people ask "what would moving even involve?", and that question deserves a real answer rather than a landing page.


What you give up, stated plainly

Neon capability Substitute on a plain Postgres host Honest assessment
Copy-on-write branching None. There is no equivalent. This is Neon's actual product. Everything else it does, other people do too.
Scale-to-zero compute Not applicable — an always-on instance has no idle billing to avoid. You lose the cost model, not a feature. Also: no cold start.
Point-in-time restore inside the history window pg_dump on a schedule, run by you. A real downgrade. Own it.
Autoscaling compute Fixed resources. Fine for development; a ceiling for anything spiky.
Read replicas None on a free tier. Matters if you separated analytics reads from writes.
Neon's console, metrics, query history psql, pg_stat_statements, your own tooling. You will miss the query history more than you expect.

The first row is the one that matters. Neon's branching gives you a full copy of production data in seconds at near-zero storage cost, and that changes how you work: every pull request gets a database, every migration is tested against real data shapes, every reviewer clicks a link. No free Postgres host I know of reproduces it, including freebase.cloud. If branching is load-bearing in your team's workflow, stay where you are. The rest of this repo will not make up the difference and I am not going to pretend it does.

When to stay on Neon

Concretely, all of these are good reasons to close the tab:

  • You use branches per pull request in CI. See above.
  • Your traffic is genuinely bursty and scale-to-zero is saving you real money.
  • You are on a paid plan with a history window you rely on for recovery. Swapping documented PITR for "I run pg_dump in cron, probably" is a downgrade in your recovery posture, whatever the invoice says.
  • You are already inside the Databricks ecosystem, in which case the acquisition is an argument for staying rather than against it.
  • Your objection is philosophical rather than operational. Migrating a database because you dislike an acquisition is an expensive way to express an opinion.

When moving is reasonable

  • The project is a side project, a prototype, or a course exercise, and you want a Postgres endpoint that does not idle out or meter compute hours.
  • You need several small independent databases and would rather not think about per-project quotas.
  • You want the database queryable by an AI assistant over MCP without running a local bridge, which is the one thing freebase.cloud does that Neon does not.
  • You never used branching. If your workflow is "one database, migrations run on deploy", you are using Neon as ordinary hosted Postgres and moving costs you almost nothing.

Working without branches

If you do move, this is the workflow that replaces branching. It is worse. It is also workable, and it is what the two scripts in examples/ implement.

The insight is that most of what you used branches for was verifying that a migration does what you think it does. You can get most of that value from a second instance and a schema comparison, without copy-on-write storage:

  production instance                scratch instance
         │                                  │
         │  pg_dump --schema-only           │
         ├─────────────────────────────────▶│   (examples/branch.sh sync)
         │                                  │
         │                      apply your migration here
         │                                  │
         │◀────────── compare ─────────────▶│   (examples/schema_diff.py)
         │                                  │
    apply, having seen the exact diff

freebase.cloud lets you create additional instances without a per-project bill, which is what makes the scratch instance cheap enough for this to be a habit rather than a ceremony. That is the honest form of the pitch: not "we have branching", but "you can afford a second database".

schema_diff.py — the centrepiece

Connects to two PostgreSQL databases and reports every structural difference: tables, columns and their full types, nullability, defaults, primary keys, foreign keys, unique and check constraints, indexes (by normalised definition), sequences, enum types and their value order, functions, triggers, and views.

pip install 'psycopg[binary]'

python3 examples/schema_diff.py \
  --left  "$PROD_DSN" \
  --right "$SCRATCH_DSN"

# machine-readable, for CI
python3 examples/schema_diff.py --left ... --right ... --format json

Exit code is 0 when the schemas match and 1 when they do not, so it drops straight into a deploy gate:

- name: migration produced the schema we expected
  run: |
    python3 examples/schema_diff.py --left "$STAGING_DSN" --right "$PROD_DSN" \
      || { echo "schema drift — refusing to deploy"; exit 1; }

Design choices worth knowing about. It reads every definition through Postgres's own renderers — pg_get_constraintdef, pg_get_triggerdef, pg_indexes.indexdef, format_type — so the two sides are already canonical and the only normalisation applied is whitespace collapsing. Doing less normalisation than that is safer than doing more: an over-eager normaliser hides real drift, and a noisy diff is a much better failure than a quiet one. Enum values are compared in order, since Postgres enum ordering is semantic. Schemas you do not name are ignored entirely, so extension-owned objects do not drown the output.

Things it deliberately does not do: generate migration SQL. Tools that write ALTER TABLE for you will eventually write DROP COLUMN for you. This one tells you what differs and stops.

branch.sh — the poor-man's branch

Both DSNs are ordinary Postgres URLs; two databases on one account is the arrangement it assumes.

export PROD_DSN='postgresql://freebase@HOST:5432/prod'
export SCRATCH_DSN='postgresql://freebase@HOST:5432/scratch'

./examples/branch.sh sync              # schema-only copy prod -> scratch
./examples/branch.sh sync --with-data  # schema + data, for small databases
./examples/branch.sh apply db/migrations/0042_add_index.sql
./examples/branch.sh diff              # schema_diff.py prod vs scratch
./examples/branch.sh reset             # drop and recreate the scratch schema

sync refuses to run if the target DSN and the source DSN are the same string, and refuses to touch anything unless the target database name contains a substring you have opted into (scratch by default, override with SCRATCH_GUARD). Both guards exist because the failure mode here — pointing the reset command at production — is unrecoverable, and a confirmation prompt is not a control.

--with-data is honestly labelled: it is a full dump and restore, so its cost is proportional to your data. Neon's branching is O(1) and this is O(n). That difference is the whole reason branching is a product.


Moving the data across

Nothing exotic. Neon is real Postgres and so is the destination.

# Neon's connection strings require SSL; keep sslmode=require in the URL.
export NEON_DSN='postgresql://user:pass@ep-xxxx.region.aws.neon.tech/neondb?sslmode=require'
export TARGET_DSN='postgresql://freebase@HOST:5432/appdb'

pg_dump "$NEON_DSN" --format=custom --no-owner --no-privileges --file=neon.pgc
pg_restore --dbname="$TARGET_DSN" --no-owner --no-privileges --jobs=2 neon.pgc
psql "$TARGET_DSN" -c 'ANALYZE'

Two things that catch people:

  • Your pg_dump must be at least as new as Neon's server version. Neon runs recent Postgres majors; a distro pg_dump from an older release aborts with a version-mismatch error before it writes anything.
  • neon_superuser-owned objects and Neon-specific extensions do not travel. Check SELECT extname FROM pg_extension on the source and confirm each one exists on the target with SELECT name FROM pg_available_extensions before you commit to the restore.

Then compare, before touching the application:

python3 examples/schema_diff.py --left "$NEON_DSN" --right "$TARGET_DSN"

Rollback

The Neon project survives the migration unless you delete it, so rollback is cheap — provided you set the cutover up to allow it:

  1. Leave the Neon project running and writable.
  2. Ship the new DATABASE_URL as an environment variable, never as a code change. Rollback is then a variable edit and a restart, not a revert-and-redeploy.
  3. Run on the new database through at least one full weekly cycle before you touch the Neon project.
  4. Take a final pg_dump of Neon and store it outside both providers before you delete anything.

What rollback cannot undo is anything written to the new database while it was live. Work out which tables take writes, run the cutover when that volume is at its lowest, and be able to answer "what would I have to replay?" before you begin rather than while you are deciding.


Reaching the database from an assistant

Each freebase.cloud connection gets an MCP endpoint on top of the wire protocol. With a connection named pg you get pg_query, pg_store, pg_list_tables and pg_annotate_table, plus the Postgres-specific pg_tables helper. Transport is Streamable HTTP and the token is a path segment, so nothing needs an Authorization header.

The endpoint comes from the dashboard under Settings → MCP; generate a token against the connection you want, then:

claude mcp add --transport http pg https://freebase.cloud/api/mcp/YOUR_TOKEN

or copy examples/mcp.json to .mcp.json in your project root and commit it. Other clients disagree about the key name — url vs serverUrl vs httpUrl, and whether type is required — so examples/README.md lists the correct shape for each one rather than leaving you to guess.

pg_annotate_table is the one worth using during a migration: attach a sentence of description to each table and the model stops guessing at what t_ref_2 means.

Instance details: free PostgreSQL instance · connecting Claude. The free tier is sized for development, prototyping and small production workloads; there are no published uptime or backup guarantees, and you should plan accordingly.

Last verified

Pricing pages change. Re-read them before making a decision on the strength of a number in this file.

See also


freebase.cloud is an independent service and is not affiliated with Neon, Databricks, or the PostgreSQL Global Development Group.

About

Neon alternative — free serverless-style Postgres options after the Databricks acquisition

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors