Documentation
¶
Overview ¶
Package multiapp provides infrastructure for running multiple apps on shared or dedicated backend infrastructure. Each app implements the AppBackend interface and is fully self-contained with its own users, organizations, and data.
Apps can be deployed in two modes:
- Multi-app mode: Multiple apps share a server, routed by X-App-ID header
- Single-app mode: One app runs on dedicated infrastructure
CoreControl integration is optional - apps work without any external dependencies.
Index ¶
- Variables
- func AppIDFromContext(ctx context.Context) string
- func AppIDFromSessionMetadata(session *bff.Session) string
- func AppLogger(next http.Handler) http.Handler
- func AppSessionMetadata(appID, appSlug string) map[string]string
- func AppSlugFromContext(ctx context.Context) string
- func AppSlugFromSessionMetadata(session *bff.Session) string
- func AuthMiddleware(cfg MiddlewareConfig) func(http.Handler) http.Handler
- func DatabaseSchemaFromContext(ctx context.Context) string
- func EnrichContext(ctx context.Context, appCtx *AppContext) context.Context
- func GetSetting(ctx context.Context, key string) any
- func HasAppContext(ctx context.Context) bool
- func HasFeature(ctx context.Context, feature string) bool
- func InjectRLS() func(http.Handler) http.Handler
- func RequireApp() func(http.Handler) http.Handler
- func RequireAuth(ctx context.Context) (uuid.UUID, error)
- func RequireAuthentication() func(http.Handler) http.Handler
- func RequireFeature(feature string) func(http.Handler) http.Handler
- func RequirePermission(permission string) func(http.Handler) http.Handler
- func RequireRole(role string) func(http.Handler) http.Handler
- func SchemaFromAppSlug(slug string) string
- func SessionKeyWithApp(appID, sessionID string) string
- func ValidateAppAccess(ctx context.Context) error
- func ValidateAppFeature(ctx context.Context, feature string) error
- func WithAppContext(ctx context.Context, appCtx *AppContext) context.Context
- func WithRLSFromClaims(ctx context.Context) context.Context
- type AppAwareDB
- func (db *AppAwareDB) Close() error
- func (db *AppAwareDB) CreateSchema(ctx context.Context, schemaName string) error
- func (db *AppAwareDB) DropSchema(ctx context.Context, schemaName string) error
- func (db *AppAwareDB) ForSchema(schemaName string) *SchemaDB
- func (db *AppAwareDB) GetSchemaSize(ctx context.Context, schemaName string) (int64, error)
- func (db *AppAwareDB) ListSchemas(ctx context.Context) ([]string, error)
- func (db *AppAwareDB) Pool() *pgxpool.Pool
- type AppBackend
- type AppClaims
- type AppConfig
- type AppContext
- type AppSession
- type Cache
- type Config
- type Dependencies
- type EntClientFactory
- func (f *EntClientFactory) EntDriverForSchema(ctx context.Context, schema string) (*entsql.Driver, error)
- func (f *EntClientFactory) PgxConnWithSchema(ctx context.Context, schema string) (*pgx.Conn, error)
- func (f *EntClientFactory) StdDBForSchema(ctx context.Context, schema string) (*sql.DB, error)
- type EntConfig
- type EntConnHook
- type FullContext
- type MemoryCache
- type MiddlewareConfig
- type Migration
- type RedisCache
- func (c *RedisCache) Close() error
- func (c *RedisCache) Delete(ctx context.Context, key string) error
- func (c *RedisCache) Get(ctx context.Context, key string) ([]byte, error)
- func (c *RedisCache) Set(ctx context.Context, key string, value []byte, ttlSeconds int) error
- func (c *RedisCache) WithPrefix(prefix string) Cache
- type SchemaDB
- func (db *SchemaDB) AcquireConn(ctx context.Context) (*pgxpool.Conn, error)
- func (db *SchemaDB) Begin(ctx context.Context) (*SchemaTx, error)
- func (db *SchemaDB) BeginTx(ctx context.Context, opts pgx.TxOptions) (*SchemaTx, error)
- func (db *SchemaDB) Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
- func (db *SchemaDB) Pool() *pgxpool.Pool
- func (db *SchemaDB) Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
- func (db *SchemaDB) QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
- func (db *SchemaDB) Schema() string
- func (db *SchemaDB) StdDB(ctx context.Context) (*sql.DB, error)
- type SchemaTx
- func (tx *SchemaTx) Commit(ctx context.Context) error
- func (tx *SchemaTx) Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
- func (tx *SchemaTx) Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
- func (tx *SchemaTx) QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
- func (tx *SchemaTx) Rollback(ctx context.Context) error
- type Server
- type ServerMode
Constants ¶
This section is empty.
Variables ¶
var ( // ErrNoAppContext is returned when app context is required but not present. ErrNoAppContext = errors.New("multiapp: no app context in request") // ErrFeatureNotEnabled is returned when a required feature is not enabled. ErrFeatureNotEnabled = errors.New("multiapp: feature not enabled for this app") // ErrNotAuthenticated is returned when authentication is required but not present. ErrNotAuthenticated = errors.New("multiapp: authentication required") // ErrAppNotFound is returned when the requested app is not registered. ErrAppNotFound = errors.New("multiapp: app not found") // ErrAppAlreadyRegistered is returned when trying to register a duplicate app. ErrAppAlreadyRegistered = errors.New("multiapp: app already registered") // ErrInvalidSchemaName is returned when a schema name is invalid. ErrInvalidSchemaName = errors.New("multiapp: invalid schema name") // ErrMigrationFailed is returned when database migrations fail. ErrMigrationFailed = errors.New("multiapp: migration failed") )
Sentinel errors for the multiapp package.
var ErrCacheMiss = errors.New("cache miss")
ErrCacheMiss is returned when a key is not found in the cache.
Functions ¶
func AppIDFromContext ¶
AppIDFromContext returns the app ID from context, or empty string if not present.
func AppIDFromSessionMetadata ¶
AppIDFromSessionMetadata extracts app ID from session metadata.
func AppLogger ¶
AppLogger returns middleware that adds app context to structured logging. This should be used after appContextMiddleware.
func AppSessionMetadata ¶
AppSessionMetadata returns metadata for storing app context in session. This can be added to bff.Session.Metadata for app tracking.
func AppSlugFromContext ¶
AppSlugFromContext returns the app slug from context, or empty string if not present.
func AppSlugFromSessionMetadata ¶
AppSlugFromSessionMetadata extracts app slug from session metadata.
func AuthMiddleware ¶
func AuthMiddleware(cfg MiddlewareConfig) func(http.Handler) http.Handler
AuthMiddleware returns middleware that validates JWT tokens and sets authentication context. This works with the app context middleware.
The middleware chain should be:
- appContextMiddleware (set by Server in multi-app mode)
- AuthMiddleware (validates JWT, sets claims)
- Your handlers
func DatabaseSchemaFromContext ¶
DatabaseSchemaFromContext returns the database schema from context, or empty string if not present.
func EnrichContext ¶
func EnrichContext(ctx context.Context, appCtx *AppContext) context.Context
EnrichContext adds app context to an existing context that may already have authentication context from session middleware. This is useful when you need to add app context to a context that was created outside of the multi-app middleware.
func GetSetting ¶
GetSetting retrieves a setting value for the current app. Returns nil if the setting doesn't exist or no app context is present.
func HasAppContext ¶
HasAppContext checks if an app context is present in the context.
func HasFeature ¶
HasFeature checks if a feature is enabled for the current app.
func InjectRLS ¶
InjectRLS returns middleware that sets RLS context from JWT claims. Use this after AuthMiddleware if you need RLS but didn't enable SetRLSContext.
func RequireApp ¶
RequireApp returns middleware that requires app context to be present. Use this for routes that must have an app context.
func RequireAuth ¶
RequireAuth is a middleware helper that ensures the request is authenticated. Returns the principal ID if authenticated, or an error if not.
func RequireAuthentication ¶
RequireAuthentication returns middleware that requires valid authentication.
func RequireFeature ¶
RequireFeature returns middleware that requires a specific feature to be enabled.
func RequirePermission ¶
RequirePermission returns middleware that requires a specific permission.
func RequireRole ¶
RequireRole returns middleware that requires a specific role.
func SchemaFromAppSlug ¶
SchemaFromAppSlug returns the database schema name for an app slug. By convention, app schemas are named "app_{slug}".
func SessionKeyWithApp ¶
SessionKeyWithApp returns a session store key that includes the app ID. This ensures sessions are isolated per-app in shared session stores.
func ValidateAppAccess ¶
ValidateAppAccess validates that the authenticated user has access to the app. This is a placeholder for future app-level access control. Currently, it just checks that the app context is present.
func ValidateAppFeature ¶
ValidateAppFeature validates that a feature is enabled for the current app.
func WithAppContext ¶
func WithAppContext(ctx context.Context, appCtx *AppContext) context.Context
WithAppContext returns a new context with the app context attached.
Types ¶
type AppAwareDB ¶
type AppAwareDB struct {
// contains filtered or unexported fields
}
AppAwareDB manages database connections with schema-per-app isolation. Each app gets its own PostgreSQL schema, providing complete data isolation.
func NewAppAwareDB ¶
func NewAppAwareDB(databaseURL string) (*AppAwareDB, error)
NewAppAwareDB creates a new app-aware database connection pool.
func (*AppAwareDB) Close ¶
func (db *AppAwareDB) Close() error
Close closes the database connection pool.
func (*AppAwareDB) CreateSchema ¶
func (db *AppAwareDB) CreateSchema(ctx context.Context, schemaName string) error
CreateSchema creates a new PostgreSQL schema for an app.
func (*AppAwareDB) DropSchema ¶
func (db *AppAwareDB) DropSchema(ctx context.Context, schemaName string) error
DropSchema drops a PostgreSQL schema and all its contents. Use with caution - this deletes all data in the schema.
func (*AppAwareDB) ForSchema ¶
func (db *AppAwareDB) ForSchema(schemaName string) *SchemaDB
ForSchema returns a SchemaDB scoped to a specific schema.
func (*AppAwareDB) GetSchemaSize ¶
GetSchemaSize returns the size of a schema in bytes.
func (*AppAwareDB) ListSchemas ¶
func (db *AppAwareDB) ListSchemas(ctx context.Context) ([]string, error)
ListSchemas returns all app schemas in the database.
func (*AppAwareDB) Pool ¶
func (db *AppAwareDB) Pool() *pgxpool.Pool
Pool returns the underlying connection pool.
type AppBackend ¶
type AppBackend interface {
// Slug returns the unique app identifier (e.g., "app1").
// This is used for routing, database schema naming, and configuration.
Slug() string
// Name returns the human-readable display name (e.g., "App1").
Name() string
// EntSchemas returns the Ent schemas for this app's database tables.
// These are created in the app's isolated database schema.
EntSchemas() []ent.Schema
// Routes returns the HTTP routes for this app.
// Routes are mounted under the app's context with no prefix needed.
// The Dependencies provide access to the database, cache, and logger.
Routes(deps Dependencies) chi.Router
// Migrations returns app-specific database migrations.
// These run in the app's isolated database schema.
Migrations() []Migration
// OnRegister is called when the app is registered with the server.
// Use this for app-specific initialization.
OnRegister(ctx context.Context, cfg *AppConfig) error
// OnShutdown is called when the server is shutting down.
// Use this to clean up app-specific resources.
OnShutdown(ctx context.Context) error
}
AppBackend is the interface that all app backends must implement. This enables composition of multiple apps into a single server or standalone deployment on dedicated infrastructure.
Each app is self-contained with its own users, organizations, and data. No external dependencies (like CoreControl) are required.
type AppClaims ¶
type AppClaims struct {
*jwt.Claims
// AppID is the app this token was issued for.
// In multi-app mode, tokens are scoped to a specific app.
AppID string `json:"app_id,omitempty"`
// AppSlug is the URL-safe app identifier.
AppSlug string `json:"app_slug,omitempty"`
}
AppClaims extends jwt.Claims with app-specific fields for multi-app mode. This is used when you need to include app context in JWT tokens.
func AppClaimsFromContext ¶
AppClaimsFromContext extracts app-aware claims from context. If the request has both app context and JWT claims, it combines them.
func NewAppClaims ¶
NewAppClaims creates AppClaims from existing jwt.Claims.
func NewAppClaimsForApp ¶
NewAppClaimsForApp creates AppClaims with app context.
type AppConfig ¶
type AppConfig struct {
// AppID is the unique identifier for this app (usually same as Slug).
AppID string
// Slug is the URL-safe identifier used for routing and schema naming.
Slug string
// DatabaseSchema is the PostgreSQL schema name for this app (e.g., "app_app1").
DatabaseSchema string
// Features lists enabled features for this app.
Features []string
// Settings contains app-specific configuration values.
Settings map[string]any
}
AppConfig contains configuration for an app instance.
type AppContext ¶
type AppContext struct {
// AppID is the unique identifier for this app.
AppID string
// AppSlug is the URL-safe identifier.
AppSlug string
// AppName is the human-readable display name.
AppName string
// DatabaseSchema is the PostgreSQL schema for this app.
DatabaseSchema string
// Features lists enabled features for this app.
Features []string
// Settings contains app-specific configuration.
Settings map[string]any
}
AppContext contains information about the current app for a request. This is injected by the app context middleware in multi-app mode.
func AppContextFromContext ¶
func AppContextFromContext(ctx context.Context) *AppContext
AppContextFromContext extracts the app context from the request context. Returns nil if no app context is present (e.g., in single-app mode without middleware).
type AppSession ¶
type AppSession struct {
*bff.Session
// AppID is the app this session belongs to.
AppID string
// AppSlug is the URL-safe app identifier.
AppSlug string
}
AppSession extends bff.Session with app-specific fields. This provides app-scoped sessions for multi-app deployments.
func CreateAppSession ¶
func CreateAppSession( appID, appSlug string, userID uuid.UUID, accessToken, refreshToken string, accessExpiry, refreshExpiry time.Duration, ) (*AppSession, error)
CreateAppSession creates a new app-scoped session.
func NewAppSession ¶
func NewAppSession(session *bff.Session, appID, appSlug string) *AppSession
NewAppSession creates an app-scoped session from a BFF session.
type Cache ¶
type Cache interface {
// Get retrieves a value from the cache.
Get(ctx context.Context, key string) ([]byte, error)
// Set stores a value in the cache with optional TTL.
Set(ctx context.Context, key string, value []byte, ttlSeconds int) error
// Delete removes a value from the cache.
Delete(ctx context.Context, key string) error
// WithPrefix returns a Cache that prefixes all keys.
WithPrefix(prefix string) Cache
}
Cache provides a cache interface for apps. Implementations can use Redis, in-memory, or other backends.
type Config ¶
type Config struct {
// Mode determines how apps are routed (multi or single).
Mode ServerMode
// DatabaseURL is the PostgreSQL connection string.
// In multi-app mode, each app gets its own schema within this database.
DatabaseURL string
// RedisURL is the Redis connection string for caching and sessions.
// Optional - if empty, an in-memory cache is used.
RedisURL string
// Logger is the logger to use. If nil, a default logger is created.
Logger *slog.Logger
}
Config contains configuration for creating a new server.
type Dependencies ¶
type Dependencies struct {
// DB provides database access scoped to the app's schema.
DB *SchemaDB
// Cache provides cache access with app-specific key prefix.
Cache Cache
// Logger is configured with app context (app slug, etc.).
Logger *slog.Logger
// Config contains app-specific configuration.
Config *AppConfig
}
Dependencies provides access to shared infrastructure for app handlers. All dependencies are scoped to the app's context.
type EntClientFactory ¶
type EntClientFactory struct {
// contains filtered or unexported fields
}
EntClientFactory creates Ent clients scoped to app schemas. This bridges the multiapp SchemaDB with Ent's database layer.
func NewEntClientFactory ¶
func NewEntClientFactory(db *AppAwareDB) *EntClientFactory
NewEntClientFactory creates a factory for app-scoped Ent clients.
func (*EntClientFactory) EntDriverForSchema ¶
func (f *EntClientFactory) EntDriverForSchema(ctx context.Context, schema string) (*entsql.Driver, error)
EntDriverForSchema returns an Ent SQL driver scoped to a schema. This is a convenience wrapper around StdDBForSchema.
Usage:
drv, err := factory.EntDriverForSchema(ctx, "app_app1") client := ent.NewClient(ent.Driver(drv))
func (*EntClientFactory) PgxConnWithSchema ¶
PgxConnWithSchema acquires a pgx connection with schema set. This is useful for raw pgx operations outside of Ent.
func (*EntClientFactory) StdDBForSchema ¶
StdDBForSchema returns a *sql.DB configured for a specific schema. The returned connection has search_path set to the app's schema.
Usage with Ent:
factory := multiapp.NewEntClientFactory(appAwareDB)
db, err := factory.StdDBForSchema(ctx, "app_app1")
if err != nil {
return err
}
drv := entsql.OpenDB(dialect.Postgres, db)
client := ent.NewClient(ent.Driver(drv))
type EntConfig ¶
type EntConfig struct {
// Schema is the PostgreSQL schema name for this app.
Schema string
// Debug enables Ent debug logging.
Debug bool
}
EntConfig provides configuration for Ent client creation.
type EntConnHook ¶
type EntConnHook struct {
// contains filtered or unexported fields
}
EntConnHook provides a hook that sets schema context for each connection. Use this with Ent's connection hooks for dynamic schema selection.
func NewEntConnHook ¶
func NewEntConnHook(db *AppAwareDB) *EntConnHook
NewEntConnHook creates a new Ent connection hook.
func (*EntConnHook) SetSchemaFromContext ¶
SetSchemaFromContext sets the search_path based on AppContext in the context. This should be used with Ent's BeginTx or connection acquire hooks.
type FullContext ¶
type FullContext struct {
// App context (from X-App-ID header routing)
App *AppContext
// JWT claims (from session/middleware)
Claims *jwt.Claims
// RLS context
TenantID uuid.UUID
UserID uuid.UUID
// Federation context (from CoreControl, optional)
FederationID uuid.UUID
}
FullContext represents the complete context available in a multi-app request. It combines app context with authentication context.
func FullContextFromContext ¶
func FullContextFromContext(ctx context.Context) *FullContext
FullContextFromContext extracts all context values into a unified struct. Returns nil for any values that are not present.
func RequireAppAndAuth ¶
func RequireAppAndAuth(ctx context.Context) (*FullContext, error)
RequireAppAndAuth is a middleware helper that ensures both app context and authentication are present. Returns the full context if valid.
func (*FullContext) HasApp ¶
func (fc *FullContext) HasApp() bool
HasApp returns true if the context has app context (multi-app mode).
func (*FullContext) IsAuthenticated ¶
func (fc *FullContext) IsAuthenticated() bool
IsAuthenticated returns true if the context has valid authentication.
func (*FullContext) IsFederated ¶
func (fc *FullContext) IsFederated() bool
IsFederated returns true if the user has a CoreControl federation ID.
func (*FullContext) OrganizationID ¶
func (fc *FullContext) OrganizationID() *uuid.UUID
OrganizationID returns the current organization context. Returns nil if no organization is selected.
func (*FullContext) PrincipalID ¶
func (fc *FullContext) PrincipalID() uuid.UUID
PrincipalID returns the authenticated principal ID. Returns uuid.Nil if not authenticated.
type MemoryCache ¶
type MemoryCache struct {
// contains filtered or unexported fields
}
MemoryCache implements Cache using in-memory storage. Useful for testing and single-instance deployments.
func NewMemoryCache ¶
func NewMemoryCache() *MemoryCache
NewMemoryCache creates a new in-memory cache.
func (*MemoryCache) Delete ¶
func (c *MemoryCache) Delete(ctx context.Context, key string) error
Delete removes a value from memory.
func (*MemoryCache) WithPrefix ¶
func (c *MemoryCache) WithPrefix(prefix string) Cache
WithPrefix returns a new cache with a key prefix.
type MiddlewareConfig ¶
type MiddlewareConfig struct {
// JWTService validates JWT tokens.
JWTService *jwt.Service
// RequireAuth requires authentication for all requests.
// If false, unauthenticated requests are allowed through.
RequireAuth bool
// SetRLSContext automatically sets RLS context from JWT claims.
SetRLSContext bool
// OnError is called when authentication fails.
// If nil, a default error response is sent.
OnError func(w http.ResponseWriter, r *http.Request, err error)
}
MiddlewareConfig configures the multiapp middleware stack.
type Migration ¶
type Migration struct {
// Version is the migration version number (must be unique and sequential).
Version int
// Name is a short description of the migration.
Name string
// Up applies the migration.
Up func(ctx context.Context, db *SchemaDB) error
// Down rolls back the migration (optional).
Down func(ctx context.Context, db *SchemaDB) error
}
Migration represents a database migration for an app.
type RedisCache ¶
type RedisCache struct {
// contains filtered or unexported fields
}
RedisCache implements Cache using Redis.
func NewRedisCache ¶
func NewRedisCache(redisURL string) (*RedisCache, error)
NewRedisCache creates a new Redis-backed cache.
func (*RedisCache) Delete ¶
func (c *RedisCache) Delete(ctx context.Context, key string) error
Delete removes a value from Redis.
func (*RedisCache) WithPrefix ¶
func (c *RedisCache) WithPrefix(prefix string) Cache
WithPrefix returns a new cache with a key prefix.
type SchemaDB ¶
type SchemaDB struct {
// contains filtered or unexported fields
}
SchemaDB provides database operations scoped to a specific schema.
func (*SchemaDB) AcquireConn ¶
AcquireConn acquires a connection with the schema's search_path set.
type SchemaTx ¶
type SchemaTx struct {
// contains filtered or unexported fields
}
SchemaTx is a transaction scoped to a schema.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server manages multiple app backends on shared or dedicated infrastructure.
func NewServer ¶
NewServer creates a new multi-app server. The server does not require any external dependencies like CoreControl.
func (*Server) GetApp ¶
func (s *Server) GetApp(slug string) (AppBackend, bool)
GetApp returns a registered app by slug.
func (*Server) RegisterApp ¶
func (s *Server) RegisterApp(backend AppBackend) error
RegisterApp registers an app backend with the server. This creates the app's database schema and runs migrations.
type ServerMode ¶
type ServerMode string
ServerMode determines how the server handles app routing.
const ( // MultiAppMode routes requests to apps based on X-App-ID header. // Multiple apps share the same server instance. MultiAppMode ServerMode = "multi" // SingleAppMode runs a single app without header-based routing. // The app owns the entire server instance. SingleAppMode ServerMode = "single" )
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package example provides a minimal example of implementing the AppBackend interface.
|
Package example provides a minimal example of implementing the AppBackend interface. |
|
cmd
command
Package main demonstrates running apps in single-app and multi-app modes.
|
Package main demonstrates running apps in single-app and multi-app modes. |