Skip to main content

Overview

The store package defines the storage abstraction for Anubis. Storage backends persist challenge data, DNS cache entries, and other temporary state.

Interface

Interface

Defines the storage operations that Anubis requires.
type Interface interface {
	// Delete removes a value from the store by key
	Delete(ctx context.Context, key string) error

	// Get returns the value of a key if it exists and hasn't expired
	Get(ctx context.Context, key string) ([]byte, error)

	// Set puts a value into the store with expiration
	Set(ctx context.Context, key string, value []byte, expiry time.Duration) error

	// IsPersistent returns true if data survives restarts
	IsPersistent() bool
}

Delete

Removes a key from the store.
Delete(ctx context.Context, key string) error
ctx
context.Context
required
Context for cancellation and timeouts
key
string
required
Key to delete
error
error
Returns ErrNotFound if key doesn’t exist, or other error on failure
Example
err := store.Delete(ctx, "challenge:abc123")
if errors.Is(err, store.ErrNotFound) {
	// Key already gone
}
See lib/store/interface.go:33

Get

Retrieves a value from the store.
Get(ctx context.Context, key string) ([]byte, error)
ctx
context.Context
required
Context for cancellation and timeouts
key
string
required
Key to retrieve
value
[]byte
Raw byte value stored at the key
error
error
Returns ErrNotFound if key doesn’t exist or has expired
Example
data, err := store.Get(ctx, "challenge:abc123")
if err != nil {
	if errors.Is(err, store.ErrNotFound) {
		// Challenge expired or doesn't exist
	}
	return err
}
// Use data...
See lib/store/interface.go:36

Set

Stores a value with automatic expiration.
Set(ctx context.Context, key string, value []byte, expiry time.Duration) error
ctx
context.Context
required
Context for cancellation and timeouts
key
string
required
Key to store value under
value
[]byte
required
Raw byte data to store
expiry
time.Duration
required
Time until the value expires and is automatically deleted
error
error
Error if storage operation fails
Example
import "time"

// Store challenge for 30 minutes
err := store.Set(ctx, "challenge:abc123", challengeData, 30*time.Minute)
if err != nil {
	return fmt.Errorf("failed to store challenge: %w", err)
}
See lib/store/interface.go:39

IsPersistent

Indicates whether the storage backend persists data across restarts.
IsPersistent() bool
persistent
bool
  • true: Data survives process restarts (bbolt, valkey, s3)
  • false: Data is volatile and lost on restart (memory)
Usage: Anubis uses this to warn administrators when using volatile storage in production.
if !store.IsPersistent() {
	log.Warn("Using volatile storage backend - challenges lost on restart")
}
See lib/store/interface.go:44

Generic Wrapper

JSON

Type-safe wrapper for JSON serialization/deserialization.
type JSON[T any] struct {
	Underlying Interface
	Prefix     string
}
Underlying
Interface
Base storage backend
Prefix
string
Optional key prefix for namespacing (e.g., “challenge:”, “dronebl:“)

Get

Retrieves and unmarshals a typed value.
func (j *JSON[T]) Get(ctx context.Context, key string) (T, error)
ctx
context.Context
required
Context
key
string
required
Key to retrieve (prefix automatically added)
value
T
Unmarshaled value of type T
error
error
Returns ErrNotFound, ErrCantDecode, or underlying error
See lib/store/interface.go:62-78

Set

Marshals and stores a typed value.
func (j *JSON[T]) Set(ctx context.Context, key string, value T, expiry time.Duration) error
ctx
context.Context
required
Context
key
string
required
Key to store under (prefix automatically added)
value
T
required
Value to marshal and store
expiry
time.Duration
required
Expiration duration
error
error
Returns ErrCantEncode or underlying error
See lib/store/interface.go:80-95

Delete

Deletes a key from the store.
func (j *JSON[T]) Delete(ctx context.Context, key string) error
ctx
context.Context
required
Context
key
string
required
Key to delete (prefix automatically added)
error
error
Error if deletion fails
See lib/store/interface.go:54-60
Example Usage
import (
	"context"
	"time"
	"github.com/TecharoHQ/anubis/lib/store"
	"github.com/TecharoHQ/anubis/lib/challenge"
)

type ChallengeStore struct {
	store *store.JSON[challenge.Challenge]
}

func NewChallengeStore(backend store.Interface) *ChallengeStore {
	return &ChallengeStore{
		store: &store.JSON[challenge.Challenge]{
			Underlying: backend,
			Prefix:     "challenge:",
		},
	}
}

func (cs *ChallengeStore) Save(ctx context.Context, chall *challenge.Challenge) error {
	return cs.store.Set(ctx, chall.ID, *chall, 30*time.Minute)
}

func (cs *ChallengeStore) Load(ctx context.Context, id string) (*challenge.Challenge, error) {
	chall, err := cs.store.Get(ctx, id)
	if err != nil {
		return nil, err
	}
	return &chall, nil
}

Registry

Factory

Interface for storage backend factories.
type Factory interface {
	Build(ctx context.Context, config json.RawMessage) (Interface, error)
	Valid(config json.RawMessage) error
}

Build

Constructs a storage backend from configuration.
Build(ctx context.Context, config json.RawMessage) (Interface, error)
ctx
context.Context
required
Context for initialization
config
json.RawMessage
required
Backend-specific configuration (from policy YAML)
store
Interface
Initialized storage backend
error
error
Configuration or initialization error
See lib/store/registry.go:16

Valid

Validates configuration without building the backend.
Valid(config json.RawMessage) error
config
json.RawMessage
required
Configuration to validate
error
error
Validation error if configuration is invalid
See lib/store/registry.go:17

Register

Registers a storage backend factory.
func Register(name string, impl Factory)
name
string
required
Unique backend name (e.g., “memory”, “bbolt”, “valkey”)
impl
Factory
required
Factory implementation
Example
func init() {
	store.Register("mystore", &MyStoreFactory{)
}
See lib/store/registry.go:20-24

Get

Retrieves a registered storage factory.
func Get(name string) (Factory, bool)
name
string
required
Backend name
factory
Factory
The storage factory
ok
bool
True if backend is registered
Example
factory, ok := store.Get("bbolt")
if !ok {
	log.Fatal("bbolt backend not available")
}
See lib/store/registry.go:27-32

Methods

Returns all registered backend names.
func Methods() []string
backends
[]string
Sorted list of registered backend names
Example
for _, backend := range store.Methods() {
	fmt.Println("Available backend:", backend)
}
See lib/store/registry.go:34-43

Built-in Implementations

memory

In-memory storage using a concurrent decay map. Characteristics:
  • Non-persistent (IsPersistent() = false)
  • Fast: O(1) operations
  • Automatic cleanup every 5 minutes
  • Not suitable for multi-instance deployments
Configuration:
store:
  backend: memory
Implementation: lib/store/memory/memory.go:25-78

bbolt

Embedded key-value database using bbolt. Characteristics:
  • Persistent (IsPersistent() = true)
  • Single-writer (file locking)
  • Automatic cleanup every hour
  • Good for single-instance deployments
Storage format:
  • Each key gets its own bucket
  • Buckets contain: data (value) and expiry (RFC3339Nano timestamp)
Configuration:
store:
  backend: bbolt
  parameters:
    path: /var/lib/anubis/data.db
    mode: 0600
Implementation: lib/store/bbolt/bbolt.go:38-171

valkey

Redis/Valkey client for distributed storage. Characteristics:
  • Persistent (IsPersistent() = true)
  • Multi-instance safe
  • Native TTL support (no cleanup needed)
  • Recommended for production
Configuration:
store:
  backend: valkey
  parameters:
    addrs:
      - redis.example.com:6379
    password: "secret"
    db: 0
    
    # Optional: cluster mode
    cluster: true
    
    # Optional: sentinel mode
    sentinel:
      master_name: mymaster
      addrs:
        - sentinel1.example.com:26379
        - sentinel2.example.com:26379
Implementation: lib/store/valkey/valkey.go:11-48

s3api

S3-compatible object storage backend. Characteristics:
  • Persistent (IsPersistent() = true)
  • Multi-instance safe
  • Higher latency than Redis
  • Good for low-traffic deployments
Configuration:
store:
  backend: s3api
  parameters:
    endpoint: s3.amazonaws.com
    region: us-east-1
    bucket: anubis-challenges
    access_key: AKIAIOSFODNN7EXAMPLE
    secret_key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
    use_path_style: false
Implementation: lib/store/s3api/s3api.go

Errors

var (
	ErrNotFound   = errors.New("store: key not found")
	ErrCantDecode = errors.New("store: can't decode value")
	ErrCantEncode = errors.New("store: can't encode value")
	ErrBadConfig  = errors.New("store: configuration is invalid")
)
ErrNotFound
error
Key does not exist or has expired
ErrCantDecode
error
Failed to unmarshal stored value
ErrCantEncode
error
Failed to marshal value for storage
ErrBadConfig
error
Backend configuration is invalid
See lib/store/interface.go:11-26

Implementing a Custom Backend

package mystore

import (
	"context"
	"encoding/json"
	"time"
	"github.com/TecharoHQ/anubis/lib/store"
)

type MyStore struct {
	// Your fields here
}

func (s *MyStore) Get(ctx context.Context, key string) ([]byte, error) {
	// Implementation
	return nil, store.ErrNotFound
}

func (s *MyStore) Set(ctx context.Context, key string, value []byte, expiry time.Duration) error {
	// Implementation
	return nil
}

func (s *MyStore) Delete(ctx context.Context, key string) error {
	// Implementation
	return store.ErrNotFound
}

func (s *MyStore) IsPersistent() bool {
	return true
}

type factory struct{}

func (factory) Build(ctx context.Context, cfg json.RawMessage) (store.Interface, error) {
	// Parse config and construct MyStore
	return &MyStore{}, nil
}

func (factory) Valid(cfg json.RawMessage) error {
	// Validate configuration
	return nil
}

func init() {
	store.Register("mystore", factory{)
}

Build docs developers (and LLMs) love