> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/TecharoHq/Anubis/llms.txt
> Use this file to discover all available pages before exploring further.

# Store

> Storage backend interface and implementations

## 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.

```go theme={null}
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.

```go theme={null}
Delete(ctx context.Context, key string) error
```

<ParamField path="ctx" type="context.Context" required>
  Context for cancellation and timeouts
</ParamField>

<ParamField path="key" type="string" required>
  Key to delete
</ParamField>

<ResponseField name="error" type="error">
  Returns `ErrNotFound` if key doesn't exist, or other error on failure
</ResponseField>

**Example**

```go theme={null}
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.

```go theme={null}
Get(ctx context.Context, key string) ([]byte, error)
```

<ParamField path="ctx" type="context.Context" required>
  Context for cancellation and timeouts
</ParamField>

<ParamField path="key" type="string" required>
  Key to retrieve
</ParamField>

<ResponseField name="value" type="[]byte">
  Raw byte value stored at the key
</ResponseField>

<ResponseField name="error" type="error">
  Returns `ErrNotFound` if key doesn't exist or has expired
</ResponseField>

**Example**

```go theme={null}
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.

```go theme={null}
Set(ctx context.Context, key string, value []byte, expiry time.Duration) error
```

<ParamField path="ctx" type="context.Context" required>
  Context for cancellation and timeouts
</ParamField>

<ParamField path="key" type="string" required>
  Key to store value under
</ParamField>

<ParamField path="value" type="[]byte" required>
  Raw byte data to store
</ParamField>

<ParamField path="expiry" type="time.Duration" required>
  Time until the value expires and is automatically deleted
</ParamField>

<ResponseField name="error" type="error">
  Error if storage operation fails
</ResponseField>

**Example**

```go theme={null}
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.

```go theme={null}
IsPersistent() bool
```

<ResponseField name="persistent" type="bool">
  * `true`: Data survives process restarts (bbolt, valkey, s3)
  * `false`: Data is volatile and lost on restart (memory)
</ResponseField>

**Usage:**

Anubis uses this to warn administrators when using volatile storage in production.

```go theme={null}
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.

```go theme={null}
type JSON[T any] struct {
	Underlying Interface
	Prefix     string
}
```

<ParamField path="Underlying" type="Interface">
  Base storage backend
</ParamField>

<ParamField path="Prefix" type="string">
  Optional key prefix for namespacing (e.g., "challenge:", "dronebl:")
</ParamField>

#### Get

Retrieves and unmarshals a typed value.

```go theme={null}
func (j *JSON[T]) Get(ctx context.Context, key string) (T, error)
```

<ParamField path="ctx" type="context.Context" required>
  Context
</ParamField>

<ParamField path="key" type="string" required>
  Key to retrieve (prefix automatically added)
</ParamField>

<ResponseField name="value" type="T">
  Unmarshaled value of type T
</ResponseField>

<ResponseField name="error" type="error">
  Returns `ErrNotFound`, `ErrCantDecode`, or underlying error
</ResponseField>

See `lib/store/interface.go:62-78`

***

#### Set

Marshals and stores a typed value.

```go theme={null}
func (j *JSON[T]) Set(ctx context.Context, key string, value T, expiry time.Duration) error
```

<ParamField path="ctx" type="context.Context" required>
  Context
</ParamField>

<ParamField path="key" type="string" required>
  Key to store under (prefix automatically added)
</ParamField>

<ParamField path="value" type="T" required>
  Value to marshal and store
</ParamField>

<ParamField path="expiry" type="time.Duration" required>
  Expiration duration
</ParamField>

<ResponseField name="error" type="error">
  Returns `ErrCantEncode` or underlying error
</ResponseField>

See `lib/store/interface.go:80-95`

***

#### Delete

Deletes a key from the store.

```go theme={null}
func (j *JSON[T]) Delete(ctx context.Context, key string) error
```

<ParamField path="ctx" type="context.Context" required>
  Context
</ParamField>

<ParamField path="key" type="string" required>
  Key to delete (prefix automatically added)
</ParamField>

<ResponseField name="error" type="error">
  Error if deletion fails
</ResponseField>

See `lib/store/interface.go:54-60`

***

**Example Usage**

```go theme={null}
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.

```go theme={null}
type Factory interface {
	Build(ctx context.Context, config json.RawMessage) (Interface, error)
	Valid(config json.RawMessage) error
}
```

#### Build

Constructs a storage backend from configuration.

```go theme={null}
Build(ctx context.Context, config json.RawMessage) (Interface, error)
```

<ParamField path="ctx" type="context.Context" required>
  Context for initialization
</ParamField>

<ParamField path="config" type="json.RawMessage" required>
  Backend-specific configuration (from policy YAML)
</ParamField>

<ResponseField name="store" type="Interface">
  Initialized storage backend
</ResponseField>

<ResponseField name="error" type="error">
  Configuration or initialization error
</ResponseField>

See `lib/store/registry.go:16`

***

#### Valid

Validates configuration without building the backend.

```go theme={null}
Valid(config json.RawMessage) error
```

<ParamField path="config" type="json.RawMessage" required>
  Configuration to validate
</ParamField>

<ResponseField name="error" type="error">
  Validation error if configuration is invalid
</ResponseField>

See `lib/store/registry.go:17`

***

### Register

Registers a storage backend factory.

```go theme={null}
func Register(name string, impl Factory)
```

<ParamField path="name" type="string" required>
  Unique backend name (e.g., "memory", "bbolt", "valkey")
</ParamField>

<ParamField path="impl" type="Factory" required>
  Factory implementation
</ParamField>

**Example**

```go theme={null}
func init() {
	store.Register("mystore", &MyStoreFactory{)
}
```

See `lib/store/registry.go:20-24`

***

### Get

Retrieves a registered storage factory.

```go theme={null}
func Get(name string) (Factory, bool)
```

<ParamField path="name" type="string" required>
  Backend name
</ParamField>

<ResponseField name="factory" type="Factory">
  The storage factory
</ResponseField>

<ResponseField name="ok" type="bool">
  True if backend is registered
</ResponseField>

**Example**

```go theme={null}
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.

```go theme={null}
func Methods() []string
```

<ResponseField name="backends" type="[]string">
  Sorted list of registered backend names
</ResponseField>

**Example**

```go theme={null}
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:**

```yaml theme={null}
store:
  backend: memory
```

**Implementation:** `lib/store/memory/memory.go:25-78`

***

### bbolt

Embedded key-value database using [bbolt](https://github.com/etcd-io/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:**

```yaml theme={null}
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:**

```yaml theme={null}
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:**

```yaml theme={null}
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

```go theme={null}
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")
)
```

<ParamField path="ErrNotFound" type="error">
  Key does not exist or has expired
</ParamField>

<ParamField path="ErrCantDecode" type="error">
  Failed to unmarshal stored value
</ParamField>

<ParamField path="ErrCantEncode" type="error">
  Failed to marshal value for storage
</ParamField>

<ParamField path="ErrBadConfig" type="error">
  Backend configuration is invalid
</ParamField>

See `lib/store/interface.go:11-26`

***

## Implementing a Custom Backend

```go theme={null}
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{)
}
```

***

## Related Types

* [Anubis Server](/api/anubis) - Store configuration
* [Challenge](/api/challenge) - Challenge persistence
* [Policy](/api/policy) - Policy data storage
