2026-03-11 13:02:40 +00:00
---
title: go-store
description: Group-namespaced SQLite key-value store with TTL expiry, namespace isolation, quota enforcement, and reactive event hooks.
---
# go-store
`go-store` is a group-namespaced key-value store backed by SQLite. It provides persistent or in-memory storage with optional TTL expiry, namespace isolation for multi-tenant use, quota enforcement, and a reactive event system for observing mutations.
2026-04-03 07:44:28 +00:00
For declarative setup, `store.NewConfigured(store.StoreConfig{...})` takes a single config struct instead of functional options. Prefer this when the configuration is already known; use `store.New(path, ...)` when you are only varying the database path.
2026-04-03 06:47:39 +00:00
2026-03-11 13:02:40 +00:00
The package has a single runtime dependency -- a pure-Go SQLite driver (`modernc.org/sqlite` ). No CGO is required. It compiles and runs on all platforms that Go supports.
2026-03-22 01:27:35 +00:00
**Module path:** `dappco.re/go/core/store`
2026-03-11 13:02:40 +00:00
**Go version:** 1.26+
**Licence:** EUPL-1.2
## Quick Start
```go
package main
import (
2026-03-30 17:32:09 +00:00
"fmt"
2026-03-11 13:02:40 +00:00
"time"
2026-03-22 01:27:35 +00:00
"dappco.re/go/core/store"
2026-03-11 13:02:40 +00:00
)
func main() {
2026-03-30 17:37:50 +00:00
// Open /tmp/app.db for persistence, or use ":memory:" for ephemeral data.
2026-04-03 07:44:28 +00:00
storeInstance, err := store.NewConfigured(store.StoreConfig{
DatabasePath: "/tmp/app.db",
PurgeInterval: 30 * time.Second,
2026-04-04 19:45:52 +00:00
WorkspaceStateDirectory: "/tmp/core-state",
2026-04-03 07:44:28 +00:00
})
2026-03-11 13:02:40 +00:00
if err != nil {
2026-03-30 16:41:56 +00:00
return
2026-03-11 13:02:40 +00:00
}
2026-03-30 15:02:28 +00:00
defer storeInstance.Close()
2026-03-11 13:02:40 +00:00
2026-03-30 18:49:17 +00:00
// Store "blue" under config/colour and read it back.
if err := storeInstance.Set("config", "colour", "blue"); err != nil {
2026-03-30 16:41:56 +00:00
return
}
2026-03-30 18:49:17 +00:00
colourValue, err := storeInstance.Get("config", "colour")
2026-03-30 16:41:56 +00:00
if err != nil {
return
}
2026-03-30 18:49:17 +00:00
fmt.Println(colourValue) // "blue"
2026-03-11 13:02:40 +00:00
2026-03-30 17:37:50 +00:00
// Store a session token that expires after 24 hours.
2026-03-30 16:41:56 +00:00
if err := storeInstance.SetWithTTL("session", "token", "abc123", 24*time.Hour); err != nil {
return
}
2026-03-11 13:02:40 +00:00
2026-03-30 18:49:17 +00:00
// Read config/colour back into a map.
2026-03-30 16:41:56 +00:00
configEntries, err := storeInstance.GetAll("config")
if err != nil {
return
}
2026-03-30 18:49:17 +00:00
fmt.Println(configEntries) // map[colour:blue]
2026-03-11 13:02:40 +00:00
2026-03-30 17:37:50 +00:00
// Render the mail host and port into smtp.example.com:587.
2026-03-30 16:41:56 +00:00
if err := storeInstance.Set("mail", "host", "smtp.example.com"); err != nil {
return
}
if err := storeInstance.Set("mail", "port", "587"); err != nil {
return
}
renderedTemplate, err := storeInstance.Render(`{{ .host }}:{{ .port }}` , "mail")
if err != nil {
return
}
2026-03-30 17:32:09 +00:00
fmt.Println(renderedTemplate) // "smtp.example.com:587"
2026-03-11 13:02:40 +00:00
2026-03-30 18:49:17 +00:00
// Store tenant-42 preferences under the tenant-42: namespace prefix.
2026-04-04 17:34:15 +00:00
scopedStore, err := store.NewScopedConfigured(storeInstance, store.ScopedStoreConfig{
Namespace: "tenant-42",
})
2026-03-30 16:41:56 +00:00
if err != nil {
return
}
2026-04-03 05:45:24 +00:00
if err := scopedStore.SetIn("preferences", "locale", "en-GB"); err != nil {
2026-03-30 16:41:56 +00:00
return
}
2026-03-30 18:49:17 +00:00
// Stored internally as group "tenant-42:preferences", key "locale"
2026-03-11 13:02:40 +00:00
2026-03-30 17:37:50 +00:00
// Cap tenant-99 at 100 keys and 5 groups.
2026-04-04 17:34:15 +00:00
quotaScopedStore, err := store.NewScopedConfigured(storeInstance, store.ScopedStoreConfig{
Namespace: "tenant-99",
Quota: store.QuotaConfig{MaxKeys: 100, MaxGroups: 5},
})
2026-03-30 16:41:56 +00:00
if err != nil {
return
}
2026-03-30 17:32:09 +00:00
// A write past the limit returns store.QuotaExceededError.
2026-04-03 05:45:24 +00:00
if err := quotaScopedStore.SetIn("g", "k", "v"); err != nil {
2026-03-30 16:41:56 +00:00
return
}
2026-03-11 13:02:40 +00:00
2026-03-30 18:17:07 +00:00
// Watch "config" changes and print each event as it arrives.
2026-04-03 04:56:08 +00:00
events := storeInstance.Watch("config")
defer storeInstance.Unwatch("config", events)
2026-03-11 13:02:40 +00:00
go func() {
2026-04-03 04:56:08 +00:00
for event := range events {
2026-03-30 17:32:09 +00:00
fmt.Println("event", event.Type, event.Group, event.Key, event.Value)
2026-03-11 13:02:40 +00:00
}
}()
2026-03-30 17:37:50 +00:00
// Or register a synchronous callback for the same mutations.
2026-03-30 16:13:55 +00:00
unregister := storeInstance.OnChange(func(event store.Event) {
2026-03-30 17:32:09 +00:00
fmt.Println("changed", event.Group, event.Key, event.Value)
2026-03-11 13:02:40 +00:00
})
2026-03-30 15:02:28 +00:00
defer unregister()
2026-03-11 13:02:40 +00:00
}
```
## Package Layout
2026-04-03 06:20:04 +00:00
The entire package lives in a single Go package (`package store` ) with the following implementation files plus `doc.go` for the package comment:
2026-03-11 13:02:40 +00:00
| File | Purpose |
|------|---------|
2026-03-30 16:33:07 +00:00
| `doc.go` | Package comment with concrete usage examples |
2026-04-04 08:46:59 +00:00
| `store.go` | Core `Store` type, CRUD operations (`Get` , `Set` , `SetWithTTL` , `Delete` , `DeleteGroup` , `DeletePrefix` ), bulk queries (`GetAll` , `GetPage` , `All` , `Count` , `CountAll` , `Groups` , `GroupsSeq` ), string splitting helpers (`GetSplit` , `GetFields` ), template rendering (`Render` ), TTL expiry, background purge goroutine, transaction support |
| `transaction.go` | `Store.Transaction` , transaction-scoped write helpers, staged event dispatch |
2026-04-03 05:25:42 +00:00
| `events.go` | `EventType` constants, `Event` struct, `Watch` /`Unwatch` channel subscriptions, `OnChange` callback registration, internal `notify` dispatch |
2026-04-04 19:53:53 +00:00
| `scope.go` | `ScopedStore` wrapper for namespace isolation, `QuotaConfig` struct, `NewScoped` /`NewScopedConfigured` constructors, namespace-local helper delegation, quota enforcement logic |
2026-04-03 06:20:04 +00:00
| `journal.go` | Journal persistence, Flux-like querying, JSON row inflation, journal schema helpers |
2026-04-04 11:19:22 +00:00
| `workspace.go` | Workspace buffers, aggregation, query analysis, commit flow, and orphan recovery |
2026-04-03 06:20:04 +00:00
| `compact.go` | Cold archive generation to JSONL gzip or zstd |
2026-03-11 13:02:40 +00:00
Tests are organised in corresponding files:
| File | Covers |
|------|--------|
| `store_test.go` | CRUD, TTL, concurrency, edge cases, persistence, WAL verification |
| `events_test.go` | Watch/Unwatch, OnChange, event dispatch, wildcard matching, buffer overflow |
| `scope_test.go` | Namespace isolation, quota enforcement, cross-namespace behaviour |
| `coverage_test.go` | Defensive error paths (scan errors, schema conflicts, database corruption) |
| `bench_test.go` | Performance benchmarks for all major operations |
## Dependencies
**Runtime:**
| Module | Purpose |
|--------|---------|
| `modernc.org/sqlite` | Pure-Go SQLite driver (no CGO). Registered as a `database/sql` driver. |
**Test only:**
| Module | Purpose |
|--------|---------|
| `github.com/stretchr/testify` | Assertion helpers (`assert` , `require` ) for tests. |
2026-03-26 19:17:11 +00:00
There are no other direct dependencies. The package uses the Go standard library plus `dappco.re/go/core` helper primitives for error wrapping, string handling, and filesystem-safe path composition.
2026-03-11 13:02:40 +00:00
## Key Types
- **`Store` ** -- the central type. Holds a `*sql.DB` , manages the background purge goroutine, and maintains the watcher/callback registry.
- **`ScopedStore` ** -- wraps a `*Store` with an auto-prefixed namespace. Provides the same API surface with group names transparently prefixed.
- **`QuotaConfig` ** -- configures per-namespace limits on total keys and distinct groups.
- **`Event` ** -- describes a single store mutation (type, group, key, value, timestamp).
2026-04-03 05:25:42 +00:00
- **`Watch` ** -- returns a buffered channel subscription to store events. Use `Unwatch(group, events)` to stop delivery and close the channel.
2026-03-30 14:38:07 +00:00
- **`KeyValue` ** -- a simple key-value pair struct, used by the `All` iterator.
2026-03-11 13:02:40 +00:00
## Sentinel Errors
2026-03-30 14:38:07 +00:00
- **`NotFoundError` ** -- returned by `Get` when the requested key does not exist or has expired.
- **`QuotaExceededError` ** -- returned by `ScopedStore.Set` /`SetWithTTL` when a namespace quota limit is reached.
2026-03-11 13:02:40 +00:00
## Further Reading
2026-03-30 15:16:16 +00:00
- [Agent Conventions ](../CODEX.md ) -- Codex-facing repo rules and AX notes
2026-03-30 18:12:28 +00:00
- [AX RFC ](RFC-CORE-008-AGENT-EXPERIENCE.md ) -- naming, comment, and path conventions for agent consumers
2026-03-11 13:02:40 +00:00
- [Architecture ](architecture.md ) -- storage layer internals, TTL model, event system, concurrency design
- [Development Guide ](development.md ) -- building, testing, benchmarks, contribution workflow