Storage
O2ID's storage layer is built around a single Provider interface. At startup,
the CLI looks up a driver by name in a registry and passes the resulting
provider to the server. Every read and write goes through that provider.
Swapping the backend means registering a different implementation — no
changes to the API or business logic are required.
Choosing a driver
Storage is configured through a TOML config file.
By default o2id serve looks for o2id.toml in the working directory it's
run from. Point it elsewhere with --config:
o2id serve --config /etc/o2id/config.toml
All three drivers ship as generic RDBMS drivers: they share one query engine
(internal/store/rdbms) and differ only in a small set of dialect-specific
facts (placeholder syntax, key column type, row locking, and unique-constraint
detection).
| Driver | store.driver value | Persistent | Use case |
|---|---|---|---|
| SQLite | sqlite (default) | Yes | Zero-config local/single-node use |
| PostgreSQL | postgres | Yes | Multi-node production |
| MySQL | mysql | Yes | Multi-node production |
SQLite (default)
[store]
driver = "sqlite"
dsn = "/var/lib/o2id/o2id.db"
The SQLite driver uses WAL journal mode, is restricted to a single connection (SQLite only supports one writer at a time), and runs schema migrations on every startup, so it is safe to restart the server against an existing database.
PostgreSQL
[store]
driver = "postgres"
dsn = "postgres://user:pass@host:5432/dbname?sslmode=disable"
MySQL
[store]
driver = "mysql"
dsn = "user:pass@tcp(host:3306)/dbname"
Provider interface
The store.Provider interface lives in internal/store/store.go and exposes
seven stores:
| Store | What it holds |
|---|---|
Users() | User identities |
Applications() | Registered OAuth 2.0 clients |
AccessTokens() | Issued Bearer access tokens |
RefreshTokens() | Single-use refresh tokens |
Sessions() | Browser session cookies |
AuthorizationCodes() | Short-lived OAuth authorization codes |
LoginTransactions() | Pending login flow state |
Users() and Applications() return the domain repository interfaces defined
alongside their respective services (user.Repository,
application.Repository). The remaining five return store-level interfaces
defined in internal/store/store.go.
Drivers are resolved by name from a registry at startup — adding or
implementing a new one (a different RDBMS, or a fully custom non-relational
provider) is a codebase-contribution task, not a deployment one, so it's
covered in CONTRIBUTING.md
instead of here.
Encryption at rest
O2ID does not encrypt its storage layer itself — securing the database at rest (a stolen disk, a leaked volume snapshot, an unencrypted backup) is a deployment concern, not something the application does on your behalf. Whoever runs O2ID is responsible for configuring their storage so the entire database is encrypted, not just specific fields:
- SQLite: put the database file on an encrypted volume or filesystem (e.g. an encrypted disk/EBS volume, LUKS, FileVault).
- PostgreSQL/MySQL: enable your managed database provider's encryption-at-rest option, or encrypt the underlying disk/volume yourself.
This is separate from, and doesn't replace, the field-level encryption O2ID's application code applies specifically to connection secrets (see Managing Connections). The two defend against different threats: whole-database encryption at rest protects data on a stolen or copied disk, while field-level encryption protects against anyone able to query the live, running database — a SQL-injection read, an over-privileged read replica, a support engineer pulling a row to debug a ticket — from seeing third-party credentials in plaintext. Neither is a substitute for the other; use both.
See Application settings for how long access tokens, refresh tokens, sessions, and the other short-lived stores above are kept before expiring, and how to change it — that's an auth/policy concern, not a storage-driver one, so it isn't covered here.