Skip to main content
← Back to Blog

Migrating from Cassandra to PostgreSQL with Neon

Next.jsPostgreSQLNeonArchitecture

When I first built this website, I chose Apache Cassandra — partly because it was the database I was using in my larger e-commerce platform, and partly because the dual-mode adapter (local Docker vs. cloud Astra DB) made for an interesting architectural showcase.

But as the site matured, the mismatch between tool and task became clear. A portfolio website with two tables doesn't need a distributed NoSQL database. It needs something simple, reliable, and cheap to host.

Why PostgreSQL?

  • Schema enforcement: NOT NULL constraints, DEFAULT values, and ON CONFLICT DO UPDATE (true upserts) replace Cassandra's implicit-null, last-write-wins model.
  • Native arrays: PostgreSQL TEXT[] replaces Cassandra SET<TEXT>, and the driver returns real JavaScript arrays — no more Set conversion logic.
  • Standard SQL: ORDER BY in queries, WHERE featured = true without ALLOW FILTERING, and $1, $2 parameterized queries.
  • Simpler cloud story: Neon gives you a connection string. That's it. No Secure Connect Bundles, no base64-encoded zip files, no USE_ASTRA toggle.

The Migration

The old architecture had two database client implementations — LocalCassandraClient and AstraDbClient — behind a DbClient interface, switched by an environment variable:

// Before: dual-mode Cassandra adapter
const useAstra = process.env.USE_ASTRA === 'true';
dbClient = useAstra ? new AstraDbClient() : new LocalCassandraClient();

The new architecture is a single client using @neondatabase/serverless. The same driver talks to a local Docker PostgreSQL instance (via the Neon HTTP proxy) in development and Neon's serverless endpoint in production:

// After: single connection string, one driver
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.POSTGRES_URL!);

The entire db.ts file went from 101 lines to 65.

What Changed

LayerBeforeAfter
Drivercassandra-driver@neondatabase/serverless
Local DBCassandra 4.1 containerPostgreSQL 16 + Neon HTTP proxy
Cloud DBDataStax Astra DB (SCB bundle)Neon (connection string)
SchemaCQL (init.cql)SQL (init.sql, auto-applied by Docker)
UUIDstypes.Uuid.random()crypto.randomUUID()
ArraysSet<string> → manual conversionTEXT[] → native arrays
UpsertsCassandra INSERT (implicit upsert)ON CONFLICT DO UPDATE

What Stayed the Same

The DbClient interface didn't change. The repositories still call db.execute(query, params). The portfolio pages, contact form, and health endpoint all work exactly as before — they never knew which database was behind the adapter.

That's the value of a clean abstraction layer: swapping the entire database engine was a one-day change with zero modifications to any page component.