Why SQLite?

Most databases need a separate server running alongside your app — PostgreSQL, MySQL, MongoDB, Redis all require you to install, configure, and keep a process running. During development that often means Docker, which eats RAM and adds yet another thing that can break.

SQLite is different. It's just a file.

No server, no setup

The database is up when you run pnpm dev. It's down when you hit Ctrl+C. No Docker, no background processes, no ports to manage, no "is the database running?" debugging.

Want to start fresh? Delete the file. Want a backup? Copy the file. It's that simple.

No port juggling

You don't realize how much pain port mapping causes until you stop dealing with it.

With server-based databases like Postgres, each project runs its own database server in Docker — but only one can use port 5432 on your machine. So you remap the second project to 5433, the third to 5434, and track each in a .env file.

Now multiply that across team members, each running different combinations of projects, each with their own Docker Compose port mappings. It becomes a mess of conflicting .env files and "works on my machine" bugs. SQLite has none of this — each project just has its own .db file.

Perfect for building lots of apps

Imagine you want to build 100 small apps to experiment with ideas. With Postgres, that's 100 database servers you'd need running — or one shared server with 100 databases to manage. With SQLite, each app is just a .db file sitting quietly on disk until you need it.

Not a toy

SQLite handles billions of rows. It powers every iPhone, every Android phone, every browser, and every Mac. It's the most deployed database engine in the world — by a wide margin. Your app won't outgrow it.

Production-ready with Turso

SQLite runs inside your app — it's not a standalone server that accepts connections over the network.

That's great for development, but in production you need your database accessible from a cluster of hosted servers. That's where Turso comes in — it runs SQLite as a hosted service. Your Drizzle queries don't change; you just point to a different URL. Free tier included.

The right trade-off

Could you use Postgres? Sure — it handles more concurrent writes and has more features. But for the vast majority of web apps, SQLite is simpler: no Docker during development, no port conflicts, no connection pool tuning.

In production with Turso, you get a hosted database like any other — but your local dev experience stays frictionless.

You can always migrate later — Drizzle ORM abstracts the SQL dialect, so switching means changing your connection config, not rewriting queries. Most apps never need to.

Advanced: How it compares

SQLitePostgreSQLMySQL
Server requiredNo — just a fileYesYes
ArchitectureIn-process, embeddedMulti-process (fork per connection)Multi-threaded (thread per connection)
Concurrency modelSingle-writer, multiple-reader (WAL mode)MVCC, concurrent writersMVCC (InnoDB), concurrent writers
Storage engineB-tree, single fileHeap tables + separate indexesClustered index (InnoDB B-tree)
UPDATE behaviorCopy-on-write (new row version)Row duplication + VACUUMIn-place update + rollback segments
ReplicationTurso (LiteFS, managed)Streaming WAL replicationBinary log + redo log (separate)
JSON supportJSON functions, json_extractJSONB with indexing, operatorsJSON type with indexing
Full-text searchFTS5 extensionBuilt-in tsvector/tsqueryInnoDB FTS
Best forRead-heavy, most web appsHeavy concurrent writes, analytics, GISHigh-connection transactional workloads

Where the others win

PostgreSQL is the right choice when you need heavy concurrent writes (SQLite serializes all writes through a single writer), advanced indexing (GIN, GiST, BRIN), PostGIS for geospatial queries, or analytical workloads with complex joins across large datasets. Its MVCC implementation handles multiple writers without blocking readers.

MySQL (InnoDB) excels at high-connection transactional workloads. Its clustered index embeds rows directly in the B-tree, so primary key lookups are a single I/O. In-place updates with rollback segments avoid the garbage collection overhead that PostgreSQL's VACUUM and SQLite's WAL checkpointing both deal with. When you have thousands of concurrent connections, MySQL's threading model uses less memory per connection than PostgreSQL's forked processes (a few MB per thread vs 5–10 MB per process).