Help us shape the future of Go! Take the 2026 Go Developer Survey

tinystore

package module
v0.1.0-rc.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 4, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

README

TinyStore

TinyStore

A small storage runtime for applications.

SQL, key-value state, durable jobs, files, metrics and logs in one directory, with one lifecycle, one memory budget and one backup.

License Go Status


Release candidates first. Releases begin with v0.1.0-rc.N, before v0.1.0. Until v0.1.0, the API may still change, and a file written by one release may not open in the next.

Go programs embed TinyStore as a library. Bun, Node and Python programs reach the same directory through tinystore serve, a sidecar that their SDK starts, and the same protocol serves remote clients.

It is not a new database engine and does not try to beat specialised ones at their own job. SQLite is underneath, with formats of its own where a workload needs one. The goal is to make the storage of an application on one machine boring to operate.

A first look

// errors left out
store, err := tinystore.Open(ctx, "./data", tinystore.Options{})
defer store.Close(ctx)

db, err := sqldb.Open(ctx, store, "app", migrations, schema)
_, err = db.Exec(ctx, `insert into users (id, name) values (?, ?)`, 42, "Ada")

state, err := kv.Open(ctx, store, kv.Options{})
sessions, err := kv.OpenBucket[string](ctx, state, "sessions", kv.Sliding(30*24*time.Hour))
err = sessions.Of("42").Set(ctx, "token", "abc123")

queues, err := jobs.Open(ctx, store, jobs.Options{})
emails, err := jobs.OpenQueue[Email](ctx, queues, "emails")
err = emails.Enqueue(ctx, Email{To: "ada@example.com"})

objects, err := blobs.Open(ctx, store, blobs.Options{})
files, err := blobs.OpenBucket(ctx, objects, "files")
_, err = files.Put(ctx, "avatars/42.png", avatar)

logs, err := records.Open(ctx, store, records.Options{})
log := slog.New(logs.Handler("api"))
log.Info("user created", "userId", 42)

series, err := metrics.Open(ctx, store, metrics.Options{})
series.Counter("signups_total").Inc()
Bun
import { open } from '@tinyshed/tinystore'

await using store = await open('./data')

const db = await store.sql('app', {
	migrations: { '0001_users.sql': 'create table users (id integer primary key, name text)' },
})
await db.exec`insert into users (id, name) values (${42}, ${'Ada'})`

const sessions = store.kv.bucket<string>('sessions', { sliding: '30d' })
await sessions.of('42').set('token', 'abc123')

const emails = store.jobs.queue<{ to: string }>('emails')
await emails.enqueue({ to: 'ada@example.com' })

const files = store.blobs.bucket('files')
await files.put('avatars/42.png', Bun.file('avatar.png'))

const log = store.records.logger('api')
log.event('user.created', { userId: 42 })

const signups = store.metrics.counter('signups_total')
signups.inc()
Python
import asyncio
import logging
from pathlib import Path

import tinystore


async def main() -> None:
    async with tinystore.open("./data") as store:
        db = await store.sql(
            "app", migrations={"0001_users.sql": "create table users (id integer primary key, name text)"}
        )
        await db.exec("insert into users (id, name) values (?, ?)", 42, "Ada")

        sessions = store.kv.bucket("sessions", str, sliding="30d")
        await sessions.of("42").set("token", "abc123")

        emails = store.jobs.queue("emails", dict[str, str])
        await emails.enqueue({"to": "ada@example.com"})

        files = store.blobs.bucket("files")
        await files.put("avatars/42.png", Path("avatar.png").read_bytes())

        logging.getLogger().addHandler(store.records.handler("api"))
        logging.info("user created", extra={"user_id": 42})

        store.metrics.counter("signups_total").inc()


asyncio.run(main())
go get github.com/tinyshed/tinystore
bun add @tinyshed/tinystore
pip install tinyshed-tinystore

examples/notes is a complete program that uses every engine, and Getting started builds a first one step by step.

Engines

SQL Relational state The application's own SQL databases: tables from structs, checked migrations.
KV Application state Current state: typed buckets, counters, expiry, versions.
Jobs Durable background work Work that runs at its time: retries, leases, repeats.
Blobs Files and objects Files by path, checked when read whole.
Records Logs and events Read by time, level and keys.
Metrics Time series Samples kept bit for bit, answered exactly.

By default each engine has its own file and its own writer. An application can keep its jobs in its SQL database and commit a job together with its rows in one Batch, as Jobs and your data shows. One call backs up every engine into one checked zip: see Backups.

Documentation

The guides cover every engine, with a page per feature and each example in Bun, Python and Go. Start with these:

For reference, see the Bun and Node API, the Python API and the wire protocol.

Looking at a store

The same binary shows a person, and an AI agent, what a store holds while its application runs. Both SDK packages install it as the tinystore command: bunx @tinyshed/tinystore … and uvx --from tinyshed-tinystore tinystore … run it with nothing else installed, and go install github.com/tinyshed/tinystore/cmd/tinystore@latest builds it.

tinystore status ./data                            # each engine's bytes, and who serves the directory
tinystore logs ./data -f --level warn              # the application's logs, as they arrive
tinystore serve ./data                             # the directory's sidecar, until Ctrl+C
tinystore stop ./data                              # its server, once its work is finished
claude mcp add tinystore -- tinystore mcp ./data   # logs, kv, jobs and SQL for an agent, read only

The command line and AI agents explain each command. The image ghcr.io/tinyshed/tinystore serves a store to remote clients, as A remote server shows.

Numbers

TinyStore does not beat every specialized database at its own workload. It makes the combined workload of a small application cheap enough that replacing several services does not mean giving up performance.

The application workload at 64 concurrent clients completes 81,531 requests/s locally and 43,178 requests/s on an 8-vCPU Yandex VM, median of three passes. With jobs joined to the application's SQL file and written in one Batch, that is 1.49x and 1.52x the Redis + PostgreSQL + VictoriaMetrics + files stack. Every pass of this configuration has zero errors, dropped logs and jobs left waiting.

Two three-minute passes on the VM complete 35,151 and 35,169 requests/s, 1.35x the service stack, and handle every queued job. This is a bounded sustained-load check, not a half-hour steady-state guarantee.

TinyStore Batch stores the queue in the application's SQL database and commits its rows and job together. TinyStore Split uses the default jobs file and commits each file independently.

Application requests per second, 64 clients Application composite memory, client and services included kv sqldb records metrics-disk metrics-memory metrics-ingest

The cards use local Linux-container medians on a Ryzen 7 7700, 16 visible CPUs, Go 1.27.1 and Docker Desktop 29.6.2. Records: 445,136 private production lines. Metrics: 5,090,400 TSBS samples, with ingest plus settle timed together. Memory is client high-water RSS plus the greater of service high-water RSS and final service-tree PSS, not a simultaneous whole-stack peak. Application memory covers the full 8/64/256-client run. In the application memory card, the upper solid bar is idle and the lower faded bar is the load peak from the same process run. That card uses a separate three-pass, three-second round with service idle memory explicitly measured. Its TinyStore row uses the Batch configuration.

These full-comparison processes also link the comparison libraries. Small, independently linked consumers on the Yandex VM start at 3.4 MiB for the empty probe, 3.7 MiB for tinystore.Open alone, 8.5-9.3 MiB for one engine and 14.9 MiB for all six engines, without a loaded corpus.

Raw binary is an actual uncompressed TSBS export: an eight-byte timestamp and eight-byte float value per sample, plus a JSON index with the series' labels, offsets and counts. Its 78.2 MiB includes both files and has been checked bit for bit. It is a plain representation, not a database with queries, retention or integrity checks.

KV: embedded, sidecar and server benchmarks

Embedded means a direct Go library call. Bun and Python use their SDKs through a local sidecar or a loopback TCP server with a token; they have no embedded mode. Go sidecar/server rows isolate the process boundary within one language; SDK rows also include that language's encoding and scheduling. All modes use the same 100,000 keys and 128-byte values, in one interleaved session.

These are workload comparisons, not universal wins: cloud KV writes did not improve, specialized KV engines read faster, and Batch at 256 clients leaves a growing jobs queue. Metrics competitors do not have identical commit durability. The cards measure ad4f047, before the exact block summaries, which a round of their own measures. The saved revisions, all passes, limits and reproduction commands are in the round report. The consumer-memory and client-mode follow-up records the small consumers, paired memory bars, raw representation and SDK modes.

The rounds, prototypes and open questions behind the design are in tinyshed/research.

Origin

TinyStore started inside Dashbin, which needed state, jobs, files and metrics without turning one self-hosted binary into a set of services. Different workloads wanted different storage shapes, but not different services, and the research grew into a project of its own.

Development

TinyStore is developed with extensive AI assistance: its direction, architecture and acceptance criteria are a person's, and coding agents do much of the implementation, review and measurement. Generated code is not evidence that anything works; the tests, fuzzers and reproducible measurements are. AGENTS.md is the contract both follow.

License

Apache-2.0.

Documentation

Overview

Package tinystore keeps one application's data in one directory: a file per engine, and one lifecycle for all of them.

data/
├── LOCK           held while a store is open
├── metrics.db     metrics.Open(ctx, store, …)
├── records.db     records.Open(ctx, store, …)
├── kv.db          kv.Open(ctx, store, …)
├── jobs.db        jobs.Open(ctx, store, …)
├── blobs/         blobs.Open(ctx, store, …): blobs.db and the files
└── sql/
    └── app.db     sqldb.Open(ctx, store, "app", migrations)

Open the store, then each engine the program uses against it. Close stops background work and closes every engine, the last opened first. The store imports no engine, so a program links only the engines it opens.

tinystore.go   Open, Close: the store and its lifecycle
errors.go      the sentinels every engine wraps
engine.go      Claim, Attach, Logger, Now: how an engine joins the store
every.go       background work, and how its failures are logged
memory.go      Reserve: one memory budget for every engine's work
self.go        opt-in bounded engine reports, stored as ordinary metrics
snapshot.go    Snapshot: every engine's file copied while it keeps working

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrInvalid   = errors.New("invalid request")
	ErrLimit     = errors.New("resource limit")
	ErrClosed    = errors.New("store is closed")
	ErrInUse     = errors.New("already in use")
	ErrConflict  = errors.New("state changed")
	ErrCorrupt   = errors.New("corrupt data")
	ErrTooOld    = errors.New("too old")
	ErrTooNew    = errors.New("too new")
	ErrSuspended = errors.New("suspended")
)

Every engine wraps these, so errors.Is means the same in all of them.

Functions

func SnapshotPath

func SnapshotPath(dir, name string) string

SnapshotPath is where an engine writes its copy of name inside dir.

Types

type Engine

type Engine interface {
	Close(ctx context.Context) error
}

type LimitError

type LimitError struct {
	Name   string
	Wanted int64
	Bound  int64
	Kind   error // ErrLimit when nil
}

LimitError is a limit a call reached, named: the bound, what the call would have taken of it, what it held included, and the bound's size, so that a caller can say which limit to raise or how much less to ask for:

resource limit: decoded samples: 120000 past 100000

errors.Is finds ErrLimit through it, and Kind, an engine's own limit, when it has one.

func (*LimitError) Error

func (e *LimitError) Error() string

func (*LimitError) Unwrap

func (e *LimitError) Unwrap() error

type Measure

type Measure struct {
	Engine, Name string
	Value        uint64
	Counter      bool
}

Measure is an integer counter or gauge reported by an engine. Names have bounded, fixed cardinality: do not put a bucket, queue, key or path in them.

type MemoryUsage

type MemoryUsage struct {
	Used, Peak, Capacity int64
}

MemoryUsage is the store's budget: Capacity is Options.Memory, zero when every engine keeps only its own per-call limits.

type Options

type Options struct {
	// Logger receives TinyStore's own logs; nil discards them.
	Logger *slog.Logger

	// Manual runs no background work: the application calls each engine's
	// maintenance itself. Tests usually want this.
	Manual bool

	// Clock replaces time.Now for the store and every engine opened against it.
	Clock func() time.Time

	// Memory bounds the bytes that all engines' in-flight work holds at once;
	// zero leaves each engine to its own per-call limits.
	Memory int64

	// SelfMetrics periodically writes available engine reports into an opened
	// metrics engine. Manual stores call FlushSelfMetrics themselves.
	SelfMetrics bool
}

type Reporter

type Reporter interface {
	Report() []Measure
}

type Reservation

type Reservation struct {
	// contains filtered or unexported fields
}

Reservation is bytes of the store's memory that one piece of work holds. The zero Reservation holds nothing, for work that needs no memory.

func (*Reservation) Release

func (r *Reservation) Release()

Release gives back what the reservation holds; a second call gives nothing.

func (*Reservation) Shrink

func (r *Reservation) Shrink(n int64)

Shrink gives back all but n of the bytes, for work that reserved its worst case and has learnt what it holds.

type SelfWriter

type SelfWriter interface {
	WriteSelf(context.Context, []Measure) error
}

SelfWriter writes one report without counting that write as user ingest. The root uses this interface without importing the metrics engine.

type Snapshot

type Snapshot struct {
	Dir   string
	Files []SnapshotFile
}

Snapshot is a copy of every engine's file, in Dir inside the store's directory, so that it takes the space of the disk the data is on.

func (Snapshot) Remove

func (s Snapshot) Remove() error

type SnapshotFile

type SnapshotFile struct {
	// Name is the file's path inside the store, such as "metrics.db",
	// "sql/app.db" or "blobs/objects/1a3/1a3f07".
	Name   string `json:"name"`
	Engine string `json:"engine"`
	// Schema is how many migrations the engine's database has.
	Schema int `json:"schema"`
	// Stored asks a backup to keep the bytes as they are rather than deflate
	// them: an application's files are mostly media, which does not compress.
	Stored bool `json:"-"`
}

SnapshotFile is one file of an engine's copy.

type Snapshotter

type Snapshotter interface {
	Snapshot(ctx context.Context, dir string) ([]SnapshotFile, error)
}

Snapshotter is an engine that can copy its files while it keeps working.

type Store

type Store struct {
	// contains filtered or unexported fields
}

func Open

func Open(ctx context.Context, dir string, options Options) (*Store, error)

Open creates dir if needed and holds it until Close; a second store on the same directory, in this process or another, is refused with ErrInUse. ctx bounds the opening only: background work lives until Close.

func (*Store) Attach

func (s *Store) Attach(engine Engine) error

Attach hands an opened engine to the store, which closes it on Close.

func (*Store) Claim

func (s *Store) Claim(name string) (filePath string, release func(), err error)

Claim reserves a file or directory inside the store for one engine and returns its path, creating the directory it lies in, or the directory itself for a name that ends in /. release gives the name back to an engine that failed to open; an attached engine keeps it until the store closes.

metrics.db  records.db  blobs/    engines
sql/<name>.db                     databases the application names

func (*Store) Close

func (s *Store) Close(ctx context.Context) error

Close stops background work, closes every engine, the last opened first, and releases the directory. When ctx ends first Close returns its error, and the cleanup still finishes; a later Close waits for it.

func (*Store) Dir

func (s *Store) Dir() string

Dir is the directory the store holds. A tool that backs the store up looks there for the files of engines no one has opened.

func (*Store) Every

func (s *Store) Every(name string, interval time.Duration, work func(context.Context) error) (soon func())

Every runs work at each interval until the store closes, and soon asks for its next run now rather than at the next interval: asks that come while it runs or waits to run make one run more. A Manual store runs nothing, and its soon does nothing; a failure is logged, and the next run still happens.

func (*Store) EveryEngine

func (s *Store) EveryEngine(engine, name string, interval time.Duration, work func(context.Context) error) (
	soon func(),
)

EveryEngine runs engine work with the same engine attribute as Store.Logger.

func (*Store) FlushSelfMetrics

func (s *Store) FlushSelfMetrics(ctx context.Context) error

FlushSelfMetrics captures available reports and the store's memory budget once, then commits their samples together. It is a no-op when disabled or when no metrics engine is open. It reports reserved bytes, not process RSS.

func (*Store) Logger

func (s *Store) Logger(engine string) *slog.Logger

Logger is the application's logger, labelled with the engine that logs.

func (*Store) Memory

func (s *Store) Memory() MemoryUsage

func (*Store) Now

func (s *Store) Now() time.Time

func (*Store) Reserve

func (s *Store) Reserve(ctx context.Context, bytes int64) (*Reservation, error)

Reserve waits, in arrival order, until bytes fit the store's memory. A reservation larger than the whole budget is refused at once with ErrLimit, and a store without Options.Memory grants every reservation.

func (*Store) ReserveNow

func (s *Store) ReserveNow(bytes int64) (*Reservation, error)

ReserveNow takes bytes only if they fit at once, ahead of any reservation waiting, and is ErrLimit otherwise. Work holding what a waiting reservation may need, such as a file's writer, reserves this way: waiting, it could wait for itself.

func (*Store) Snapshot

func (s *Store) Snapshot(ctx context.Context) (Snapshot, error)

Snapshot copies every attached engine's file. Each copy is one moment of its engine; two engines are two moments, as no write spans two engines anyway. The engines keep working, and Close waits for the copying to finish.

Directories

Path Synopsis
Package backup writes every engine of a tinystore.Store into one zip, and restores it into an empty directory before the store opens.
Package backup writes every engine of a tinystore.Store into one zip, and restores it into an empty directory before the store opens.
Package blobs keeps an application's files in blobs/ inside a tinystore.Store: objects under keys that are paths, their rows in blobs.db, their bytes inline up to 16 KiB and in a file of their own above it.
Package blobs keeps an application's files in blobs/ inside a tinystore.Store: objects under keys that are paths, their rows in blobs.db, their bytes inline up to 16 KiB and in a file of their own above it.
cmd
tinystore module
examples
notes command
Command notes is what a program using TinyStore looks like: one directory, its own SQL, its own metrics and logs, its drafts, its jobs, its notes' attachments, a backup, and one Close.
Command notes is what a program using TinyStore looks like: one directory, its own SQL, its own metrics and logs, its drafts, its jobs, its notes' attachments, a backup, and one Close.
internal
admission
Package admission lets work into an engine while it is open, a bounded number of each kind at a time, and lets Close wait for the work already in.
Package admission lets work into an engine while it is open, a bounded number of each kind at a time, and lets Close wait for the work already in.
dbstat
Package dbstat divides a SQLite file's pages among the tables and indexes that own them, as SQLite's dbstat table does, for measurements: SQLite as the driver builds it leaves dbstat out.
Package dbstat divides a SQLite file's pages among the tables and indexes that own them, as SQLite's dbstat table does, for measurements: SQLite as the driver builds it leaves dbstat out.
dirlock
Package dirlock holds a store's directory for one process, through a lock on its LOCK file.
Package dirlock holds a store's directory for one process, through a lock on its LOCK file.
release command
Command release makes a TinyStore release: the three tags of its Go modules, and into dist/ the tinystore binary for every platform, an archive of each with SHA256SUMS, the npm packages and the Python wheels that carry it, and the release's notes.
Command release makes a TinyStore release: the three tags of its Go modules, and into dist/ the tinystore binary for every platform, an archive of each with SHA256SUMS, the npm packages and the Python wheels that carry it, and the release's notes.
sizeprobe command
Command sizeprobe links the public API so task size can report what importing it costs.
Command sizeprobe links the public API so task size can report what importing it costs.
sizeprobe/baseline command
Command baseline is the same program without the import, so task size can subtract it.
Command baseline is the same program without the import, so task size can subtract it.
sizeprobe/report command
Command report prints what task size measured: the probe's target, its size and what the import added over the baseline.
Command report prints what task size measured: the probe's target, its size and what the import added over the baseline.
sqlite
Package sqlite owns file and transaction mechanics, not engine data.
Package sqlite owns file and transaction mechanics, not engine data.
term
Package term says whether a file is a terminal a person reads, and whether it shows colours, for the console lines a logger writes beside the store.
Package term says whether a file is a terminal a person reads, and whether it shows colours, for the console lines a logger writes beside the store.
Package jobs keeps work that runs at its time in jobs.db inside a tinystore.Store: typed queues whose jobs wait for their time, are leased to a worker, retried, repeated by cron text, and run at least once.
Package jobs keeps work that runs at its time in jobs.db inside a tinystore.Store: typed queues whose jobs wait for their time, are leased to a worker, retried, repeated by cron text, and run at least once.
Package kv keeps an application's current state in kv.db inside a tinystore.Store: buckets of one value type and counters, keyed by text in branches, with expiry by the store's clock and versions that never repeat.
Package kv keeps an application's current state in kv.db inside a tinystore.Store: buckets of one value type and counters, keyed by text in branches, with expiry by the store's clock and versions that never repeat.
Package metrics stores exact numeric time series in one SQLite file.
Package metrics stores exact numeric time series in one SQLite file.
Package records keeps an application's logs and events in records.db inside a tinystore.Store.
Package records keeps an application's logs and events in records.db inside a tinystore.Store.
server module
Package sqldb gives an application its own SQLite databases inside a tinystore.Store.
Package sqldb gives an application its own SQLite databases inside a tinystore.Store.
internal/catalog
Package catalog reads what a SQLite file says its tables are, through its pragmas and the text sqlite_schema keeps, and says how two files differ: in structure, which sqldb's Open checks, and in the text of expressions, which a test reports.
Package catalog reads what a SQLite file says its tables are, through its pragmas and the text sqlite_schema keeps, and says how two files differ: in structure, which sqldb's Open checks, and in the text of expressions, which a test reports.
sqldbtest
Package sqldbtest checks, in a program's own tests, that a database's migrations make what its schema declares, and writes the next migration when go tool tinystore asks it to.
Package sqldbtest checks, in a program's own tests, that a database's migrations make what its schema declares, and writes the next migration when go tool tinystore asks it to.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL