Skip to main content
Version: Next

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).

Driverstore.driver valuePersistentUse case
SQLitesqlite (default)YesZero-config local/single-node use
PostgreSQLpostgresYesMulti-node production
MySQLmysqlYesMulti-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:

StoreWhat 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.