Embedded Databases: SQLite, LibSQL, and DuckDB
Not every project needs a database server. If you're building a CLI tool, a desktop app, an edge function, or a small SaaS that doesn't justify running PostgreSQL, an embedded database might be exactly right. The database lives in your process or a single file, not in a separate server you have to install, configure, and keep running.
"Embedded" here means no daemon process, no TCP connections to manage, no connection pool to tune. You link a library (or open a file), call functions, and get results. There's no network hop. When your app shuts down, the database stops too. When your app starts, the database is just there.
There are three embedded databases worth knowing about right now: SQLite, LibSQL, and DuckDB. They solve different problems, and picking the right one saves you from fighting your tools later. I'll walk through what each one does well, where it falls short, and when I reach for it.
Contents
- SQLite: The Default Choice
- LibSQL: SQLite with Server Mode
- DuckDB: SQLite for Analytics
- Comparison Table
- Running Them in the Cloud
- When to Pick Each One
- Further Reading
SQLite: The Default Choice
SQLite is the most deployed database in the world. It's in every phone, every browser, every Electron app. It reads and writes to a single file. No configuration, no server process, nothing to manage.
I reach for SQLite constantly for CLI tools and small apps. It's fast, reliable, and the surface area is tiny. You get full SQL support (joins, CTEs, window functions, JSON), transactions that actually work, and a file you can copy or back up with cp. Expensify runs it in production at 4 million queries per second.
The limitations are real though. SQLite supports only a single writer at a time. There's no network access built in (only the process that opens the file can use it). And there's no replication without third-party tools. For many use cases, none of that matters. For others, it's a dealbreaker.
Best for: read-heavy workloads, local-first apps, CLI tools, development databases, single-process applications.
Quick Start with SpinDB
SpinDB runs SQLite behind a PostgreSQL-compatible proxy, so you can connect from any PG client or driver. (What is SpinDB?)
npm i -g spindb
spindb create sqlite1 -e sqlite --start
spindb url sqlite1postgresql://127.0.0.1:5432/sqlite1Connect and run a query:
import pg from 'pg'
const client = new pg.Client({
connectionString: 'postgresql://127.0.0.1:5432/sqlite1',
})
await client.connect()
await client.query(`
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
body TEXT,
created_at TEXT DEFAULT (datetime('now'))
)
`)
await client.query(
`INSERT INTO notes (title, body) VALUES ($1, $2)`,
['First note', 'SQLite is surprisingly capable.']
)
const { rows } = await client.query('SELECT * FROM notes')
console.log(rows)
await client.end()For in-process usage without SpinDB, better-sqlite3 gives you synchronous access with no network layer at all.
For the full walkthrough, see Getting Started with SQLite.
LibSQL: SQLite with Server Mode
SQLite has two hard limitations: no network access and no built-in replication. LibSQL is an open-source fork by the Turso team that removes both.
LibSQL adds HTTP and WebSocket access so multiple clients can connect to the same database over the network. It supports embedded replicas that sync from a primary, which is great for edge deployments where you want local reads with central writes. It also adds native vector search via an F32_BLOB type and distance functions, plus ALTER TABLE improvements that SQLite still lacks.
The important part: LibSQL is fully SQLite-compatible. Same SQL dialect, same file format. Your existing knowledge and queries transfer directly. It's a superset, not a replacement.
Best for: local-first apps that need sync, edge deployments with embedded replicas, apps that want SQLite simplicity but also need network access or vector search.
Quick Start with SpinDB
spindb create libsql1 -e libsql --start
spindb url libsql1http://127.0.0.1:8080LibSQL runs its own HTTP server natively, so you use the @libsql/client package instead of a PG driver:
import { createClient } from '@libsql/client'
const client = createClient({ url: 'http://127.0.0.1:8080' })
await client.execute(`
CREATE TABLE IF NOT EXISTS bookmarks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
url TEXT NOT NULL,
title TEXT,
tags TEXT DEFAULT '[]'
)
`)
await client.execute({
sql: 'INSERT INTO bookmarks (url, title) VALUES (?, ?)',
args: ['https://layerbase.com', 'Layerbase'],
})
const result = await client.execute('SELECT * FROM bookmarks')
console.log(result.rows)For the full walkthrough, see Getting Started with LibSQL.
DuckDB: SQLite for Analytics
SQLite and LibSQL are built for transactional workloads: inserting rows, reading individual records, updating data. DuckDB is built for the opposite. Columnar storage, vectorized execution, and query optimizations designed for aggregation-heavy analytical work. Think rollups, pivots, and scanning millions of rows across a few columns.
DuckDB's file querying is genuinely one of my favorite features in any database. You can run SQL directly against CSV and Parquet files without importing them first. No ETL pipeline, no data loading step. Just SELECT * FROM 'sales.csv' WHERE revenue > 1000.
DuckDB is not for transactional workloads. If your app does CRUD operations (insert a user, update a profile, delete a record), use SQLite or LibSQL. But if you need to answer analytical questions over data, DuckDB is hard to beat.
Best for: ad-hoc data analysis, embedded analytics in applications, data transformation pipelines, querying files without ETL.
Quick Start with SpinDB
SpinDB runs DuckDB behind a PostgreSQL-compatible proxy, just like SQLite:
spindb create duck1 -e duckdb --start
spindb url duck1postgresql://127.0.0.1:5432/duck1import pg from 'pg'
const client = new pg.Client({
connectionString: 'postgresql://127.0.0.1:5432/duck1',
})
await client.connect()
await client.query(`
CREATE TABLE IF NOT EXISTS sales (
product TEXT,
category TEXT,
revenue DECIMAL(10,2),
sale_date DATE
)
`)
await client.query(`
INSERT INTO sales VALUES
('Widget A', 'Hardware', 29.99, '2026-01-15'),
('Widget B', 'Hardware', 49.99, '2026-01-15'),
('Service X', 'Software', 99.00, '2026-02-01')
`)
const { rows } = await client.query(`
SELECT category, SUM(revenue) as total
FROM sales
GROUP BY category
ORDER BY total DESC
`)
console.log(rows)
await client.end()For the full walkthrough, see Getting Started with DuckDB.
Comparison Table
| Feature | SQLite | LibSQL | DuckDB |
|---|---|---|---|
| Primary workload | OLTP (transactions, CRUD) | OLTP (transactions, CRUD) | OLAP (analytics, aggregations) |
| Storage model | Row-oriented | Row-oriented | Columnar |
| Network access | None (in-process only) | HTTP/WebSocket server mode | None (in-process only) |
| Replication | Not built-in | Embedded replicas from primary | Not built-in |
| Vector search | Requires sqlite-vec extension | Native F32_BLOB type | Not built-in |
| File querying (CSV/Parquet) | No | No | Yes, native |
| Concurrent writers | Single writer | Single writer (server manages access) | Single writer |
| SpinDB connection | PG proxy (pg driver) | HTTP (@libsql/client) | PG proxy (pg driver) |
| Maturity | 25+ years | ~3 years | ~5 years |
| Best use case | Embedded apps, CLI tools, local-first | Multi-client access, edge sync, vector search | Analytics, data pipelines, file queries |
Running Them in the Cloud
All three are available as managed instances on Layerbase Cloud. SQLite and DuckDB run behind PG-compatible proxies, so you connect with any PostgreSQL client. LibSQL runs its own HTTP server.
Cloud instances use TLS. The Quick Connect panel gives you the exact connection snippet for your instance and language.
When to Pick Each One
Pick SQLite when you need a reliable embedded database and your app is a single process. CLI tools, desktop apps, mobile apps, development databases. If you don't need network access or replication, SQLite is the simplest and most battle-tested option.
Pick LibSQL when you need SQLite's simplicity but also need server mode (multiple clients connecting), embedded replicas for edge sync, or native vector search. If you're starting fresh and think you might eventually need any of those features, starting with LibSQL costs nothing since the SQL is identical.
Pick DuckDB when you need to run analytical queries, not transactional ones. Aggregations, rollups, pivots, scanning large datasets, querying CSV or Parquet files directly. Do not pick DuckDB for CRUD workloads.
Pick PostgreSQL if you need concurrent writers, complex transactions, a rich extension ecosystem, or any of the hundreds of things a full database server provides. Embedded databases are great when they fit, but they're not a replacement for a real server when your use case demands one. SpinDB and Layerbase Cloud both support PostgreSQL too.
Further Reading
- Getting Started with SQLite
- Getting Started with LibSQL
- Getting Started with DuckDB
- SQLite vs LibSQL
To manage your local instances:
spindb stop sqlite1 # Stop SQLite
spindb stop libsql1 # Stop LibSQL
spindb stop duck1 # Stop DuckDB
spindb start sqlite1 # Start SQLite
spindb list # See all instancesSpinDB handles 20+ database engines, so you can run SQLite, LibSQL, DuckDB, PostgreSQL, Redis, and more side by side from one CLI. Layerbase Desktop wraps the same functionality in a macOS GUI if you prefer that.