Skip to content
Analytik
Esc
↑↓navigate↵open⌘Jpreview
On this page

Storage

The Storage interface, the D1, Postgres, SQLite and in-memory drivers, the tables, and how migrations work.

Storage is a small interface over SQL. The server builds its queries with a shared set of builders that take the dialect for the few differences (placeholders, upsert, date bucketing, JSON), so every driver runs the same logic.

Interface

interface Storage {
  readonly dialect: 'sqlite' | 'postgres' | 'memory'
  migrate(): Promise<MigrationResult> // applies pending migrations, idempotent
  query<T>(sql: string, params?: unknown[]): Promise<T[]>
  run(sql: string, params?: unknown[]): Promise<{ changes: number }>
  batch(statements: Statement[]): Promise<void> // atomic where the driver allows
  close?(): Promise<void>
}

You rarely call these yourself. They are public so you can write a driver for another database, or run your own reporting query against the same connection.

Drivers

Import Backend Runtime
analytik/storage/d1 Cloudflare D1 (SQLite dialect) Workers
analytik/storage/postgres Postgres, Supabase Anywhere with postgres or pg
analytik/storage/sqlite SQLite file Bun (bun:sqlite), Node 22.19+ (node:sqlite)
analytik/storage/memory In-process SQLite Tests, local demos
import { d1Storage } from 'analytik/storage/d1'
import { memoryStorage } from 'analytik/storage/memory'
import { postgresStorage } from 'analytik/storage/postgres'
import { sqliteStorage } from 'analytik/storage/sqlite'

d1Storage(env.DB)
postgresStorage(postgres(process.env.DATABASE_URL!)) // a postgres.js client
sqliteStorage('./data/analytik.db')
memoryStorage()

postgresStorage() does not take a connection string and does not open connections itself. Pass a client you created and configured: a postgres.js sql instance, or a pg Pool or Client.

import { Pool } from 'pg'
import postgres from 'postgres'

postgresStorage(postgres(process.env.DATABASE_URL!, { prepare: false, max: 1 }))
postgresStorage(new Pool({ connectionString: process.env.DATABASE_URL }))

Calling storage.close() ends the client you passed in.

memoryStorage() is a real SQLite database held in memory, not a mock, so tests that use it run the same SQL as production. D1 batches are atomic. On Postgres and SQLite, batch runs in a transaction.

Tables

The same logical tables exist in every dialect.

Table Holds
sites Id, name, domain, timezone, currency, privacy mode, API key hash, webhook secrets, retention, share token
visitors Site, id, first seen, user id, name, email
sessions Visit with source, channel, entry and exit, device, location, latitude and longitude
events Pageviews, goals, outbound clicks, pings, with properties as JSON
payments Amount in minor units, kind, currency, provider, how it was matched
payment_claims Payment ids seen on return pages, for return_page matching
funnels Funnel definitions
vitals Core Web Vitals reports: site, time, page path and the three metrics, with no visitor or session id
salts The monthly secret for strict-mode visitor ids. Older months are deleted by the first request of a new month.
rate_limits Counters for the rate-limited public routes
schema_migrations Applied migration versions and timestamps

The IP address is not stored in any of them.

Migrations

Migrations are ordered SQL scripts, one set per dialect (sqlite and postgres; D1 uses sqlite). migrate() applies the ones not recorded in schema_migrations and returns what it did. createAnalytik() calls it before the first request unless you pass autoMigrate: false. You can also call it at startup yourself, or run analytik migrate --db <url> as a deploy step for Postgres and SQLite. D1 cannot be migrated from the command line; load the schema with analytik migrate --print-sql sqlite and wrangler d1 execute, see the CLI reference. See Upgrades.

Was this page helpful?