Authsome
1.x
Docs/Authsome/Memory Store
Open

Reading7 min
Updated31 Jul 2026
Sourcev1/stores/memory.mdx

The memory store (store/memory) is a fully in-memory implementation of store.Store. All data is held in Go maps protected by a sync.RWMutex, making it safe for concurrent access. No external dependencies or configuration are required — creating a store is a single function call.

Installation01

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

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

Creating a store02

memStore := memory.New()

memory.New() returns a *memory.Store with all internal maps initialised and ready to use. The zero value is not valid — always use New().

Wiring into the engine03

package main

import (
    "context"
    "log"
    "net/http"
    "time"

    "github.com/xraph/authsome"
    "github.com/xraph/authsome/plugins/password"
    "github.com/xraph/authsome/store/memory"
)

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

    // In-memory store: no database required.
    memStore := memory.New()

    eng, err := authsome.New(
        authsome.WithStore(memStore),
        authsome.WithPlugin(password.New()),
        authsome.WithConfig(authsome.Config{
            AppID:    "testapp",
            BasePath: "/v1/auth",
            Session: authsome.SessionConfig{
                TokenTTL:        1 * time.Hour,
                RefreshTokenTTL: 24 * time.Hour,
            },
        }),
    )
    if err != nil {
        log.Fatal(err)
    }
    defer eng.Stop(ctx)

    if err := eng.Start(ctx); err != nil {
        log.Fatal(err)
    }

    mux := http.NewServeMux()
    eng.RegisterRoutes(mux)
    log.Fatal(http.ListenAndServe(":8080", mux))
}

Implemented interfaces04

The memory store satisfies every interface in the store.Store composite:

InterfaceInternal map
user.Storemap[string]*user.User (keyed by TypeID string)
session.Storemap[string]*session.Session (keyed by TypeID string)
account.Storemap[string]*account.Verification, map[string]*account.PasswordReset (keyed by token)
app.Storemap[string]*app.App (keyed by TypeID string)
organization.Storemap[string]*organization.Organization, map[string]*organization.Member, map[string]*organization.Invitation, map[string]*organization.Team
device.Storemap[string]*device.Device (keyed by TypeID string)
webhook.Storemap[string]*webhook.Webhook (keyed by TypeID string)
notification.Storemap[string]*notification.Notification (keyed by TypeID string)
apikey.Storemap[string]*apikey.APIKey (keyed by TypeID string)
environment.Storemap[string]*environment.Environment (keyed by TypeID string)
formconfig.Storemap[string]*formconfig.FormConfig (keyed by TypeID string)
formconfig.BrandingStoremap[string]*formconfig.BrandingConfig (keyed by TypeID string)
appsessionconfig.Storemap[string]*appsessionconfig.Config (keyed by AppID string)

The compile-time assertion at the top of the package guarantees this:

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

Concurrency safety05

All methods acquire either a read lock (sync.RWMutex.RLock) or a write lock (sync.RWMutex.Lock) before accessing internal maps. Read operations (all Get* and List* methods) acquire a read lock, allowing multiple concurrent readers. Write operations acquire an exclusive lock.

This makes the memory store safe for use in concurrent test scenarios where multiple goroutines sign in and sign out simultaneously.

Lifecycle methods06

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

The memory store has no schema to create and no connection to verify, so all lifecycle methods succeed immediately.

Lookup behaviour07

User lookups

The memory store scans all users linearly for lookups by email, phone, and username. This is acceptable for test datasets but would not scale to production volumes:

func (s *Store) GetUserByEmail(_ context.Context, appID id.AppID, email string) (*user.User, error) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    for _, u := range s.users {
        if u.AppID.String() == appID.String() && u.Email == email {
            return u, nil
        }
    }
    return nil, store.ErrNotFound
}

In production stores (PostgreSQL, SQLite, MongoDB), this lookup uses an indexed column and is O(log n).

Session token lookups

Session tokens and refresh tokens are also looked up by linear scan:

func (s *Store) GetSessionByToken(_ context.Context, token string) (*session.Session, error) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    for _, sess := range s.sessions {
        if sess.Token == token {
            return sess, nil
        }
    }
    return nil, store.ErrNotFound
}

For test suites with hundreds of sessions, this remains fast. For performance benchmarking of the authentication hot path, use a real database backend.

RBAC in the memory store08

The memory store also implements rbac.Store and includes a full in-memory RBAC evaluator with hierarchical role support. HasPermission walks the role parent chain (up to depth 20 to prevent infinite loops) to evaluate inherited permissions:

// HasPermission checks if userID has action+resource permission,
// including permissions inherited from parent roles.
func (s *Store) HasPermission(ctx context.Context, userID string, action, resource string) (bool, error)

This allows you to test complex RBAC scenarios — role hierarchies, wildcard permissions (*), resource-level checks — without any database setup.

Error handling09

The memory store returns store.ErrNotFound for all "not found" cases, matching the behaviour of all production store backends:

var ErrNotFound = errors.New("authsome: not found")

Use errors.Is(err, store.ErrNotFound) to detect missing entities consistently across all backends.

Using the memory store in tests10

package auth_test

import (
    "context"
    "testing"
    "time"

    "github.com/stretchr/testify/require"
    "github.com/xraph/authsome"
    "github.com/xraph/authsome/plugins/password"
    "github.com/xraph/authsome/store/memory"
)

func newTestEngine(t *testing.T) *authsome.Engine {
    t.Helper()

    memStore := memory.New()

    eng, err := authsome.New(
        authsome.WithStore(memStore),
        authsome.WithPlugin(password.New()),
        authsome.WithConfig(authsome.Config{
            AppID:    "testapp",
            BasePath: "/v1/auth",
            Session: authsome.SessionConfig{
                TokenTTL:        5 * time.Minute,
                RefreshTokenTTL: 1 * time.Hour,
            },
            Password: authsome.PasswordConfig{
                MinLength: 8,
            },
        }),
    )
    require.NoError(t, err)

    ctx := context.Background()
    require.NoError(t, eng.Start(ctx))
    t.Cleanup(func() { eng.Stop(ctx) })

    return eng
}

func TestSignUpAndSignIn(t *testing.T) {
    eng := newTestEngine(t)
    ctx := context.Background()

    // Sign up
    signupResult, err := eng.SignUp(ctx, &authsome.SignUpRequest{
        Email:    "[email protected]",
        Password: "securepassword123",
    })
    require.NoError(t, err)
    require.NotEmpty(t, signupResult.Token)

    // Sign in
    signinResult, err := eng.SignIn(ctx, &authsome.SignInRequest{
        Email:    "[email protected]",
        Password: "securepassword123",
    })
    require.NoError(t, err)
    require.Equal(t, signupResult.User.ID, signinResult.User.ID)
}

Swapping to a production backend11

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

// Testing
memStore := memory.New()

// Production (PostgreSQL)
pgStore := postgres.New(grove.Open(pgdriver.New(os.Getenv("DATABASE_URL"))))

Pass either value to authsome.WithStore(...) — no other code changes are required.

When to use12

  • Unit tests — No setup, no teardown, zero dependencies. Every test starts with a clean empty store.

  • Integration tests — Test full request/response cycles without a database.

  • CI pipelines — Fast, deterministic, no database container required.

  • Local prototyping — Explore Authsome's API without configuring any infrastructure.

When not to use13

  • Production — All data is lost on process restart. There is no persistence.

  • Multi-instance deployments — Each process has an isolated store. Sign-in on instance A cannot be validated on instance B.

  • Performance benchmarking — Linear scans do not reflect production database performance characteristics.