Strata Go
Strata Go is a Go SDK and embeddable Strata runtime for applications that need
a consistent, layered storage model for structured state and related binary
payloads.
At the API level, Strata stores append-only stories inside anthologies.
Each story is a timeline of immutable chapters. A chapter contains JSON plus
zero or more named artifacts such as logs, model outputs, reports, images,
or other binary payloads. Small artifact bytes can be returned inline with the
chapter; larger artifacts are stored in a blob layer and fetched lazily through
the same artifact interface.
The same Go client can talk to a remote Strata service, run the Go daemon
in-process without opening a TCP port, or start a local Strata-compatible HTTP
daemon backed by local storage.
Why Strata
Strata is useful when an application needs to keep durable, ordered state while
also attaching larger files to each step of that state.
- Consistent story state: story metadata, chapter ordinals, chapter JSON,
lifecycle flags, clone links, and artifact descriptors are persisted in the
rowstore.
- Blob-aware artifacts: artifact metadata includes name, content type, size,
and SHA-256. Bytes are either kept inline below a configurable threshold or
stored in a blobstore and downloaded only when requested.
- Immutable chapters: appends create ordered chapters. Existing chapters are
read back by ordinal, including through linked clones.
- Same API everywhere: code written against
pkg/client works against a
remote service, an in-process daemon, or a local HTTP daemon.
- Embeddable by default: tests, CLIs, desktop tools, and local workers can
use Strata storage directly without deploying a separate server.
Storage Model
Strata separates the consistency layer from the byte layer:
| Layer |
Responsibility |
Current local backends |
| Rowstore |
Stories, chapters, lifecycle state, clone links, artifact descriptors, and inline artifact bytes. |
SQLite, Pebble |
| Blobstore |
Large artifact bodies referenced by rowstore descriptors. |
Filesystem blobstore |
The rowstore is the source of truth for committed story state. The blobstore
holds payload bytes that are too large or inconvenient to keep inline. When a
chapter is read, each artifact is represented by the same
artifact.Artifact interface whether the bytes were included inline or need to
be streamed from the blobstore-backed download endpoint.
MaxInlineArtifactBytes controls the inline cutoff. The default is 256
bytes.
Concepts
| Concept |
Meaning |
| Anthology |
A namespace for stories. |
| Story |
A consistent append-only timeline keyed by anthology ID and story ID. |
| Chapter |
An immutable JSON payload at a specific ordinal in a story. |
| Artifact |
A named payload attached to a chapter. Artifacts can be inline or lazily downloaded. |
| Clone |
A linked story that inherits chapters through a fork ordinal and stores only post-fork appends. |
Installation
go get github.com/colony-2/strata-go@latest
Choose A Runtime
Remote Service
Use a remote client when Strata is deployed as a central HTTP service.
c, err := client.NewBuilder().
WithRemote("https://api.strata.example.com", os.Getenv("STRATA_TOKEN")).
Build(ctx)
if err != nil {
log.Fatal(err)
}
The simpler constructor is still available when you already have a base URL and
API key:
c := client.MustNew(client.Config{
BaseURL: "https://api.strata.example.com",
APIKey: os.Getenv("STRATA_TOKEN"),
})
Embedded In-Process
Use embedded mode for tests, local tools, single-process services, or edge
deployments. The client routes requests directly to the daemon handler; no TCP
listener is opened.
c, err := client.NewBuilder().
WithEmbeddedConfig(daemon.Config{
RowStoreURI: "sqlite:///tmp/strata.db",
BlobStoreURI: "blobfs:///tmp/strata-blobs",
MaxInlineArtifactBytes: 256,
}).
Build(ctx)
if err != nil {
log.Fatal(err)
}
defer c.Close(ctx)
If your application already owns storage handles, pass them in with
WithEmbeddedStores. Borrowed stores are not closed by Client.Close.
rows, err := sqliterowstore.Open("/tmp/strata.db")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
blobs, err := blobstore.NewFS("/tmp/strata-blobs")
if err != nil {
log.Fatal(err)
}
c, err := client.NewBuilder().
WithEmbeddedStores(rows, blobs).
Build(ctx)
if err != nil {
log.Fatal(err)
}
Supported embedded storage URI schemes:
| Setting |
Value |
Notes |
RowStoreURI |
sqlite:///absolute/path/to/strata.db |
SQLite rowstore; the usual default for durable local embedded use. |
RowStoreURI |
pebble:///absolute/path/to/rows-dir |
Pebble rowstore. |
BlobStoreURI |
blobfs:///absolute/path/to/blob-dir |
Filesystem blobstore for large artifact bytes. |
Local HTTP Daemon
Use the daemon when you want a real Strata-compatible HTTP endpoint backed by
local stores.
cfg := daemon.Config{
ListenAddr: "127.0.0.1:0",
RowStoreURI: "sqlite:///tmp/strata.db",
BlobStoreURI: "blobfs:///tmp/strata-blobs",
MaxInlineArtifactBytes: 256,
}
srv, err := daemon.New(cfg)
if err != nil {
log.Fatal(err)
}
defer srv.Shutdown(context.Background())
if err := srv.Start(context.Background()); err != nil {
log.Fatal(err)
}
addr, err := srv.Addr()
if err != nil {
log.Fatal(err)
}
c := client.MustNew(client.Config{
BaseURL: fmt.Sprintf("http://%s", addr),
APIKey: "local",
})
Quick Start
package main
import (
"context"
"log"
"os"
"strings"
"github.com/colony-2/strata-go/pkg/client"
"github.com/colony-2/strata-go/pkg/client/story"
)
func main() {
ctx := context.Background()
c := client.MustNew(client.Config{
BaseURL: "https://api.strata.example.com",
APIKey: os.Getenv("STRATA_TOKEN"),
})
key := story.Key{AnthologyID: "demo", StoryID: "pipeline"}
handle, err := c.CreateStory(ctx, key, story.CreateOptions{
RequestID: "50a045d3-f77a-4e42-84e2-3d4dbd16a313",
})
if err != nil {
log.Fatalf("create story: %v", err)
}
chapter := handle.NewChapter().
WithJSON(strings.NewReader(`{"event":"build","status":"ok"}`))
if err := handle.AppendChapter(ctx, chapter); err != nil {
log.Fatalf("append chapter: %v", err)
}
}
Run the example application:
go run ./examples/quickstart
Chapters And Artifacts
Artifacts are intentionally uniform. The same artifact.Artifact value can be
created from memory, a file, a reader factory, inline API data, or a remote
download locator.
import (
"context"
"io"
"os"
"strings"
"github.com/colony-2/strata-go/pkg/client/artifact"
)
summary := []byte(`{"tests":42,"failed":0}`)
logArtifact := artifact.FromReader(
"build.log",
"text/plain",
logSize,
func(ctx context.Context) (io.ReadCloser, error) {
return os.Open(logFile)
},
)
chapter := handle.NewChapter().
WithJSON(strings.NewReader(`{"event":"build"}`)).
AddArtifact(artifact.FromBytes("summary.json", "application/json", summary)).
AddArtifact(logArtifact)
if err := handle.AppendChapter(ctx, chapter); err != nil {
return err
}
latest, err := handle.GetLastChapter(ctx)
if err != nil {
return err
}
for _, art := range latest.Artifacts() {
// Bytes returns inline data directly or fetches remote artifact bytes on demand.
// For large artifacts, prefer WriteTo or SaveToFile to avoid materializing bytes.
data, err := art.Bytes(ctx)
if err != nil {
return err
}
_ = data
}
When you already know the story key and chapter ordinal, read a chapter
directly:
chapter, err := c.Chapter(ctx, story.Key{
AnthologyID: "demo",
StoryID: "pipeline",
}, 42)
if err != nil {
return err
}
payload := chapter.Body()
_ = payload
If the story already exists and you only need to append, skip loading a story
handle:
chapter := story.NewChapter().
WithJSON(strings.NewReader(`{"event":"fast-path"}`))
if err := c.SaveChapter(ctx, key, chapter); err != nil {
return err
}
Story Lifecycle
key := story.Key{AnthologyID: "demo", StoryID: "pipeline-123"}
initial := story.NewChapter().
WithJSON(strings.NewReader(`{"event":"seed"}`))
handle, err := c.CreateStory(ctx, key, story.CreateOptions{
RequestID: "50a045d3-f77a-4e42-84e2-3d4dbd16a313",
InitialChapter: initial,
})
if err != nil {
log.Fatal(err)
}
clone, err := c.CloneStory(ctx, key, story.CloneOptions{
DestinationKey: story.Key{
AnthologyID: "demo",
StoryID: "pipeline-123-copy",
},
LastOrdinal: handle.ChapterCount() - 1,
})
if err != nil {
log.Fatal(err)
}
if err := clone.Finalize(ctx, story.FinalizeOptions{}); err != nil {
log.Fatal(err)
}
iter, err := c.ListStories(ctx, "demo", story.ListOptions{PageSize: 25})
if err != nil {
log.Fatal(err)
}
for iter.HasNext() {
summary, err := iter.Next(ctx)
if err != nil {
log.Fatal(err)
}
log.Printf("story=%s finalized=%v", summary.StoryId, summary.Finalized)
}
List helpers return pull-based iterators so callers do not need to manage page
tokens directly.
iter, err := handle.Chapters(ctx, story.ChaptersOptions{PageSize: 50})
if err != nil {
log.Fatal(err)
}
for iter.HasNext() {
chapter, err := iter.Next(ctx)
if err != nil {
if errors.Is(err, pagination.ErrNoMoreItems) {
break
}
log.Fatal(err)
}
consume(chapter)
}
Operational Notes
WithRetryPolicy, WithUserAgent, and WithLogger work for both remote and
embedded clients. WithHTTPClient is remote-only because embedded clients use
an internal transport wired directly to the daemon handler.
The integration test starts the Go daemon with temporary rowstore and blobstore
directories, binds to a random localhost port, and exercises the public client
against that endpoint. No external Strata server binary is required.
The repository also includes an inspection CLI under cmd/strata:
go run ./cmd/strata --url http://localhost:8080 stories list --anthology demo
go run ./cmd/strata --url http://localhost:8080 chapters get --anthology demo --story pipeline --chapter 0
go run ./cmd/strata --url http://localhost:8080 artifacts get --anthology demo --story pipeline --chapter 0 --name summary.json
Development
Generated OpenAPI files live in internal/openapi. Do not edit them by hand.
After changing the OpenAPI sources or generator config, regenerate with:
make generate
For validation:
make test
Maintainers should also read: