Custom Store
Implement a custom storage backend by satisfying the composite store.Store interface.
Every Authsome storage backend implements a single Go interface. This guide shows how to build a custom store for your preferred database or storage layer.
The composite interface
Your store must implement the composite store.Store interface, which embeds 12 subsystem stores plus lifecycle methods:
import "github.com/xraph/authsome/store"
type Store interface {
user.Store
session.Store
account.Store
app.Store
organization.Store
device.Store
webhook.Store
notification.Store
apikey.Store
environment.Store
formconfig.Store
formconfig.BrandingStore
appsessionconfig.Store
Migrate(ctx context.Context, extraGroups ...*migrate.Group) error
Ping(ctx context.Context) error
Close() error
}Key subsystem interfaces
user.Store
type Store interface {
CreateUser(ctx context.Context, u *User) error
GetUser(ctx context.Context, userID id.UserID) (*User, error)
GetUserByEmail(ctx context.Context, email string) (*User, error)
UpdateUser(ctx context.Context, u *User) error
DeleteUser(ctx context.Context, userID id.UserID) error
ListUsers(ctx context.Context, filter *ListFilter) ([]*User, int64, error)
}session.Store
type Store interface {
CreateSession(ctx context.Context, s *Session) error
GetSession(ctx context.Context, sessionID id.SessionID) (*Session, error)
GetSessionByToken(ctx context.Context, token string) (*Session, error)
GetSessionByRefreshToken(ctx context.Context, refreshToken string) (*Session, error)
UpdateSession(ctx context.Context, s *Session) error
DeleteSession(ctx context.Context, sessionID id.SessionID) error
DeleteUserSessions(ctx context.Context, userID id.UserID) error
ListUserSessions(ctx context.Context, userID id.UserID) ([]*Session, error)
CountActiveSessions(ctx context.Context, userID id.UserID) (int64, error)
}account.Store
type Store interface {
CreateAccount(ctx context.Context, a *Account) error
GetAccountByProvider(ctx context.Context, provider, providerID string) (*Account, error)
GetAccountsByUserID(ctx context.Context, userID id.UserID) ([]*Account, error)
DeleteAccount(ctx context.Context, accountID id.AccountID) error
}organization.Store
type Store interface {
CreateOrganization(ctx context.Context, o *Organization) error
GetOrganization(ctx context.Context, orgID id.OrgID) (*Organization, error)
UpdateOrganization(ctx context.Context, o *Organization) error
DeleteOrganization(ctx context.Context, orgID id.OrgID) error
ListOrganizations(ctx context.Context, filter *ListFilter) ([]*Organization, int64, error)
ListUserOrganizations(ctx context.Context, userID id.UserID) ([]*Organization, error)
AddMember(ctx context.Context, m *Member) error
GetMember(ctx context.Context, memberID id.MemberID) (*Member, error)
UpdateMember(ctx context.Context, m *Member) error
RemoveMember(ctx context.Context, memberID id.MemberID) error
ListMembers(ctx context.Context, orgID id.OrgID) ([]*Member, error)
}Compile-time check
Use a compile-time assertion to verify your implementation satisfies all interfaces:
var _ store.Store = (*MyStore)(nil)This causes a compile error if any required method is missing, which is much faster than discovering it at runtime.
Migration handling
The Migrate method accepts optional extra migration groups. These come from plugins that define their own tables. Your store must run both its own core migrations and any extra groups passed in.
func (s *MyStore) Migrate(ctx context.Context, extraGroups ...*migrate.Group) error {
// Run core migrations first
if err := s.runCoreMigrations(ctx); err != nil {
return err
}
// Run plugin migration groups
for _, group := range extraGroups {
if err := group.Run(ctx, s.db); err != nil {
return fmt.Errorf("plugin migration: %w", err)
}
}
return nil
}Complete example: DynamoDB store
Below is a skeleton implementation targeting AWS DynamoDB. The pattern applies to any database.
package dynamostore
import (
"context"
"fmt"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/xraph/authsome/id"
"github.com/xraph/authsome/store"
"github.com/xraph/authsome/user"
"github.com/xraph/authsome/session"
"github.com/xraph/grove/migrate"
)
var _ store.Store = (*DynamoStore)(nil)
type DynamoStore struct {
client *dynamodb.Client
tableName string
}
func New(client *dynamodb.Client, tableName string) *DynamoStore {
return &DynamoStore{client: client, tableName: tableName}
}
// ── User methods ──
func (s *DynamoStore) CreateUser(ctx context.Context, u *user.User) error {
// Marshal user to DynamoDB item and PutItem
return nil
}
func (s *DynamoStore) GetUser(ctx context.Context, userID id.UserID) (*user.User, error) {
// GetItem by PK = "USER#<userID>"
return nil, nil
}
func (s *DynamoStore) GetUserByEmail(ctx context.Context, email string) (*user.User, error) {
// Query GSI on email
return nil, nil
}
// ... implement remaining user.Store methods ...
// ── Session methods ──
func (s *DynamoStore) CreateSession(ctx context.Context, sess *session.Session) error {
// PutItem with PK = "SESSION#<sessionID>"
return nil
}
func (s *DynamoStore) GetSessionByToken(ctx context.Context, token string) (*session.Session, error) {
// Query GSI on session_token
return nil, nil
}
// ... implement remaining session.Store methods ...
// ── Lifecycle ──
func (s *DynamoStore) Migrate(ctx context.Context, extraGroups ...*migrate.Group) error {
// DynamoDB tables are created via CloudFormation/CDK, not in-app migrations.
// Plugin migrations may still need handling via a side-channel.
return nil
}
func (s *DynamoStore) Ping(ctx context.Context) error {
_, err := s.client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
TableName: &s.tableName,
})
return err
}
func (s *DynamoStore) Close() error {
return nil // DynamoDB client doesn't need explicit close
}Testing your store
Write tests against the store interface to verify correctness. The test pattern is the same for every backend:
package dynamostore_test
import (
"context"
"testing"
"myapp/dynamostore"
"github.com/xraph/authsome/id"
"github.com/xraph/authsome/user"
)
func TestCreateAndGetUser(t *testing.T) {
s := dynamostore.New(testClient, "authsome-test")
ctx := context.Background()
u := &user.User{
ID: id.NewUserID(),
Email: "test@example.com",
Name: "Test User",
}
if err := s.CreateUser(ctx, u); err != nil {
t.Fatal("create:", err)
}
got, err := s.GetUser(ctx, u.ID)
if err != nil {
t.Fatal("get:", err)
}
if got.Email != u.Email {
t.Errorf("email = %q, want %q", got.Email, u.Email)
}
}
func TestGetUserNotFound(t *testing.T) {
s := dynamostore.New(testClient, "authsome-test")
ctx := context.Background()
_, err := s.GetUser(ctx, id.NewUserID())
if err == nil {
t.Fatal("expected error for non-existent user")
}
}Registering with the engine
Pass your custom store when building the engine:
eng, err := authsome.NewEngine(
authsome.WithStore(dynamostore.New(client, "authsome")),
)Or when using the Forge extension:
ext := extension.New(
extension.WithEngineOption(
authsome.WithStore(dynamostore.New(client, "authsome")),
),
)Reference implementations
Study the existing implementations for patterns:
| Backend | Package | Notes |
|---|---|---|
| PostgreSQL | store/postgres | Bun ORM, embedded SQL migrations, recommended for production |
| SQLite | store/sqlite | Bun ORM, file-based, good for single-node deployments |
| MongoDB | store/mongo | Native MongoDB driver, document-oriented |
| Memory | store/memory | Hash maps with RWMutex, simplest reference |