@veridot/databases
v3.0.0
Published
SQL-backed implementations for Veridot — metadata broker, refresh-token store and revocation store on PostgreSQL, MySQL, MariaDB and SQLite.
Maintainers
Readme
@veridot/databases
SQL backends for Veridot — metadata broker and persistent stores.
This package ships three SQL-backed implementations of Veridot's pluggable contracts:
| Class | Implements | Use it when… |
| -------------------------------- | ----------------------- | -------------------------------------------------------------- |
| DatabaseMetadataBroker | MetadataBroker | You don't want to operate Kafka — public keys live in SQL. |
| DatabaseRefreshTokenStore | RefreshTokenStore | You want refresh tokens persisted across replicas. |
| DatabaseRevocationStore | RevocationStore | You want revocations to survive restarts and span replicas. |
Supported flavors: PostgreSQL, MySQL, MariaDB, SQLite.
Installation
pnpm add @veridot/core @veridot/databases
# Pick the driver(s) you need:
pnpm add pg # PostgreSQL
pnpm add mysql2 # MySQL / MariaDB
pnpm add sqlite3 sqlite # SQLite (tests / single-node only)Quick start (with the Veridot facade)
import { Pool } from 'pg';
import { Veridot } from '@veridot/core';
import {
DatabaseMetadataBroker,
DatabaseRefreshTokenStore,
DatabaseRevocationStore,
} from '@veridot/databases';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const broker = await DatabaseMetadataBroker.of({
type: 'postgresql',
host: 'db',
port: 5432,
database: 'veridot',
user: 'veridot',
password: process.env.DB_PASSWORD!,
});
const refreshStore = new DatabaseRefreshTokenStore({
type: 'postgresql',
client: pool,
autoCreateSchema: true,
});
const revocationStore = new DatabaseRevocationStore({
type: 'postgresql',
client: pool,
autoCreateSchema: true,
});
const veridot = await Veridot.create({
metadataBroker: broker,
refreshTokenStore: refreshStore,
revocationStore: revocationStore,
salt: process.env.VDOT_SALT!,
hashPepper: process.env.VDOT_PEPPER!,
expectedIssuer: 'https://auth.example.com',
expectedAudience: 'billing-api',
});DatabaseMetadataBroker.of(options)
Distributes Veridot's public keys via a single SQL table.
| Option | Type | Required | Default |
| ------------ | ------------------------------------------ | -------- | -------------------- |
| type | 'postgresql' \| 'mysql' \| 'mariadb' \| 'sqlite' | yes | — |
| host | string | no | 'localhost' |
| port | number | no | 5432 / 3306 |
| database | string | yes | — |
| user | string | conditional | (required except SQLite) |
| password | string | conditional | (required except SQLite) |
| tableName | string | no | 'veridot_metadata' |
| poolSize | number | no | driver default |
| logger | Logger (@veridot/core) | no | ConsoleLogger |
The broker creates its table automatically on first use; no manual migration needed.
const broker = await DatabaseMetadataBroker.of({
type: 'postgresql',
host: 'localhost',
port: 5432,
database: 'veridot',
user: 'postgres',
password: 'password',
poolSize: 20,
});Compatible with the Kafka broker
You can mix and match: one service uses KafkaMetadataBroker, another uses
DatabaseMetadataBroker, as long as they share the same table or topic
schema. Tokens signed on one side verify on the other.
DatabaseRefreshTokenStore
Persists hashed refresh tokens with rotation metadata.
const refreshStore = new DatabaseRefreshTokenStore({
type: 'postgresql',
client: pgPool, // any { query: (text, params?) => Promise<{ rows }> }
tableName: 'app_refresh_tokens', // optional, defaults to 'veridot_refresh_tokens'
autoCreateSchema: true, // optional, runs ensureSchema() on first call
});If you prefer to manage migrations yourself, omit autoCreateSchema and create
the table:
CREATE TABLE veridot_refresh_tokens (
token_hash VARCHAR(128) PRIMARY KEY,
family_id VARCHAR(128) NOT NULL,
subject VARCHAR(255) NOT NULL,
payload TEXT NOT NULL,
created_at BIGINT NOT NULL,
expires_at BIGINT NOT NULL,
status VARCHAR(16) NOT NULL,
replaced_by_hash VARCHAR(128),
revoked_at BIGINT
);
CREATE INDEX veridot_refresh_tokens_family_idx ON veridot_refresh_tokens(family_id);
CREATE INDEX veridot_refresh_tokens_expires_idx ON veridot_refresh_tokens(expires_at);| Option | Type | Required | Default |
| ------------------ | ---------------- | -------- | ---------------------------- |
| type | 'postgresql' \| 'mysql' \| 'mariadb' | yes | — |
| client | SqlClient | yes | — |
| tableName | string | no | 'veridot_refresh_tokens' |
| autoCreateSchema | boolean | no | false |
Tokens are stored as SHA-256 hashes; the original value never touches the database. Reuse of an already-rotated token revokes the entire family.
DatabaseRevocationStore
Persists revocations (token IDs, tracking IDs, JWT IDs) with TTL semantics.
const revocationStore = new DatabaseRevocationStore({
type: 'postgresql',
client: pgPool,
tableName: 'app_revocations',
autoCreateSchema: true,
});Schema (auto-created when autoCreateSchema: true):
CREATE TABLE veridot_revocations (
target VARCHAR(255) PRIMARY KEY,
reason VARCHAR(255),
expires_at BIGINT NOT NULL,
created_at BIGINT NOT NULL
);
CREATE INDEX veridot_revocations_expires_idx ON veridot_revocations(expires_at);isRevoked(target) is an indexed primary-key lookup — O(log n) on the table
size.
Custom client adapter
SqlClient is intentionally minimal so any driver works:
import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
// pg.Pool already exposes .query(text, params) → use it directly:
const refreshStore = new DatabaseRefreshTokenStore({
type: 'postgresql',
client: pool,
});
// MySQL2 example:
import mysql from 'mysql2/promise';
const myPool = await mysql.createPool({ uri: process.env.MYSQL_URL });
const refreshStoreMy = new DatabaseRefreshTokenStore({
type: 'mysql',
client: {
query: async (text, params) => {
const [rows] = await myPool.execute(text, params);
return { rows: rows as unknown[] };
},
},
});Cleanup
expires_at is honored on every read — expired records are filtered out — but
nothing prunes them automatically. Schedule a daily cleanup:
DELETE FROM veridot_refresh_tokens WHERE expires_at < EXTRACT(EPOCH FROM NOW())*1000;
DELETE FROM veridot_revocations WHERE expires_at < EXTRACT(EPOCH FROM NOW())*1000;Or use RefreshTokenService.deleteExpiredRefreshTokens() from @veridot/core.
Logger injection
Both the broker and the stores accept a logger matching the
@veridot/core Logger contract:
const broker = await DatabaseMetadataBroker.of({
type: 'postgresql',
database: 'veridot',
user: 'veridot',
password: process.env.DB_PASSWORD!,
logger: pinoLoggerAdapter,
});Graceful shutdown
process.on('SIGTERM', async () => {
await veridot.shutdown();
await broker.disconnect();
await pool.end();
process.exit(0);
});Related packages
@veridot/core— Core facade & interfaces@veridot/kafka— Kafka alternative for the metadata broker@veridot/redis— Redis-backed alternative for the persistent stores@veridot/nestjs— NestJS module
License
MIT — see LICENSE.
