Choosing a database for a small SaaS
Most indie SaaS projects pick PostgreSQL on day one without thinking about it. It is the "safe default" and nobody ever got fired for choosing Postgres. But that does not mean it is the right choice for every project, especially small ones where you are the only developer and the only ops person.
Here is how I actually think through the database decision for small SaaS projects.
Start with the real question
The question is not "which database is best?" It is "how many servers will my application run on?" That single question eliminates most of the debate.
If your app runs on one server, SQLite is almost certainly the right starting point. If you need multiple servers writing to the same database, you need a client-server database. That is it. Everything else is details.
The thing most developers get wrong is overestimating how soon they will need multiple servers. A single VPS with 4 cores and 8 GB of RAM can handle a surprising amount of traffic. Thousands of concurrent users, easily. Most indie SaaS projects never outgrow a single server, and the ones that do usually take years to get there.
SQLite: the underrated option
I have written about SQLite before, so I will keep this brief. SQLite is an embedded database. No server process, no network overhead, no connection pooling to configure. Your database is a single file on disk.
For read-heavy workloads on a single server, SQLite is genuinely faster than PostgreSQL. There is no network round trip between your app and the database. Reads take microseconds instead of milliseconds. For a small SaaS doing mostly reads with occasional writes, that performance difference adds up.
Configuring SQLite for production
The default SQLite configuration is not optimized for web applications. These PRAGMAs make a significant difference:
PRAGMA journal_mode = WAL; -- Concurrent reads + writes (non-negotiable)
PRAGMA synchronous = NORMAL; -- Safe with WAL, avoids fsync on every commit
PRAGMA busy_timeout = 5000; -- Wait 5s for locks instead of failing immediately
PRAGMA cache_size = -64000; -- 64MB cache
PRAGMA foreign_keys = ON; -- Enforce FK constraints
PRAGMA temp_store = MEMORY; -- Keep temp tables in RAMWAL mode (Write-Ahead Logging) is the most important one. Without it, readers and writers block each other. With it, you get concurrent reads alongside a single writer, which is how most web apps actually operate.
The busy_timeout is the second most critical setting. Without it, any concurrent write that hits contention throws an immediate error instead of waiting for the lock. Setting it to 5000ms dramatically reduces crash rates in production.
When SQLite's limits actually bite
SQLite allows many concurrent readers but only one writer at a time. The single-writer limit becomes a real problem when:
- You have more than ~100 sustained write transactions per second
- You have long write transactions that block other writers
- You need multiple application servers writing to the same database
For a typical small SaaS (task manager, CRM, internal tool) doing a few writes per second with reads dominating, SQLite is more than sufficient. Companies like Tailscale, Expensify, and Signal run SQLite in production at significant scale. Tailscale migrated from etcd to SQLite because it simplified their architecture.
Backups: Litestream
The real concern with SQLite is durability. If your server dies, your database file goes with it. Litestream solves this. It continuously replicates your SQLite database to S3 (or any S3-compatible storage) by streaming WAL pages as they are written. You get point-in-time recovery without running a separate database server.
Litestream v0.5 (released October 2025) introduced a new file format with hierarchical compaction that dramatically improves restore times, and added point-in-time recovery to any moment in the past. It effectively solves the "but what about backups?" objection that used to push people toward Postgres prematurely.
PostgreSQL: when you actually need it
PostgreSQL is excellent software. I am not arguing against it. I am arguing against choosing it by default when you do not need it yet.
Running Postgres adds operational complexity. You need to manage a database server (or container), configure connection pooling, handle backups, monitor replication lag if you set up replicas, and deal with connection limits. For a solo developer shipping a SaaS, every piece of infrastructure you add is something you have to maintain at 2 AM when it breaks.
That said, there are legitimate reasons to start with Postgres:
- You know from day one you will need multiple servers writing to the same database
- You need advanced features like PostGIS for geospatial queries, full-text search with ranking, or JSONB indexing
- Your write volume is genuinely high and you need fine-grained concurrency control
- You need complex transactions spanning multiple tables with row-level locking
The key word is "know." Not "might need someday" but "know right now." Premature infrastructure decisions are one of the biggest time sinks for indie developers.
Managed databases: the landscape in 2026
The managed database market has shifted since PlanetScale removed their free tier in early 2024 (jumping from $0 to $39/month with no intermediate option). That triggered a mass migration of indie developers to other providers.
Neon (serverless Postgres)
Neon's free tier is the most generous for pure Postgres: 100 compute-unit-hours per month, 0.5 GB storage, and scale-to-zero so you pay nothing when idle. The killer feature is instant database branching, which gives you a full copy of your database for preview environments and CI/CD without the storage cost of a full clone.
Paid plans start at $19/month. After Databricks acquired Neon in May 2025, pricing dropped significantly.
Supabase (Postgres + backend)
Supabase is more than a database. It includes auth, realtime subscriptions, file storage, edge functions, and a REST API on top of Postgres. The free tier gives you 500 MB of database storage but pauses after 7 days of inactivity, which makes it unsuitable for always-on production.
The Pro plan at $25/month includes daily backups, 8 GB storage, and no pausing. It is a good option if you want a full backend platform rather than just a database.
Turso (managed SQLite)
Turso runs libSQL, a fork of SQLite that adds native replication. The standout feature is embedded replicas: your application syncs a local SQLite file that handles reads with zero network latency (microsecond reads), while writes go to the remote primary and propagate back automatically.
The free tier is generous: 5 GB storage, 100 databases, 500M row reads per month. Paid plans start at $5/month. If you like SQLite but want managed infrastructure and multi-region replication, Turso is the best option available.
Cloudflare D1 (SQLite at the edge)
If you are already on Cloudflare Workers, D1 is worth considering. It runs SQLite on Cloudflare's edge network with automatic global read replication. The free tier includes 5 million rows read per day and 5 GB storage.
The limitation is that D1 is designed for the Workers ecosystem. Each database maxes out at 10 GB, and it is best suited for a per-tenant or per-user database architecture rather than a single monolithic database.
Edge databases: the latency story
The trend in 2025-2026 is moving databases closer to users. The latency differences are significant:
| Solution | Read latency |
|---|---|
| SQLite local file | ~0.01ms |
| Turso embedded replica | ~0.02ms |
| Cloudflare D1 from Workers | ~0.5ms |
| Managed Postgres (same region) | 5-15ms |
| Managed Postgres (cross-region) | 30-80ms |
For read-heavy applications, the gap between local SQLite and a remote Postgres instance is two to three orders of magnitude. This does not matter for a dashboard that loads once, but it matters a lot for applications that make many database calls per page render.
ORMs make switching easier
One thing that reduces the stakes of this decision is using an ORM or query builder that supports multiple databases.
Drizzle is my current preference. It uses code-first schema definition in TypeScript (no separate schema language or code generation step), supports PostgreSQL, MySQL, and SQLite including serverless variants, and generates migrations from schema diffs. The query API maps directly to SQL, so you always know what the database is executing.
Prisma is the other major option. Prisma 7 (late 2025) was a significant improvement: 85-90% smaller engine, up to 3.4x faster queries, and 9x better cold starts after rewriting from Rust to TypeScript/WASM. It is more mature and has richer tooling, but Drizzle's SQL-like approach and tiny bundle size (7.4KB vs Prisma's larger footprint) make it a better fit for new projects.
If you start with SQLite and outgrow it, migrating to Postgres through either ORM is a weekend project, not a rewrite. Your queries stay the same, you update your connection config, run migrations, and move on.
The cost of managed vs self-hosted
Self-hosting looks cheaper on paper ($5-20/month for a VPS) but the hidden cost is operational time. For a small team, the realistic breakdown:
| Approach | Monthly cost | Operational burden |
|---|---|---|
| SQLite + Litestream on VPS | $5-20 | Medium: you manage backups, updates, monitoring |
| Turso Developer | $5 | Low: managed, embedded replicas |
| Cloudflare D1 | $0-5 | Very low: managed, Workers ecosystem |
| Neon free tier | $0 | Very low: managed, scale-to-zero |
| Supabase Pro | $25 | Very low: full backend included |
| Self-hosted Postgres on VPS | $5-20 | High: backups, upgrades, monitoring, HA |
For most small teams, the $5-25/month premium for a managed service is worth it compared to the operational burden of self-hosting Postgres. The exception is SQLite + Litestream, where the operational burden is genuinely low because there is no database server to manage.
My recommendation
Start with SQLite unless you have a concrete reason not to. Configure it properly (WAL mode, busy_timeout, cache_size). Set up Litestream for backups on day one. Use Drizzle so migration to Postgres is painless later. Ship your product instead of configuring infrastructure.
If you want managed infrastructure without running a VPS, use Turso (if you like SQLite) or Neon's free tier (if you want Postgres). Both are genuinely free for small projects and scale gracefully as you grow.
When you hit the actual limits of SQLite (multiple servers, high write concurrency, advanced Postgres features), you will know. And at that point you will have real users and real revenue to justify the added complexity. That is a much better position to be in than running a Postgres cluster for an app with twelve users.
Sources
Related posts
Why I built Omnibase: a universal database MCP server
I got tired of copy-pasting query results between DataGrip and AI agents. So I built an MCP server that gives AI agents secure, direct access to any database.
Delta libraries: how diffing works and which library to use
What delta libraries do, how diff algorithms work under the hood, and a practical comparison of the most popular options in the JavaScript ecosystem.
Offline-first apps: harder than it sounds
Building apps that work without internet is one of those things that seems straightforward until you actually try it. Here is what makes it hard and how to approach it.
Enjoying the blog? Subscribe via RSS to get new posts in your reader.
Subscribe via RSS