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

stellarjay

package module
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Sep 27, 2026 License: AGPL-3.0 Imports: 19 Imported by: 0

README

Stellar Jay logo

Stellar Jay

Agent setup

Give an agent safe write access with the MCP server. Every change is kept and attributed to the agent that made it, and any change can be undone.

{
  "mcpServers": {
    "aviansuite": {
      "command": "stellarjay-mcp",
      "env": {"STELLARJAY_URL": "https://store.example.com", "STELLARJAY_TOKEN": "writer-token"}
    }
  }
}

Install it with go install github.com/kyle-visner/stellarjay/cmd/stellarjay-mcp@latest. Hosted on AvianSuite, use the remote server https://mcp.aviansuite.com/mcp instead. Tools and details: docs/mcp.md.

TL;DR

Stellar Jay is an append-only fact store for AI agents trusted with critical business data. Agents can add flexible JSON facts, but the hosted API cannot rewrite or delete history. Every write is attributed, encrypted, safe to retry, checked for stale state, and available for replay.

Requires Go 1.22 or later:

git clone https://github.com/kyle-visner/stellarjay.git
cd stellarjay
go install ./cmd/stellarjay-server
stellarjay-server init ./secrets

The initializer prints reader, writer, and admin tokens once. Save them in a password manager, then continue to Run Stellar Jay.

Who Stellar Jay is for

Stellar Jay is for developers and small teams moving from read-only copilots to agents that are allowed to operate. It fits accounting, operations, compliance, approvals, and other work where agents write critical data, mistakes must stay visible and correctable, and fact shapes evolve with the job.

Stellar Jay is designed for single-tenant systems: one organization, one trust boundary, and one writer process per store. Many agents and applications can share that store, including dashboards, internal tools, APIs, and automated workflows. Stellar Jay is not meant to be the globally distributed, multi-tenant backend for a web application.

Why Stellar Jay exists

Traditional databases assume deterministic application code owns every read and write. Agents make judgment calls, retry uncertain work, and sometimes behave in unexpected ways—at machine speed. A mutable database can turn one bad decision, runaway loop, or malicious instruction into lost source data before anyone notices.

Agent risk Stellar Jay response
Destructive behavior or a wrong decision Append-only writes, credential roles, throttling, and corrections that preserve evidence
A timeout or stale decision Return the original retry result or reject a write based on old history
A changing job Accept new JSON fields and fact types without rewriting old facts

You can build these protections around a general-purpose database. Stellar Jay makes them part of every write instead of leaving them to each application.

Stellar Jay does not decide whether a fact is true. An authorized agent can still write a bad fact; Stellar Jay keeps that action visible and correctable.

How it works

Stellar Jay stores a linear chain of events. Each event records what happened, who did it, an encrypted JSON payload, and the event before it. Payloads stay flexible; the history rules do not.

The normal write flow is:

  1. Read the current root.
  2. Submit a fact with that expected_root and a stable Idempotency-Key.
  3. Stellar Jay derives the actor, encrypts and hashes the event, writes it, and advances the root.
  4. Identical retries return the original event. Stale roots and reused keys with different content return 409 conflict.

Each event address depends on the event before it. Changing old content changes the hashes that follow, so an off-host copy of the root can detect rewritten or replaced history.

Corrections, retractions, and approvals are new events, never edits. The hosted API has no update or delete path for history, and callers cannot choose their own identity. One writer process serializes writes for each data volume; many agents can use that process, but Stellar Jay is not a distributed consensus system.

Run Stellar Jay

Deploy the hosted service

Prerequisites: a Linux host with Docker Compose, ports 80 and 443 reachable, and an A/AAAA record pointing a domain at the host.

cp .env.example .env
# Edit .env and set STELLARJAY_DOMAIN.

stellarjay-server init ./secrets

docker compose up -d --build
docker compose ps
curl https://stellarjay.example.com/health/ready
curl https://stellarjay.example.com/llm.txt

The origin is the website. / links to /llm.txt, which is the agent setup contract (CLI + STELLARJAY_URL / STELLARJAY_TOKEN, not per-app MCP).

The initializer will not replace existing secrets. The server requires an external data key and hashed credential file.

Append a fact

Fetch the current root first:

export STELLARJAY_URL=https://stellarjay.example.com
export STELLARJAY_TOKEN='the-writer-token'

curl -fsS \
  -H "Authorization: Bearer $STELLARJAY_TOKEN" \
  "$STELLARJAY_URL/v1/root"

Use the returned root as expected_root and choose one stable idempotency key for the logical operation. Use an empty string only for the first event in a new database.

curl -fsS -X POST "$STELLARJAY_URL/v1/events" \
  -H "Authorization: Bearer $STELLARJAY_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: fact-primary-contact-v1-7f3d2a" \
  --data '{
    "type": "business.fact",
    "entity_id": "01JOPAQUE8F3K2M7Q9R4T6V1WX",
    "command": "fact assert",
    "payload": {"primary_contact": "Ada Lovelace"},
    "expected_root": "sha256:root-from-the-previous-response"
  }'

See the API guide for metadata-first replay, selective bounded payload reads, stable-root pagination, refs, snapshots, and administrative endpoints.

Use the embedded Go library
store, err := stellarjay.OpenStore(".stellarjay")
if err != nil { /* handle error */ }
defer store.Close()
root, err := store.Append(stellarjay.Context{Actor: "agent"}, stellarjay.AppendOptions{
    Type: "business.fact", Command: "fact assert", Payload: fact,
})

OpenStore is a local-development convenience that co-locates the key and data. Production processes must use OpenStoreWithDataKey; the server enforces this.

Production boundaries

  • One process owns each writable data volume.
  • Caddy handles HTTPS; bearer credentials provide reader, writer, or admin access. An optional operator role manages the catalog and cannot append.
  • Omitted token scopes and an unconfigured catalog preserve open writes. A scoped token, or an enforced catalog, rejects appends outside that boundary.
  • Payloads are encrypted at rest, with the data key stored outside the volume and snapshots.
  • Snapshots should be copied off-host.
  • Containers run as non-root with a read-only root filesystem.

Read the architecture, security, API, and operations guides before running Stellar Jay with sensitive data. Point an agent at $STELLARJAY_URL/llm.txt to set up. llm.md is the full write/replay contract.

Verify

GOCACHE=/tmp/stellarjay-gocache go test -race ./...
GOCACHE=/tmp/stellarjay-gocache go vet ./...
docker compose config
docker build -t stellarjay:test .

License

AGPL-3.0-or-later. See LICENSE.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DefaultDir

func DefaultDir() string

DefaultDir is the local store directory used when none is given. A store created before the rename under .stellarjay keeps being used.

func Getenv

func Getenv(name string) string

Getenv returns the trimmed value of a STELLARJAY_ variable. When it is unset, the pre-rename STELLARJAY_ name is read instead and a one-time deprecation warning is logged, so existing deployments keep working.

Types

type AppError

type AppError struct {
	Code    ErrorCode `json:"code"`
	Message string    `json:"message"`
}

func (*AppError) Error

func (e *AppError) Error() string

type AppendOptions

type AppendOptions struct {
	Type        string
	EntityID    string
	Command     string
	Payload     any
	CreatedAt   time.Time
	RequestID   string
	RequestHash string
}

type Context

type Context struct {
	Actor string
	Role  string
}

type EncryptedPayload

type EncryptedPayload struct {
	Algorithm  string `json:"algorithm"`
	Nonce      string `json:"nonce"`
	Ciphertext string `json:"ciphertext"`
}

type ErrorCode

type ErrorCode string
const (
	ErrValidation ErrorCode = "validation_error"
	ErrPermission ErrorCode = "permission_denied"
	ErrNotFound   ErrorCode = "not_found"
	ErrConflict   ErrorCode = "conflict"
	ErrIntegrity  ErrorCode = "integrity_error"
	ErrCapacity   ErrorCode = "capacity_exceeded"
	ErrRateLimit  ErrorCode = "rate_limited"
)

type EventPage

type EventPage struct {
	Nodes []Node
	// Root is the current history tip captured atomically with this page. In the
	// append-only linear history, a root captured from the first page remains a
	// reachable replay boundary even if later page reads report a newer root.
	Root string
	// HasMore reports whether more events followed this page at the time of this
	// read. A concurrent append can change it on a later request.
	HasMore bool
}

type EventPayload

type EventPayload struct {
	EventID string
	Payload []byte
}

EventPayload is a plaintext payload retrieved by opaque event identity. The identity is currently the event's content hash; callers must treat it as an opaque value and must not derive storage paths from it.

type KeyMigrationInfo

type KeyMigrationInfo struct {
	SourceRoot      string            `json:"source_root"`
	DestinationRoot string            `json:"destination_root"`
	Nodes           int               `json:"nodes"`
	NamedRefs       int               `json:"named_refs"`
	HashMap         map[string]string `json:"hash_map"`
}

KeyMigrationInfo describes an offline migration to a new data key. Node hashes necessarily change because the ciphertext is content-addressed.

type Node

type Node struct {
	Schema        int               `json:"schema"`
	Hash          string            `json:"hash"`
	Type          string            `json:"type"`
	EntityID      string            `json:"entity_id,omitempty"`
	Parents       []string          `json:"parents"`
	Payload       json.RawMessage   `json:"payload,omitempty"`
	SealedPayload *EncryptedPayload `json:"sealed_payload,omitempty"`
	Actor         string            `json:"actor"`
	Role          string            `json:"role"`
	Command       string            `json:"command"`
	CreatedAt     time.Time         `json:"created_at"`
	RequestID     string            `json:"request_id,omitempty"`
	RequestHash   string            `json:"request_hash,omitempty"`
}

type SnapshotInfo

type SnapshotInfo struct {
	Path      string    `json:"path"`
	Root      string    `json:"root"`
	CreatedAt time.Time `json:"created_at"`
	Nodes     int       `json:"nodes"`
}

type Store

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

func Open

func Open(dir string) (*Store, error)

func OpenStore

func OpenStore(dir string) (*Store, error)

OpenStore is for local development and compatibility. It creates or reads a data key inside the store directory. Production and hosted processes must use OpenStoreWithDataKey so snapshots and storage do not share a key location.

func OpenStoreWithDataKey

func OpenStoreWithDataKey(dir, encodedKey string) (*Store, error)

OpenStoreWithDataKey opens a store with an explicit base64- or hex-encoded 32-byte key. Host processes should use this instead of the local key fallback.

func (*Store) Append

func (s *Store) Append(ctx Context, opts AppendOptions) (string, error)

func (*Store) AppendAt

func (s *Store) AppendAt(ctx Context, opts AppendOptions, expectedRoot string) (string, error)

AppendAt appends only when expectedRoot is still the current root. Passing an empty expectedRoot is how a caller safely creates the first node in a store.

func (*Store) AppendIdempotent

func (s *Store) AppendIdempotent(ctx Context, opts AppendOptions, expectedRoot, requestID, requestHash string, equivalentHashes ...string) (string, bool, error)

AppendIdempotent combines optimistic concurrency with a durable request ID. A retry of the same request returns its original node even if newer nodes have since been appended. Reusing a request ID for different content is rejected. equivalentHashes are older spellings of requestHash that also count as the same content, so requests recorded under a previous hashing rule still replay.

func (*Store) AuditLog

func (s *Store) AuditLog() ([]Node, error)

func (*Store) Close

func (s *Store) Close() error

Close releases the store's process-wide writer lock. A Store must not be used after Close. Close is safe to call more than once.

func (*Store) ContainsRoot

func (s *Store) ContainsRoot(root string) (bool, error)

ContainsRoot reports whether root is an ancestor of, or equal to, the current root. It lets an operator prove that a previously pinned tip remains in the live linear history and therefore detect a volume-level rollback.

func (*Store) CreateSnapshot

func (s *Store) CreateSnapshot(dir string) (SnapshotInfo, error)

CreateSnapshot creates a timestamped snapshot in dir. The filename and manifest timestamp are derived from the same store clock reading.

func (*Store) CurrentRoot

func (s *Store) CurrentRoot() (string, error)

func (*Store) Dir

func (s *Store) Dir() string

func (*Store) EventPage

func (s *Store) EventPage(after string, limit int) (EventPage, error)

EventPage returns at most limit events in oldest-to-newest order without materializing the complete history. The after hash is exclusive. Root and Nodes are read under the same lock, so clients can capture the first page's Root and stop exactly when that event is reached even if concurrent appends advance the root between page requests.

func (*Store) EventPayloads

func (s *Store) EventPayloads(eventIDs []string, observedRoot string) ([]EventPayload, error)

EventPayloads retrieves and authenticates payloads in request order. Every event must belong to this store and be at or before observedRoot. The caller owns batch and response-size limits. Membership and boundary checks are atomic under the store read lock; immutable node reads and decryption happen after releasing it so readers do not delay appends.

func (*Store) EventPayloadsBounded

func (s *Store) EventPayloadsBounded(eventIDs []string, observedRoot string, maxBytes int64) ([]EventPayload, error)

EventPayloadsBounded applies a cumulative plaintext byte limit while decrypting, before retaining an oversized batch in memory.

func (*Store) MetadataEventPageAt

func (s *Store) MetadataEventPageAt(after, observedRoot string, limit int) (EventPage, error)

MetadataEventPageAt returns a metadata-only page bounded by observedRoot. When observedRoot is empty, the current root is captured. Supplying the captured root on later calls prevents concurrent appends from changing the scan's contents or has-more result.

Metadata reads deliberately do not authenticate or decrypt sealed payloads. Payload integrity is checked only by EventPayloads, NodePayload, or VerifyAll, so damage to an unselected payload cannot make metadata replay unavailable.

func (*Store) MigrateDataKey

func (s *Store) MigrateDataKey(destinationDir, newEncodedKey string) (result KeyMigrationInfo, returnErr error)

MigrateDataKey decrypts the complete source history and writes an equivalent history to a new, previously nonexistent directory using newEncodedKey. The source is held read-only for the duration. Callers must keep the service offline so the returned destination is a complete cutover point. If only the final destination close fails, the completed destination is preserved and the populated result is returned alongside the error for operator verification.

func (*Store) NamedRef

func (s *Store) NamedRef(name string) (string, error)

func (*Store) NodePath

func (s *Store) NodePath(hash string) string

func (*Store) NodePayload

func (s *Store) NodePayload(node Node) ([]byte, error)

func (*Store) NodesFromRoot

func (s *Store) NodesFromRoot(root string) ([]Node, error)

func (*Store) SetClock

func (s *Store) SetClock(now func() time.Time)

func (*Store) Snapshot

func (s *Store) Snapshot(dest string) (SnapshotInfo, error)

Snapshot writes a consistent gzip-compressed tar archive containing all encrypted nodes and refs. Encryption keys are deliberately excluded so a copied backup is not sufficient to decrypt the data.

func (*Store) SnapshotSizeEstimate

func (s *Store) SnapshotSizeEstimate() (uint64, error)

SnapshotSizeEstimate returns a conservative estimate of the temporary free space needed to create a gzip snapshot. This intentionally scans only files that are included in snapshots and is used only by the admin endpoint.

func (*Store) VerifyAll

func (s *Store) VerifyAll() (root string, nodes int, err error)

VerifyAll verifies every node and authenticates every encrypted payload without materializing a second in-memory copy of the history.

func (*Store) VerifyHead

func (s *Store) VerifyHead() error

VerifyHead verifies that the current root points to a complete, correctly addressed node. It is intentionally constant-time with respect to history length and is suitable for readiness probes.

func (*Store) WriteNamedRef

func (s *Store) WriteNamedRef(name string, root string) error

func (*Store) WriteNamedRefAt

func (s *Store) WriteNamedRefAt(name, root, expectedRoot string) error

WriteNamedRefAt updates a named ref only when its current value exactly matches expectedRoot. An empty expectedRoot creates a ref only when it does not already exist.

Directories

Path Synopsis
Package client is a small Go client for the Stellar Jay HTTP API described in docs/api.md.
Package client is a small Go client for the Stellar Jay HTTP API described in docs/api.md.
cmd
stellarjay-mcp command
Command stellarjay-mcp serves the AvianSuite MCP tools over stdio for one Stellar Jay store, for MCP clients that launch a local command.
Command stellarjay-mcp serves the AvianSuite MCP tools over stdio for one Stellar Jay store, for MCP clients that launch a local command.
Package facts folds business-fact events into current state, following the payload conventions in llm.md.
Package facts folds business-fact events into current state, following the payload conventions in llm.md.
Package mcp is the AvianSuite MCP server for a Stellar Jay store.
Package mcp is the AvianSuite MCP server for a Stellar Jay store.

Jump to

Keyboard shortcuts

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