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.

┌─────────────────────────────────────────┐ │ Express API (server/routes.ts) │ └──────────────────┬──────────────────────┘ │ ReportStore ┌───────────┼───────────┐ ▼ ▼ ▼ sqlite-adapter postgres-adapter your-adapter.js (default) (DATABASE_URL) (custom module)

Shared logic (validation, filter parsing, SQL WHERE builders) lives in server/storage/ so adapters stay thin.

Built-in adapters

DriverUse caseConfig
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

VariableDefaultDescription
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() { /* ... */ },
  };
}
Tip: Reuse 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

MethodPurpose
driverShort 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:

ColumnDescription
idUUID primary key
generated_atISO timestamp from report
framework, label, project, tagsMetadata for filtering
total, passed, failedSuite-level summary (denormalized)
duration_ms, environmentOptional run context
report_jsonFull TestRunReport payload
created_atHub ingest timestamp

Filter/query helpers in buildFilterClause() use @named placeholders — SQLite uses them natively; the PostgreSQL adapter converts to $1, $2.