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

stellarjay

package module
v0.7.1 Latest Latest
Warning

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

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

README

Stellar Jay logo

Stellar Jay

Safe write access for AI agents. Stellar Jay is where your agents keep business data. Every change is kept, attributed to the agent that made it, and can be undone. Think of it as Git for your business data.

  • Hosted: AvianSuite runs Stellar Jay for you. $20/month per store, with a 14-day free trial.
  • Self-hosted: free and open source under the AGPL. The quick start below gets a store running on your machine in a few minutes.

Agent setup

Connect an agent with the MCP server. On AvianSuite, add the remote server:

# Claude Code
claude mcp add --transport http aviansuite https://mcp.aviansuite.com/mcp
// Cursor: .cursor/mcp.json
{"mcpServers": {"aviansuite": {"url": "https://mcp.aviansuite.com/mcp"}}}

In Claude and ChatGPT, add a custom connector with the same URL.

For a self-hosted store, run the local server instead:

{
  "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. Tools and details: docs/mcp.md.

Why

Clients want agents that do the work, not just read about it. But when an agent writes straight into a CRM or ticketing system, one bad decision or runaway loop can overwrite or delete records, and there is often no way back.

Stellar Jay is built so that can't happen:

  • Nothing is overwritten or deleted. A correction or retraction is a new entry, and the original stays in history.
  • Every change has a name on it. Each agent gets its own token, so you can see exactly which agent changed what, and when.
  • Any change can be undone. Roll back everything one agent did in a time window, with a preview first.
  • History is tamper-evident. If anyone rewrites or removes past entries, it shows.
  • Retries are safe. An agent that retries after a timeout doesn't create duplicates, and a write based on stale information is refused.
  • Your data stays flexible. Facts are JSON, so new fields and new kinds of records need no migrations.

Stellar Jay records what agents say happened. It doesn't decide whether a fact is true; it makes sure a wrong one stays visible and correctable.

Who it's for

Developers, consultants, and small teams moving from read-only copilots to agents that are allowed to act: operations, accounting, approvals, support, and other work where the data matters. Each store serves one organization. Many agents and apps can share it.

Quick start (self-hosted)

Requires Go 1.22 or later.

1. Install and create secrets.

go install github.com/kyle-visner/stellarjay/cmd/stellarjay-server@latest
stellarjay-server init ./secrets

init creates a data encryption key and prints an admin, writer, and reader token once. Save them in a password manager.

2. Start the server.

export STELLARJAY_DATA_DIR=./data
export STELLARJAY_DATA_KEY_FILE=./secrets/data_key
export STELLARJAY_AUTH_FILE=./secrets/auth.json
stellarjay-server serve

It listens on 127.0.0.1:8080.

3. Record a fact. In another terminal:

export STELLARJAY_URL=http://127.0.0.1:8080
export STELLARJAY_TOKEN='the-writer-token'

curl -fsS -X POST "$STELLARJAY_URL/v1/events" \
  -H "Authorization: Bearer $STELLARJAY_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: first-fact" \
  --data '{
    "type": "business.fact",
    "entity_id": "customer-42",
    "command": "fact assert",
    "payload": {"predicate": "primary_contact", "value": "Ada Lovelace"},
    "expected_root": ""
  }'

expected_root is empty only for the first entry in a new store. After that, read the current value from GET /v1/root and send it with each write, so a write based on stale information is refused. The API guide covers reading history, pagination, named checkpoints, and snapshots.

4. Connect an agent. Point the MCP server at your store:

go install github.com/kyle-visner/stellarjay/cmd/stellarjay-mcp@latest

Then use the self-hosted config from Agent setup with STELLARJAY_URL=http://127.0.0.1:8080 and your writer token. Agents that don't use MCP can read $STELLARJAY_URL/llm.txt, which explains how to work with the store.

Deploy to a server

For a production store with HTTPS, you need a Linux host with Docker Compose, ports 80 and 443 open, and a DNS record pointing a domain at the host.

git clone https://github.com/kyle-visner/stellarjay.git
cd stellarjay
cp .env.example .env
# Edit .env and set STELLARJAY_DOMAIN.

go run ./cmd/stellarjay-server init ./secrets

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

init will not replace existing secrets. Read the operations runbook for backups, token rotation, and upgrades, and the security model before storing sensitive data. Or skip all of this and use AvianSuite.

Documentation

Development

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. Hosted Stellar Jay is available from AvianSuite.

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