Vault

Memory Store

In-memory store for development and testing with no persistence.

The memory store (herald/store/memory) is a fully in-memory implementation of store.Store. It keeps all data in Go maps protected by a sync.RWMutex, making it safe for concurrent access. No external dependencies, database connections, or migrations are required.

Installation

The memory store is included in the Herald module. No additional packages are needed.

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

Creating a Store

store := memory.New()

memory.New() returns a *memory.Store with all internal maps initialized and ready to use.

Constructor

func New() *Store

No arguments are needed. The store initializes maps for all 7 entity types:

  • providers -- map[string]*provider.Provider
  • templates -- map[string]*template.Template
  • versions -- map[string]*template.Version
  • messages -- map[string]*message.Message
  • notifications -- map[string]*inbox.Notification
  • preferences -- map[string]*preference.Preference (key: "appID:userID")
  • scopedConfigs -- map[string]*scope.Config (key: "appID:scope:scopeID")

Implemented Interfaces

The memory store satisfies all six subsystem store interfaces plus the three lifecycle methods that form the composite store.Store interface:

InterfacePackageMethods
provider.Storegithub.com/xraph/herald/providerCreateProvider, GetProvider, UpdateProvider, DeleteProvider, ListProviders, ListAllProviders
template.Storegithub.com/xraph/herald/templateCreateTemplate, GetTemplate, GetTemplateBySlug, UpdateTemplate, DeleteTemplate, ListTemplates, ListTemplatesByChannel, CreateVersion, GetVersion, UpdateVersion, DeleteVersion, ListVersions
message.Storegithub.com/xraph/herald/messageCreateMessage, GetMessage, UpdateMessageStatus, ListMessages
inbox.Storegithub.com/xraph/herald/inboxCreateNotification, GetNotification, DeleteNotification, MarkRead, MarkAllRead, UnreadCount, ListNotifications
preference.Storegithub.com/xraph/herald/preferenceGetPreference, SetPreference, DeletePreference
scope.Storegithub.com/xraph/herald/scopeGetScopedConfig, SetScopedConfig, DeleteScopedConfig, ListScopedConfigs

Compile-time interface check:

var _ store.Store = (*Store)(nil)

Concurrency Safety

All methods acquire either a read lock (RLock) or a write lock (Lock) on the internal sync.RWMutex. This makes the store safe for concurrent use from multiple goroutines, including parallel test execution.

Lifecycle Methods

MethodBehaviour
Migrate(ctx)No-op -- returns nil
Ping(ctx)Always returns nil
Close()No-op -- returns nil

The memory store has no schema to create and no connection to check, so all lifecycle methods succeed immediately. Calling Migrate is good practice for consistency with other backends.

Key Scheme

Entities are keyed using their TypeID string representation. Composite-keyed entities use colon-separated keys:

EntityKey format
Providers, Templates, Versions, Messages, NotificationsTypeID string (e.g., hpvd_...)
Preferences"appID:userID"
Scoped Configs"appID:scope:scopeID"

Template Version Association

When retrieving templates, the memory store automatically attaches associated versions by scanning the versions map for matching TemplateID values. Deleting a template also removes all its associated versions.

No Migration Needed

Since data is stored in Go maps, there is no schema to create or migrate. The Migrate method is a no-op that always returns nil.

Complete Example

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "github.com/xraph/herald"
    "github.com/xraph/herald/driver/email"
    "github.com/xraph/herald/id"
    "github.com/xraph/herald/provider"
    "github.com/xraph/herald/store/memory"
    "github.com/xraph/herald/template"
)

func main() {
    ctx := context.Background()

    // Create the in-memory store.
    store := memory.New()

    // Create a Herald instance with the memory store.
    h, err := herald.New(
        herald.WithStore(store),
        herald.WithDriver(&email.SMTPDriver{}),
    )
    if err != nil {
        log.Fatal(err)
    }

    // Create a provider.
    now := time.Now().UTC()
    p := &provider.Provider{
        ID:      id.NewProviderID(),
        AppID:   "myapp",
        Name:    "SMTP",
        Channel: "email",
        Driver:  "smtp",
        Credentials: map[string]string{
            "host": "smtp.example.com",
            "port": "587",
        },
        Enabled:   true,
        CreatedAt: now,
        UpdatedAt: now,
    }
    if err := store.CreateProvider(ctx, p); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Provider created: %s\n", p.ID)

    // Create a template.
    tmpl := &template.Template{
        ID:        id.NewTemplateID(),
        AppID:     "myapp",
        Slug:      "welcome",
        Name:      "Welcome Email",
        Channel:   "email",
        Category:  "transactional",
        Enabled:   true,
        CreatedAt: now,
        UpdatedAt: now,
    }
    if err := store.CreateTemplate(ctx, tmpl); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Template created: %s\n", tmpl.ID)

    _ = h
}

Swapping to a Persistent Backend

The memory store implements the same store.Store interface as all other backends. Switching to a production store is a single-line change:

// Development / testing
store := memory.New()

// Production (PostgreSQL)
store := postgres.New(db)

// Production (MongoDB)
store := mongo.New(db)

When to Use

  • Unit tests -- no setup, no teardown, zero dependencies.
  • Development -- quick iteration without running a database server.
  • CI pipelines -- fast, deterministic tests with no external services.
  • Prototyping -- explore Herald's API surface without infrastructure.

When Not to Use

  • Production -- data is lost on process restart.
  • Multi-instance deployments -- each process has its own isolated store.
  • Performance benchmarking -- behaviour characteristics differ from a real database.

On this page