---
title: "Database"
description: "The metadata (meta) DB stores what can't be derived from files: SQLite by default, Postgres via META_DB_URL for a team."
---

# Database

In Notarium, notes are files, and the search index and graph are rebuilt from them. But part of the state **can't be derived** from files: a separate metadata (meta) DB holds it. By default this is a SQLite file under the data root — `<DATA_DIR>/meta.db`; the `META_DB_URL` variable is only needed to point to an external Postgres.

## What the metadata DB stores

| Data | Why it isn't from files |
|---|---|
| Note identifiers | The `notarium-id` ↔ path registry: it survives move/rename. |
| Version history | The revision log (version snapshots, where an edit came from) is kept by the app itself, not by git. |
| Users and access | Accounts, roles, membership, tokens. |
| Rename history | Aliases for the old slugs of spaces and projects — so previous addresses keep resolving. |

None of this can be reconstructed from `.md` files alone — which is why the metadata DB must always be included in your [backup](/docs/self-hosting/backup/). There's a registry of spaces in this DB too, but it's derived: a space's identity lives in a marker file at its root and is recovered by a scan ([File-first](/docs/concepts/file-first/)). The engine's derived indexes (`<DATA_DIR>/engine`) are simply rebuilt from files on the next start if lost.

For more on versions and where they come from, see [Concepts: versioning](/docs/concepts/versioning/).

## Schema and migrations

The application owns the metadata DB schema: migrations are applied **at startup**, and there's no separate command for them. The DB itself carries a ledger of the migrations applied to it — that's how a build knows what it's dealing with.

Startup accepts exactly three states:

- **an empty DB** — the base schema is laid down, and the ledger entry is written in the same transaction;
- **a DB whose ledger is an exact prefix of the expected one** — versions, names, and checksums are verified, then the missing tail is applied;
- **a non-empty DB with no ledger** — startup **fails closed**, before any schema change or application query.

That last one is deliberate. A build doesn't guess the version of a database it doesn't recognize, and it won't stamp the ledger on its own: that would corrupt data silently. If such a database is what you have on hand (an instance older than the base schema, say), first upgrade and check it with the standard procedure for its own version, and only then move it across this boundary.

> [!important] Rolling back means restoring from a backup
> Notarium's mechanism for rolling data back is a verified archive, not reverse SQL migrations. Take a backup and verify it **before** you upgrade: [Backup and restore](/docs/self-hosting/backup/).

## SQLite (default)

Zero-config: by default the meta DB is `sqlite:<DATA_DIR>/meta.db`, that is, a file on the `/data` volume. No separate service is needed, and there's nothing to configure. This is enough for a personal instance and a small team on a **single** container.

## Postgres (for a team and shared state)

To move state outside the container — for shared storage, fault tolerance, or maintenance — point to Postgres:

```bash
# .env
META_DB_URL=postgres://user:pass@db:5432/notarium
```

You need Postgres when state must live independently of the container's lifecycle. The notes themselves still remain files under `<DATA_DIR>/spaces` — only what can't be derived from them goes into the DB.

> [!important] `password` mode relies on the metadata DB
> In `AUTH_MODE=password` mode, the metadata DB is needed for accounts and tokens — and it's already there by default (SQLite under the data root). You don't need to set `META_DB_URL` separately; you specify it only to move to Postgres. Only `AUTH_MODE=none` works without a metadata DB. See [Authentication](/docs/self-hosting/authentication/).

> [!note] Single instance
> Moving state into Postgres doesn't by itself enable horizontal scaling: Notarium runs as a single instance, and multiple instances behind a load balancer aren't supported (see [Production](/docs/self-hosting/production/)).
