Chronicle

Custom Store

Implement your own Chronicle store backend.

To connect Chronicle to a storage backend not covered by the built-in implementations (Postgres, Bun, SQLite, Memory), implement the store.Store composite interface.

The composite interface

store.Store embeds 6 sub-store interfaces plus 3 lifecycle methods — approximately 36 methods in total:

type Store interface {
    audit.Store        // 9 methods: Append, AppendBatch, Get, Query, Aggregate, ByUser, Count, LastSequence, LastHash
    stream.Store       // 5 methods: CreateStream, GetStream, GetStreamByScope, ListStreams, UpdateStreamHead
    verify.Store       // 2 methods: EventRange, Gaps
    erasure.Store      // ~5 methods: SaveErasure, GetErasure, ListErasures, MarkEventsErased, ListSubjectEvents
    retention.Store    // ~6 methods: SavePolicy, GetPolicy, ListPolicies, DeletePolicy, EventsOlderThan, SaveArchive, ListArchives
    compliance.ReportStore // 4 methods: SaveReport, GetReport, ListReports, DeleteReport

    Migrate(ctx context.Context) error
    Ping(ctx context.Context) error
    Close() error
}

Sub-interface reference

InterfacePackageKey methods
audit.StoreauditAppend, AppendBatch, Get, Query, Aggregate, ByUser, Count, LastSequence, LastHash
stream.StorestreamCreateStream, GetStream, GetStreamByScope, ListStreams, UpdateStreamHead
verify.StoreverifyEventRange, Gaps
erasure.StoreerasureSaveErasure, GetErasure, ListErasures, MarkEventsErased
retention.StoreretentionSavePolicy, GetPolicy, ListPolicies, DeletePolicy, EventsOlderThan, SaveArchive, ListArchives
compliance.ReportStorecomplianceSaveReport, GetReport, ListReports, DeleteReport

Scaffold

package mystore

import (
    "context"

    "github.com/xraph/chronicle/audit"
    "github.com/xraph/chronicle/compliance"
    "github.com/xraph/chronicle/erasure"
    "github.com/xraph/chronicle/id"
    "github.com/xraph/chronicle/retention"
    "github.com/xraph/chronicle/stream"
    "github.com/xraph/chronicle/verify"
)

// Store implements store.Store using your custom backend.
type Store struct {
    // your fields
}

// Compile-time check.
var _ interface {
    audit.Store
    stream.Store
    verify.Store
    erasure.Store
    retention.Store
    compliance.ReportStore
} = (*Store)(nil)

// ─── audit.Store ───────────────────────────────────────────────────────────────

func (s *Store) Append(ctx context.Context, event *audit.Event) error               { panic("not implemented") }
func (s *Store) AppendBatch(ctx context.Context, events []*audit.Event) error        { panic("not implemented") }
func (s *Store) Get(ctx context.Context, eventID id.ID) (*audit.Event, error)        { panic("not implemented") }
func (s *Store) Query(ctx context.Context, q *audit.Query) (*audit.QueryResult, error) { panic("not implemented") }
// ... (implement all methods)

// ─── Lifecycle ─────────────────────────────────────────────────────────────────

func (s *Store) Migrate(ctx context.Context) error { return nil }
func (s *Store) Ping(ctx context.Context) error    { return nil }
func (s *Store) Close() error                      { return nil }

Use the memory store as a fallback

During development, embed *memory.Store and promote all methods, then override only the ones you've implemented:

import "github.com/xraph/chronicle/store/memory"

type Store struct {
    *memory.Store // satisfies all unimplemented interfaces
    db            *mydb.Client
}

// Override only the methods you've implemented.
func (s *Store) Append(ctx context.Context, event *audit.Event) error {
    return s.db.Insert(ctx, event)
}

Error mapping

Map backend-specific "not found" errors to Chronicle sentinel errors so callers can use errors.Is consistently:

import (
    "errors"
    "github.com/xraph/chronicle"
)

func (s *Store) Get(ctx context.Context, eventID id.ID) (*audit.Event, error) {
    event, err := s.db.FindEvent(ctx, eventID.String())
    if errors.Is(err, mydb.ErrNotFound) {
        return nil, chronicle.ErrEventNotFound
    }
    return event, err
}

Wire with store.NewAdapter

After implementing your store, wrap it with store.NewAdapter before passing to chronicle.New:

import "github.com/xraph/chronicle/store"

myStore := mystore.New(cfg)
adapter := store.NewAdapter(myStore)
c, err := chronicle.New(chronicle.WithStore(adapter))

Or pass directly to the extension:

ext := extension.New(extension.WithStore(myStore))

Testing

Use the store/memory store as a reference implementation to validate your store's behaviour:

func TestMyStore(t *testing.T) {
    ctx := context.Background()
    s := mystore.NewTest()  // a clean in-memory instance for testing

    event := &audit.Event{
        Action:   "login",
        Resource: "session",
        Category: "auth",
        AppID:    "test",
    }
    if err := s.Append(ctx, event); err != nil {
        t.Fatal(err)
    }

    got, err := s.Get(ctx, event.ID)
    if err != nil {
        t.Fatal(err)
    }
    if got.Action != event.Action {
        t.Errorf("got action %q, want %q", got.Action, event.Action)
    }
}

On this page