Skip to content
Docs
API Reference

Analytics API

Boltstore tracks every database query (SELECT, INSERT, UPDATE, DELETE) and periodic storage snapshots. All analytics data is stored in a dedicated _analytics.db database alongside your data.

All analytics endpoints require an admin session. Per-database API keys are not accepted.

Data Model

_query_log

Inserted for every query or record CRUD operation. Rows older than 30 days are pruned automatically (at most once per hour).

ColumnTypeDescription
idINTEGERAuto-increment primary key
databaseTEXTTarget database name
table_nameTEXTTarget table, or NULL for raw SQL queries
operationTEXTselect, insert, update, or delete
duration_msREALQuery execution time in milliseconds
row_countINTEGERRows returned (SELECT) or affected (write)
statusTEXTok or error
error_msgTEXTError message if status = 'error'
timestampTEXTISO-8601 timestamp
database_idTEXTStable UUID referencing the database (added in v1.0.2). Historical events still show the database name for backward compatibility.

Note on table_name: Raw SQL queries via POST /api/databases/:db/query always log table_name = NULL since a single SQL statement can reference multiple tables, perform joins, or run DDL/PRAGMA. Only record CRUD operations (POST /api/databases/:db/tables/:table/records, etc.) populate table_name from the URL parameter.

_storage_snapshots

Inserted every 5 minutes for every database.

ColumnTypeDescription
idINTEGERAuto-increment primary key
databaseTEXTTarget database name
size_bytesINTEGERDatabase file size in bytes, recorded periodically by the analytics snapshot timer
table_countINTEGERNumber of user tables (excluding internal _* tables)
timestampTEXTISO-8601 timestamp
database_idTEXTStable UUID referencing the database (added in v1.0.2)

_daily_stats

Daily aggregated query counts used for fast dashboard loading.

ColumnTypeDescription
dateTEXTDate string, 2026-06-30
database_nameTEXTTarget database name
database_idTEXTStable UUID referencing the database
operationTEXTselect, insert, update, delete, or raw_query
countINTEGERQuery count for that operation on that day
rows_readINTEGERRows returned for SELECT operations
rows_writtenINTEGERRows returned + rows affected for write operations

Note on the composite unique constraint: UNIQUE (database_id, date, operation) prevents race conditions with concurrent flush requests.

_daily_queries

Daily aggregated query patterns used for analytics dashboards.

ColumnTypeDescription
dateTEXTDate string (2026-06-30)
database_nameTEXTTarget database name
database_idTEXTStable UUID referencing the database
sql_textTEXTSQL query text, or NULL for CRUD (e.g., SELECT * FROM users WHERE active = ?)
countINTEGERQuery count for that text pattern on that day
row_countINTEGERRows returned across all matching queries

Note on the composite unique constraint: UNIQUE (database_id, date, sql_text) prevents race conditions. For CRUD queries, the sql_text pattern is derived from the constructed template.

Analytics Query Parameters

All analytics endpoints accept a ?range= query parameter that controls the time window and grouping:

ValueWindowGrouping
24h (default)Last 24 hoursBy hour
7dLast 7 daysBy day
30dLast 30 daysBy ISO week

Endpoints

Overview

GET/api/analytics/overview?range=24h

Aggregated stats across all databases.

{
"data": {
"databases": 3,
"queries": 15234,
"writes": 2341,
"avgLatencyMs": 2.3,
"errorCount": 12,
"rows_read": 48293,
"rows_written": 3510,
"totalStorageBytes": 52428800
}
FieldDescription
databasesTotal number of databases on the server
queriesTotal query count (SELECT + writes + errors) in the time window
writesCount of INSERT/UPDATE/DELETE operations
avgLatencyMsAverage query latency across all databases
errorCountNumber of failed queries
rows_readSum of row_count for SELECT operations (rows returned)
rows_writtenSum of row_count for INSERT/UPDATE/DELETE operations (rows affected)
totalStorageBytesCurrent total storage across all databases

Per-Database Analytics

GET/api/analytics/:database/overview?range=24h

Per-database stats, plus the top 10 tables by call count.

Predefined response structure:

json
{
  "data": {
    "database": "my-app",
    "queries": 8234,
    "writes": 1203,
    "rows_read": 48293,
    "avgLatencyMs": 1.8,
    "errorCount": 5,
    "storageBytes": 16777216,
    "tableCount": 4,
    "topTables": [
      {
        "sql_text": "SELECT * FROM \"users\" WHERE active = ?",
        "calls": 4210,
        "avg_ms": 0.5,
        "writes": 202,
        "total_rows": 14212
      }
    ]
  }
}
FieldDescription
queriesTotal query count (SELECT + writes + errors) in the time window
writesCount of INSERT/UPDATE/DELETE operations
rows_readSum of row_count across all operations (rows returned + rows affected)
avgLatencyMsAverage query latency
errorCountNumber of failed queries
storageBytesCurrent storage size of the database
tableCountNumber of user tables
topTablesTop 10 query patterns by call count with metadata

Query Log

GET/api/analytics/:database/queries?range=24h&limit=20&offset=0

Paginated query log for a specific database. Returns the raw log entries sorted by most recent first.

Query Parameters:

  • limit: Max rows (max 100, default 20)
  • offset: Pagination offset (default 0)

Response includes meta.total for the total matching entry count in the time window.

Top Queries (All Databases)

GET/api/analytics/top-queries?range=24h

Top 1 query pattern per database (most-called query), sorted by call count descending. Grouped by COALESCE(sql_text, operation) to capture raw SQL and CRUD operations together.

Response structure:

json
{
  "data": [
    {
      "database": "my-app",
      "sql_text": "SELECT * FROM \"users\" WHERE active = ?",
      "calls": 4008,
      "avg_ms": 0.5,
      "total_rows": 14000
    }
  ]
}

Response structure details:

  • data: Array of query objects
  • Each object contains:
    • database: Database name
    • sql_text: The SQL query text (or operation name for raw SQL)
    • calls: Number of calls (query executions)
    • avg_ms: Average execution time in milliseconds
    • total_rows: Total rows returned or affected

Note: Raw SQL queries (no table context) appear as their SQL text; record CRUD operations show their constructed SQL template.

Errors

GET/api/analytics/errors?limit=20

Recent failed queries, sorted by most recent first.

Volume (Time-Series)

GET/api/analytics/volume?range=24h

Time-series data suitable for chart rendering. Returns evenly-spaced slots (hours for 24h, days for 7d, ISO weeks for 30d) with query counts and errors per slot.

Response structure:

json
{
  "data": {
    "slots": ["00", "01", "02", "03"],
    "counts": [120, 85, 42, 18],
    "errors": [0, 1, 0, 0],
    "rows_read": [480, 340, 168, 72],
    "rows_written": [24, 10, 6, 2],
    "max": 120,
    "max_read": 480,
    "max_written": 24
  }
}

Response fields:

  • data: Object containing analytics data
  • data.slots: Time slot labels (hour "00""23", date "2026-01-01", or ISO week "2026-01")
  • data.counts: Query count per slot, in the same order
  • data.errors: Error count per slot
  • data.rows_read: Total rows read (SELECT) per slot
  • data.rows_written: Total rows written (INSERT/UPDATE/DELETE) per slot
  • data.max: Maximum query count across all slots (for chart Y-axis scaling)
  • data.max_read: Maximum rows_read across all slots
  • data.max_written: Maximum rows_written across all slots

Implementation details:

  • Generated from 5-second aggregation window
  • Aggregates data inserted into _daily_stats, _daily_queries, and _query_log tables
  • Prefers analytics data (reducing load on server operations)

Storage History

GET/api/analytics/:database/size

Last 100 storage snapshots for a database, sorted by most recent first.

How Data Is Recorded

Analytics events originate from two sources:

SourceEndpointoperationtable_namerow_count
Raw SQLPOST /api/databases/:db/queryselect or updateNULLRows returned or affected
Create recordPOST /api/databases/:db/tables/:table/recordsinsertFrom URLRecords created
List recordsGET /api/databases/:db/tables/:table/recordsselectFrom URLRows returned
Get recordGET /api/databases/:db/tables/:table/records/:idselectFrom URL1
Update recordPATCH /api/databases/:db/tables/:table/records/:idupdateFrom URL1
Delete recordDELETE /api/databases/:db/tables/:table/records/:iddeleteFrom URL1

Dashboard browsing counts too. Every action in the dashboard's Data tab — listing records, paginating, sorting, filtering, or clicking a record — hits the record CRUD endpoints and generates analytics entries. They appear grouped by their SQL template in the Top Queries views.

Events are buffered in memory and flushed to _analytics.db every 5 seconds, or when the buffer reaches 100 events — whichever comes first. On flush failure the batch is re-queued.

Storage snapshots are taken every 5 minutes by reading PRAGMA page_count × PRAGMA page_size for each database and counting user tables.

Retention

  • Query log entries older than 30 days are pruned automatically. Pruning runs at most once per hour and is attempted as part of the regular flush cycle.
  • Storage snapshots are not pruned.

Dashboard

The analytics data powers the Boltstore Dashboard:

  • Overview — metric cards (databases, storage, queries, latency) + query volume chart
  • Analytics — full page with charts, per-database stats table, top queries across all databases, and error log
  • Databases — per-database rows read, rows written, and total queries for the last 24 hours
  • Database Detail > Top Queries — top tables by call count with row totals and latency

Server-Side Caching & Parallelization

The admin dashboard loads five analytics endpoints in parallel:

  • Overview (/api/analytics/overview)
  • Per-database overview (/api/analytics/:database/overview)
  • Volume chart (/api/analytics/volume)
  • Top queries (/api/analytics/top-queries)
  • Errors (/api/analytics/errors)

Instead of sequential await calls, all endpoints fire simultaneously via Promise.allSettled(), reducing load time by ~2-3x.

Additionally, the overview, databases, and volume endpoints use a server-side cache with a 60-second TTL. Subsequent requests within that window return cached Response objects directly, reducing SQLite query load to near zero.

Daily Aggregation (Pre-computed Tables)

To power fast dashboard loading, analytics maintains two daily aggregation tables that are updated during the existing 5-second flush cycle:

_daily_stats

Daily query counts per operation type. Used by the Databases dashboard panel to show per-database totals.

ColumnDescription
dateDate string (2026-06-30)
database_nameTarget database name
database_idStable UUID referencing the database (v3 migration)
operationselect, insert, update, delete, or raw_query
countQuery count for that operation on that day
rows_readRows returned for SELECT operations

_daily_queries

Daily query pattern aggregates. Used by the Top Queries panel.

ColumnDescription
dateDate string (2026-06-30)
database_nameTarget database name
database_idStable UUID referencing the database (v3 migration)
sql_textSQL query text, or NULL for CRUD (e.g., SELECT * FROM users WHERE active = ?)
countQuery count for that text pattern on that day
row_countTotal rows returned across all matching queries

These tables are upserted atomically with the query log flush (default every 5 seconds), staying within ~5 seconds of real-time and eliminating the need for expensive GROUP BY queries on the dashboard.

Snapshot Timer

The storage snapshots are taken every 5 minutes via a scheduled timer:

  • For every database in _databases, the server executes PRAGMA page_count × PRAGMA page_size and counts user tables (CREATE TABLE excluding internal _* tables)
  • Data is written to _storage_snapshots with the database's database_id (v3 migration)
  • No rollback logic — if the write fails, the snapshot timer continues on the next iteration

The analytics endpoints (queries, top-queries, errors) accept optional ?search= query parameters:

  • Query Log: Filters by exact sql_text match
  • Top Queries: Filters by sql_text prefix match
  • Errors: Filters by error_msg prefix match

Case-insensitive prefix matching (SQLite's LIKE :search).

Example: GET /api/analytics/my-app/queries?search=SELECT%20*%20FROM returns all queries starting with SELECT * FROM

Permission Notes

  • Volume endpoint (/api/analytics/volume) was historically incompatible with cross-database preview functionality due to timezone handling issues.
  • Charts and cards APIs do not share the same generation logic, which can lead to counting discrepancies between the two views.
  • The volume endpoint response schema was previously under-documented, contributing to confusion about expected data structure.
  • Analytics data aggregation involves complex timezone considerations across different time windows.