Skip to content

Databases ​

What you'll learn

SQLite, Turso, Postgres and PGlite: how to connect, choose and tune each.

Before this page: Getting started.

SQLite ​

bash
npm install @easy-cms/db-sqlite
bash
pnpm add @easy-cms/db-sqlite
bash
yarn add @easy-cms/db-sqlite
bash
bun add @easy-cms/db-sqlite
ts
import { sqlite } from '@easy-cms/db-sqlite'

db: sqlite({ url: 'file:./cms.db' }) // relative to the project root
db: sqlite({ url: 'libsql://my-db.turso.io', authToken: process.env.TURSO_TOKEN })

Uses libSQL. In-memory databases (:memory:) are not supported.

SQLite has one writer at a time. Writes from one process wait their turn in a queue; when another process writes to the same file (a second server, easy-cms commands), a write waits for it up to busyTimeout (default 10000 ms), during which that process pauses. Many busy processes writing to one file are better served by Postgres.

ts
db: sqlite({ url: 'file:./cms.db', busyTimeout: 5_000 })

Postgres ​

bash
npm install @easy-cms/db-postgres
bash
pnpm add @easy-cms/db-postgres
bash
yarn add @easy-cms/db-postgres
bash
bun add @easy-cms/db-postgres
ts
import { postgres } from '@easy-cms/db-postgres'

db: postgres({ url: process.env.DATABASE_URL }) // a server, via postgres.js
db: postgres({ pglite: '.pglite' }) // PGlite: Postgres in WebAssembly, needs @electric-sql/pglite

PGlite is handy locally and in tests: real Postgres with nothing to install. A common setup:

ts
db: process.env.DATABASE_URL ? postgres({ url: process.env.DATABASE_URL }) : postgres({ pglite: '.pglite' }),

Common options ​

OptionDefault
tablePrefixecms_Prefix of every table Easy CMS creates
migrationDireasy-cms/migrationsWhere migration files live

Sharing a database with your app ​

Easy CMS creates and changes only tables with its prefix, so it can use your app's database. Your own tables are never touched, in development or by migrations.

Differences to know ​

  • Text sorting follows the database's collation: SQLite and PGlite sort case-sensitively, most Postgres servers don't.
  • Migration files are made for one database; files created for SQLite are refused on Postgres.

Performance ​

Measured with packages/integration/load.ts on Postgres 17 (Docker, a 10-core laptop): 100,000 posts with two locales, drafts and versions, relationships, hasMany tags and blocks; 20 concurrent REST requests, a pool of 10 connections.

RequestRequests/sp50p95
By id, relationships populated (depth=2)2,7508 ms10 ms
By slug (where[slug][equals])2,7708 ms10 ms
First page of a list1,06018 ms23 ms
Filter by relationship, sort by date56034 ms55 ms
Admin list with drafts55035 ms49 ms
Save a draft (with a version)50038 ms52 ms
Random page among 8,000150129 ms189 ms
Filter by hasMany value, sort by number115159 ms258 ms
like search100198 ms262 ms
Inside blocks (layout.blockType)83237 ms304 ms

What that means for your site:

  • Pages and lookups are fast. Frontends read by id or slug, and list the first pages.
  • Deep pages cost more the further in they are: the database skips the rows before them. Paginate archives by date (where[publishedAt][lt]=…) rather than to page 5,000.
  • like reads every row. For site search, use a search service (Meilisearch, Algolia, Typesense) fed by webhooks, or add a pg_trgm index on Postgres yourself.
  • Queries inside blocks read the JSON of every document. On Postgres, equals and in use JSON containment and are about six times faster than other operators; keep them for filters, not for every page view.
  • Cache public pages (a CDN, or your framework's cache) and revalidate them from webhooks: most traffic then never reaches the CMS.
  • More concurrent requests than connections wait for a connection; raise max on postgres() if the database has room.

Next steps ​

Released under the MIT License.