Embedded Databases: SQLite, LibSQL, and DuckDB

SQLiteLibSQLDuckDBDatabases

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

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?)

bash
npm i -g spindb
spindb create sqlite1 -e sqlite --start
spindb url sqlite1
text
postgresql://127.0.0.1:5432/sqlite1

Connect and run a query:

typescript
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

bash
spindb create libsql1 -e libsql --start
spindb url libsql1
text
http://127.0.0.1:8080

LibSQL runs its own HTTP server natively, so you use the @libsql/client package instead of a PG driver:

typescript
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:

bash
spindb create duck1 -e duckdb --start
spindb url duck1
text
postgresql://127.0.0.1:5432/duck1
typescript
import 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

FeatureSQLiteLibSQLDuckDB
Primary workloadOLTP (transactions, CRUD)OLTP (transactions, CRUD)OLAP (analytics, aggregations)
Storage modelRow-orientedRow-orientedColumnar
Network accessNone (in-process only)HTTP/WebSocket server modeNone (in-process only)
ReplicationNot built-inEmbedded replicas from primaryNot built-in
Vector searchRequires sqlite-vec extensionNative F32_BLOB typeNot built-in
File querying (CSV/Parquet)NoNoYes, native
Concurrent writersSingle writerSingle writer (server manages access)Single writer
SpinDB connectionPG proxy (pg driver)HTTP (@libsql/client)PG proxy (pg driver)
Maturity25+ years~3 years~5 years
Best use caseEmbedded apps, CLI tools, local-firstMulti-client access, edge sync, vector searchAnalytics, 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

To manage your local instances:

bash
spindb stop sqlite1     # Stop SQLite
spindb stop libsql1     # Stop LibSQL
spindb stop duck1       # Stop DuckDB
spindb start sqlite1    # Start SQLite
spindb list             # See all instances

SpinDB 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.

Something not working?

Embedded Databases: SQLite, LibSQL, and DuckDB | Layerbase Blog