Content Store
The Content Store is the versioned, content-addressable persistence layer for Bowrain projects.
Architecture
The store sits between connectors, which pull and push external content, and the processing pipeline: flows, tools, content memory and terms. Versions record named snapshots of the store.
Key Concepts
- BlockIdentity: content-addressable hashing (SHA-256) for block deduplication and change detection
- ContentRef: links blocks to their external connector source with sync tracking
- DisplayHint: UI rendering guidance (preview, context, max length, content type)
- Version: named snapshot of project state with block-level diffing
ContentStore Interface
ContentStore (bowrain/core/store/store.go) is the union of role
interfaces, one per concern. All content operations are stream-scoped: an
empty stream name defaults to "main", which every project implicitly has.
// ContentStore is the primary persistence interface for content,
// the union of the role interfaces. All content operations are stream-scoped.
type ContentStore interface {
ProjectStore // projects: create, get, list, update, delete
StreamStore // streams within a project
CollectionStore // collections (stream-scoped)
ItemStore // items (stream-scoped)
BlockStore // blocks, notes, history (stream-scoped)
VersionStore // named versions + diffs (stream-scoped)
ChangeFeed // incremental sync change log (stream-scoped)
AssetStore // assets and locale variants
Close() error
}
Representative signatures; note the stream parameter throughout:
// BlockStore
StoreBlocks(ctx context.Context, projectID, stream string, blocks []*model.Block) error
GetBlocks(ctx context.Context, query BlockQuery) ([]*StoredBlock, error)
// VersionStore
CreateVersion(ctx context.Context, projectID, stream, label, description string) (*Version, error)
Diff(ctx context.Context, fromVersion, toVersion string) (*VersionDiff, error)
Backends
Two backends implement ContentStore, with different roles:
- PostgreSQL is the server's only backend.
bowrain-serverrefuses to start without apostgres://database URL and builds all of its stores on that connection. This is the source of truth for every workspace. - SQLite (
bowrain/store/sqlitestore) backs the desktop app's local working copy: a cache for speed and offline edits that mirrors the server and is never a source of truth.
import "github.com/neokapi/neokapi/bowrain/store/sqlitestore"
store, err := sqlitestore.NewSQLiteStore("working-copy.db")
if err != nil {
log.Fatal(err)
}
defer store.Close()
Both backends share one logical schema: projects, streams, collections, items, blocks, versions, the change log, and assets.
Held block writes
The server's change service reads and writes a stream's blocks through
BlockWriteStore, which the PostgreSQL backend implements and the
event-emitting decorator forwards:
// BlockWriteStore
ItemBlocks(ctx context.Context, projectID, stream, itemName string, keys []string) ([]*StoredBlock, error)
BeginBlockWrite(ctx context.Context, projectID, stream string) (BlockWrite, error)
// BlockWrite
Hold(ctx context.Context, itemName string, keys []string) ([]*StoredBlock, error)
Store(ctx context.Context, blocks []*StoredBlock) error
Commit() error
Rollback() error
keys names blocks by their durable key, their name or their row id. A
BlockWrite is one transaction under the stream's shared write lock. Hold
reads an item's blocks and locks their rows until the write ends. Store
writes the changed blocks back to those rows with their history and change-log
entries, and deletes the row of each translation the change removed, recording
the removal as target_removed. The change service holds every item a change
set names on one BlockWrite and compares the revision each operation names
against the held rows, so no other write lands between that comparison and the
commit, and the change set lands whole on one connection.
Revert, restore-to-point and rollback read a translation's history by the rows
that record its content (target_added, target_modified, target_removed).
A target_removed row restores no translation, and a decision row restores
nothing.
Block Identity
Every stored block gets a content-addressable identity computed from its source text:
identity := model.ComputeIdentity(block)
// identity.ContentHash = SHA-256 of normalized source text
// identity.ContextHash = SHA-256 of block name, type, and properties
This enables:
- Deduplication: identical source text shares the same content hash
- Change detection: version diffs compare content hashes instead of full text
- Cache invalidation: results can be cached by content hash
Version Tracking
Versions are named snapshots of a project's block state:
// Create a snapshot of a stream
v, err := store.CreateVersion(ctx, projectID, "main", "v1.0", "Initial release")
// List a stream's versions
versions, err := store.ListVersions(ctx, projectID, "main")
// Diff two versions
diff, err := store.Diff(ctx, v1.ID, v2.ID)
for _, change := range diff.Changes {
fmt.Printf("%s: %s\n", change.BlockID, change.ChangeType)
}
Flow Integration
Server-side flows read from and write to the content store through the flow service: a run loads the project's blocks from the store, executes the flow, and stores the produced targets back. See Server-side flows.