Skip to content

Thunder

Thunder is a key-value store for unstructured data. It is optimised for fast reads and infrequent writes, which makes it a good fit for data that does not change often — configuration, feature flags, user preferences, or any data that does not belong in a typed component schema.

Each record belongs to a named collection (the first argument to every method). Collections are created implicitly on first write.

SDK Usage

import { Thunder } from '@ptkl/sdk/beta'

const thunder = new Thunder()

Read Methods

find(name, filters, options?)

Returns all records in the collection that match the filters.

const flags = await thunder.find('feature_flags', { enabled: true })

With field projection and cache:

const flags = await thunder.find(
  'feature_flags',
  { enabled: true },
  { cacheTTL: 60, only: ['key', 'enabled'] }
)

findOne(name, filters, options?)

Returns the first record that matches the filters, or null.

const flag = await thunder.findOne('feature_flags', { key: 'dark_mode' })

paginate(name, filters, options?)

Returns a paginated result set. Pass page and perPage inside filters.

const page = await thunder.paginate(
  'audit_log',
  { user_id: 42, page: 0, perPage: 20 },
  { cacheTTL: 30 }
)

Read Options

All read methods accept an optional options object as the last argument.

Option Type Description
cacheTTL number Maximum age (seconds) of a cached response to accept. See below.
only string[] Limit which fields are returned.

cacheTTL has three distinct states, and omitting it is not the same as passing 0:

cacheTTL Behaviour
omitted Serve whatever is cached, regardless of age. The cheapest read.
0 Skip the cache, read from the database, then repopulate the cache
> 0 Serve a cached response only if it is younger than cacheTTL seconds, otherwise refresh from the database

Values are not clamped — cacheTTL: 1 and cacheTTL: 86400 are both honoured exactly.

// Cheapest: any cached value will do.
const flags = await thunder.find('feature_flags', {})

// Tolerate up to 10s of staleness.
const recent = await thunder.find('feature_flags', {}, { cacheTTL: 10 })

// Must reflect the latest write, wherever it came from.
const live = await thunder.findOne('feature_flags', { key: 'dark_mode' }, { cacheTTL: 0 })

Omission is the default because Thunder is optimised for fast reads, and most callers are reading configuration that a stale-by-seconds answer serves perfectly well.

When you actually need cacheTTL

Writes through Thunder already invalidate every cached read key for the collection, so a read following an insertOne/updateOne/upsert/operation/delete sees current data without any option at all.

Reach for cacheTTL when the data can change without a Thunder write — a direct database write, another service touching the same collection, or an invalidation that failed — since those changes are invisible to the cache.

Dashboard reads always bypass the cache

Requests authenticated with a platform token — that is, calls made from the Protokol Dashboard — read straight from the database regardless of cacheTTL, so operators always see live data.

Component reads use an option of the same name with the same unit, but omitting it there behaves like 0, and the value is clamped to 5–300s — see Components → Cache.


Write Methods

insertOne(name, data)

Inserts a new record.

await thunder.insertOne('feature_flags', { key: 'dark_mode', enabled: false })

updateOne(name, filters, data)

Updates the first record that matches the filters.

await thunder.updateOne('feature_flags', { key: 'dark_mode' }, { enabled: true })

upsert(name, filters, data)

Updates a matching record or creates one if none exists.

await thunder.upsert('user_settings', { user_id: 42 }, { theme: 'dark', language: 'en' })

operation(name, filters, data)

Runs a custom server-side operation on matching records.

await thunder.operation('counters', { key: 'page_views' }, { $inc: { count: 1 } })

delete(name, filters)

Deletes all records matching the filters.

await thunder.delete('feature_flags', { key: 'old_flag' })