Storage Adapters
Snapline Hub uses a pluggable ReportStore interface. SQLite is the default for demos and local use; PostgreSQL and custom adapters are supported with minimal configuration.
Overview
All report persistence goes through a single interface (ReportStore). The Express API, React UI, and Snapline push clients are storage-agnostic — they never import database drivers directly.
Shared logic (validation, filter parsing, SQL WHERE builders) lives in server/storage/ so adapters stay thin.
Built-in adapters
| Driver | Use case | Config |
|---|---|---|
sqlite (default) |
Local dev, demos, single-node deploy | SNAPLINE_HUB_DB file path |
postgres |
Team servers, managed DB, HA | DATABASE_URL connection string |
custom |
MySQL, DynamoDB, S3, corporate DB | SNAPLINE_HUB_STORAGE_MODULE |
Configuration
| Variable | Default | Description |
|---|---|---|
SNAPLINE_HUB_STORAGE |
sqlite |
Storage driver: sqlite, postgres, or custom |
SNAPLINE_HUB_DB |
./data/snapline-hub.db |
SQLite file path (sqlite driver) |
DATABASE_URL |
(unset) | PostgreSQL URL, e.g. postgres://user:pass@host:5432/snapline_hub |
SNAPLINE_HUB_STORAGE_MODULE |
(unset) | Path to a module exporting createReportStore(config) |
SQLite (default — demos & local)
npm run dev
# or
SNAPLINE_HUB_STORAGE=sqlite SNAPLINE_HUB_DB=./data/hub.db npm start
PostgreSQL
# Create database
createdb snapline_hub
# Run Hub
SNAPLINE_HUB_STORAGE=postgres \
DATABASE_URL=postgres://localhost:5432/snapline_hub \
npm start
Schema and indexes are created automatically on first connect. The table layout mirrors SQLite for portability.
Custom adapter
Implement ReportStore and point Hub at your module. Start from the example:
cp server/storage/custom-adapter.example.ts my-company-adapter.mjs
# edit my-company-adapter.mjs — implement createReportStore()
SNAPLINE_HUB_STORAGE=custom \
SNAPLINE_HUB_STORAGE_MODULE=./my-company-adapter.mjs \
npm start
Your module must export:
export function createReportStore(config) {
return {
driver: 'my-db',
insertReport(report, meta) { /* ... */ },
listReports(filters) { /* ... */ },
countReports(filters) { /* ... */ },
aggregateStats(filters) { /* ... */ },
getReport(id) { /* ... */ },
deleteReport(id) { /* ... */ },
getFacets(filters) { /* ... */ },
close() { /* ... */ },
};
}
server/storage/validation.ts and server/storage/query.ts in SQL-based adapters. Methods may return values or Promises — the API layer handles both.
ReportStore interface
| Method | Purpose |
|---|---|
driver | Short name returned in GET /api/health as storage |
insertReport(report, meta?) | Persist a TestRunReport; return new UUID |
listReports(filters?) | Paginated summaries, newest first |
countReports(filters?) | Total matching rows (for pagination) |
aggregateStats(filters?) | SUM of suite counts across all matching runs |
getReport(id) | Full stored report including report JSON |
deleteReport(id) | Remove by ID; return true if deleted |
getFacets(filters?) | Project, tag, and framework counts for filter UI |
close() | Release connections on shutdown |
Shared schema
SQL adapters use a single reports table defined in server/storage/schema.ts:
| Column | Description |
|---|---|
id | UUID primary key |
generated_at | ISO timestamp from report |
framework, label, project, tags | Metadata for filtering |
total, passed, failed | Suite-level summary (denormalized) |
duration_ms, environment | Optional run context |
report_json | Full TestRunReport payload |
created_at | Hub ingest timestamp |
Filter/query helpers in buildFilterClause() use @named placeholders — SQLite uses them natively; the PostgreSQL adapter converts to $1, $2.