Keysmith

Custom Store

Implementing a custom Keysmith store backend.

Keysmith defines a composite store.Store interface that embeds five subsystem store interfaces plus lifecycle methods. Implement this interface to add a new storage backend.

Composite store interface

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

type Store interface {
    Keys() key.Store
    Policies() policy.Store
    Scopes() scope.Store
    Usage() usage.Store
    Rotations() rotation.Store

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

The accessor pattern (Keys(), Policies(), etc.) avoids method name collisions when composing five store interfaces into one.

Implementing a custom store

Step 1: Implement each subsystem store

Start with the key store, which is the most critical:

type MyKeyStore struct {
    // your backend connection
}

func (s *MyKeyStore) Create(ctx context.Context, k *key.Key) error {
    // Insert key into your backend
}

func (s *MyKeyStore) GetByHash(ctx context.Context, hash string) (*key.Key, error) {
    // Look up key by SHA-256 hash
}

// ... implement remaining methods

Step 2: Compose into a Store

type MyStore struct {
    keys      *MyKeyStore
    policies  *MyPolicyStore
    scopes    *MyScopeStore
    usage     *MyUsageStore
    rotations *MyRotationStore
}

func (s *MyStore) Keys() key.Store         { return s.keys }
func (s *MyStore) Policies() policy.Store   { return s.policies }
func (s *MyStore) Scopes() scope.Store      { return s.scopes }
func (s *MyStore) Usage() usage.Store       { return s.usage }
func (s *MyStore) Rotations() rotation.Store { return s.rotations }

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

Step 3: Use with the engine

eng, err := keysmith.NewEngine(keysmith.WithStore(&MyStore{...}))

Key store interface (critical path)

The key.Store.GetByHash method is the hot path for validation. Ensure it is optimized for O(1) or O(log n) lookup:

type Store interface {
    Create(ctx context.Context, k *Key) error
    GetByID(ctx context.Context, id id.KeyID) (*Key, error)
    GetByHash(ctx context.Context, hash string) (*Key, error)
    List(ctx context.Context, filter *ListFilter) ([]*Key, error)
    UpdateState(ctx context.Context, id id.KeyID, state State) error
    UpdateLastUsed(ctx context.Context, id id.KeyID, t time.Time) error
    Delete(ctx context.Context, id id.KeyID) error
}

Testing your store

Use the existing engine tests as a harness. Replace memory.New() with your custom store:

func TestMyStore(t *testing.T) {
    store := NewMyStore(...)
    eng, err := keysmith.NewEngine(keysmith.WithStore(store))
    require.NoError(t, err)

    ctx := keysmith.WithTenant(context.Background(), "test-app", "test-tenant")

    // Run through the key lifecycle
    result, err := eng.CreateKey(ctx, &keysmith.CreateKeyInput{
        Name:   "Test Key",
        Prefix: "sk",
    })
    require.NoError(t, err)

    vr, err := eng.ValidateKey(ctx, result.RawKey)
    require.NoError(t, err)
    assert.Equal(t, "test-tenant", vr.Key.TenantID)
}

On this page