Content v3 keeps its pages in SQL and can talk to five databases. duxt has an
opinion about none of them: the adapter is your deployment's decision, Content
already owns the surface, and the layer's only job is not to get in the way.

This page says how far that has actually been checked, rather than implying it.

## Two databases, not one

Almost every surprise in this area comes from reading "the database" as one
thing. Content keeps two, and they have different lifetimes and different types.

| | `content._localDatabase` | `content.database` |
| :--- | :--- | :--- |
| What it holds | The parse cache — one row per file, as parsed | The collections the site queries |
| When it exists | During a build, in `.data/content/` | On the deployed server |
| Which adapters | `sqlite` or `d1`, and nothing else | Any of the five below |
| Who reads it | `duxt report`, the build validator, the redirect map | `queryCollection()`, on a request |

The consequence is the useful part: **choosing an adapter changes the deployed
database and nothing else.** A site deployed on PostgreSQL still builds through
a local SQLite parse cache, because Content's own type for that cache admits
nothing else.

::callout{type="info" title="Where the cache lives"}
`.data/content/contents.sqlite`, in the directory you build in. It is a cache —
deleting it costs one build, never any content.
::

## The adapters

`content.database.type` in your own `nuxt.config.ts`. duxt sets it for no site;
leaving it unset gives you Content's default.

| Adapter | Needs installed | duxt |
| :------ | :-------------- | :--- |
| `sqlite` (default) | Nothing on Node 24 — read through the built-in `node:sqlite` | Verified in CI on every commit |
| `d1` | Nothing; a Cloudflare binding | Verified — this site runs on it |
| `libsql` | `@libsql/client` | Verified in CI |
| `postgresql` | `pg` | Verified in CI |
| `pglite` | `@electric-sql/pglite` | **Untested** |

"Verified in CI" means a build of this repository's own site against that
adapter, followed by a probe of every endpoint that reads the database at
runtime and a run of `duxt report`. The workflow is
`.github/workflows/adapters.yml`; it runs on each pull request, so the rows
above cannot quietly stop being true.

::callout{type="warning" title="PGlite is untested, not broken"}
Nothing is known to be wrong with it. Nothing is known to be right with it
either — it has had no dedicated run, so it is marked untested rather than
implied to work. If you deploy on it, the layer would like to hear how it went.
::

### No native SQLite driver is installed

Content's own default is `better-sqlite3`, a native addon that compiles through
node-gyp. duxt sets `content.experimental.sqliteConnector` to `'native'`, which
uses Node 24's built-in `node:sqlite` instead and needs no package at all —
which is why the layer's install pulls no compiler and why the build wants
Node 24.

You inherit that setting by extending the layer, and you should never see a
deprecation warning for it. The option Content deprecated is the older
`experimental.nativeSqlite`; duxt has a test that reads Content's own
declarations and fails the layer's checks if anything it sets under
`content.experimental` carries an `@deprecated` tag, so the next rename lands
in this repository rather than in your build log.

## What runs during the build, and never on the server

Three parts of the layer read the filesystem outright. All three run inside a
build, which is what makes a Worker — or any target with no disk — possible at
all.

| | What it does | When |
| :--- | :--- | :--- |
| `content-cache.ts` | Opens the parse cache with `node:sqlite`, and puts it into WAL mode | Build |
| `modules/git-meta.ts` | Runs `git log` over the checkout for "last updated" and contributors | Build |
| `duxt report` | Reads the same parse cache from the command line | Beside the build |

None of them is reachable from a request, and none of them is affected by your
adapter: they read the local SQLite cache described above, which exists whatever
`content.database` says.

## The rule

> The layer never reads the content database at runtime through anything but
> `queryCollection()`.

That is the whole of duxt's adapter compatibility. The moment a server route
opened the database itself, the layer would start assuming a filesystem and a
SQLite dialect — and it would fail in your deploy rather than in this
repository's tests.

It is enforced rather than promised: `tests/content-database.test.ts` fails if
any file under `server/` or `app/` imports a database driver, and keeps the list
of build-time files that may open one closed, so a new one has to be justified
in writing before it lands.

### What reads the database on a request

Everything else a duxt site serves is a file by the time it is served. These are
not, and these are what the adapter matrix probes:

- **`…/page.md`** — the Markdown source behind "View as Markdown" and the
  hand-off links.
- **`/mcp`** — every tool, since each answers out of a collection.
- **`/llms.txt` and `/llms-full.txt`** — both walk every collection.
- **`/rss.xml`**, once you configure it.

A page is on that list too on a server-rendered deployment, and off it on a
prerendered one. See [Deploy a site](/guides/deploy-a-site) for which of those
you are running.