Glass Box Terrarium, explained from the inside

Last updated · Latest source on GitHub ↗

Nuxt ContentLesson

Content database

Content written as Markdown and YAML files has to become something a page can query quickly. Nuxt Content does that work once, when the site is built, and ships the result. Knowing where that result lives, on the server and sometimes in the browser, explains what a page costs to load and how it can fail.

Know how Nuxt Content's database works already? Skip to In Terrarium.

Outside the box · General

The concept

Nuxt Content moves content through three places.

At build time, it parses every collection's files, creates a table per collection from its schema, inserts the parsed rows and saves the result as a dump: the SQL that rebuilds that table.

On the server, nothing is parsed while the site runs. The first query for a collection after the server starts loads that collection's dump into the server's SQLite database. A checksum stored with each table tells Nuxt Content whether the table already matches the build, so a restart with unchanged content doesn't import it again. Which SQLite library runs it is a setting, experimental.sqliteConnector; native uses the module built into Node 22.5 and later (unflagged from 22.13), node:sqlite, so no native package has to be compiled.

In the browser, a query that runs after the page has loaded, such as one during client-side navigation, doesn't call the server. The first one downloads an SQLite build compiled to WebAssembly and the collection's dump, builds a local database, and answers that query and later ones from it.

Full referenceNuxt Content docs: Database

Looking inside

In Terrarium

Terrarium uses Node's own SQLite:

nuxt.config.ts ↗30–33as of
30  content: {
31    // Native node:sqlite (unflagged from Node 22.13) — no better-sqlite3 native build in CI.
32    experimental: { sqliteConnector: 'native' },
33  },

The live server runs on a Node 22 image, pinned in deploy/Dockerfile. That container builds main itself and serves the build's output, dumps included, so content changes only when a new build goes live.

The server side runs inside (Nitro server engine) whenever a page renders there. After the server starts, the first query for each Collection compares its table's checksum with the build and restores the table from its dump if they differ.

The browser side is mostly avoided: a link inside the app reads a payload the server rendered, so a typical navigation runs no query in the browser. When the browser database still loads is in Data fetching.

When the browser database fails to load, the page doesn't go blank. Space pages mount app/components/ContentLoadErrorDialog.vue with their useAsyncData status, and it hands a failure to app/composables/contentLoadRecovery.ts: a failed download reloads the page once; a second failure, or any other kind, shows a dialog that offers a reload.

Why it's built this way

  • Reload, not retry. Pages mostly skip the browser database, but when it fails to load, Nuxt Content remembers the failure and a section stays empty. A retry patch was dropped as it needed redoing every upgrade; a full reload always recovers. The cost: a flash and lost unsaved in-page state. ADR-0019 (docs/adr/0019-content-load-error-recovery.md).
  • Node's own SQLite. The native connector uses the SQLite (a one-file database) built into Node, so CI never compiles a separate package. The cost: the site relies on a feature Node still marks experimental, and every machine that builds or serves it, the live server too (Continuous deployment), needs a Node that has it. docs/research/nuxt-content-review-grounding.md.

What to watch for

  • The browser loads a whole Collection. Its first query in the browser downloads the WebAssembly runtime and that Collection's entire dump, however few rows it wants. Check the dump's size before you send a page's queries to the browser.
  • The browser keeps dumps in localStorage. In a production build, Nuxt Content caches each downloaded dump there under a content_ key, on the one origin every Tenant shares. A Tenant that calls localStorage.clear() throws them away; .claude/rules/tenant-layers.md says to remove only your own keys.
  • Editing a file on the server changes nothing. The live site reads the dumps of its last build. Content changes reach visitors through main and a new build, never through the running server.

Try it in your project

Build your site and start it with node .output/server/index.mjs. Open the browser's network tab, load one page, then follow a link to another page that queries content. Look for a sql_dump.txt request under /__nuxt_content/ and a .wasm file: that is the browser database loading, and their sizes are what the first query in the browser costs.

Where next

Walkthroughs through this Lesson

Last updated · Latest source on GitHub ↗

Something went wrong

This content couldn’t be loaded. Reloading the page usually fixes it.

Technical details
route: /t/glassbox/guide/nuxt-content/content-database