multiapp

package
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 12, 2026 License: MIT Imports: 27 Imported by: 0

README

Multi-App Framework

The multiapp package enables multiple apps to share backend infrastructure while maintaining complete data isolation. 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

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                           HTTP Request                          │
│                     X-App-ID: app1                         │
│                     Authorization: Bearer <jwt>                 │
└─────────────────────────────────┬───────────────────────────────┘
                                  │
┌─────────────────────────────────▼───────────────────────────────┐
│                          multiapp.Server                        │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │  appContextMiddleware                                     │  │
│  │  - Extracts X-App-ID header                               │  │
│  │  - Looks up registered app                                │  │
│  │  - Sets AppContext in request context                     │  │
│  │  - Routes to app's router                                 │  │
│  └───────────────────────────────────────────────────────────┘  │
└─────────────────────────────────┬───────────────────────────────┘
                                  │
          ┌───────────────────────┼───────────────────────┐
          │                       │                       │
          ▼                       ▼                       ▼
┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   App1     │     │   App2     │     │   App3    │
│   AppBackend    │     │   AppBackend    │     │   AppBackend    │
├─────────────────┤     ├─────────────────┤     ├─────────────────┤
│ Schema:         │     │ Schema:         │     │ Schema:         │
│ app_app1   │     │ app_app2   │     │ app_app3  │
├─────────────────┤     ├─────────────────┤     ├─────────────────┤
│ Own users       │     │ Own users       │     │ Own users       │
│ Own orgs        │     │ Own orgs        │     │ Own orgs        │
│ Own data        │     │ Own data        │     │ Own data        │
└─────────────────┘     └─────────────────┘     └─────────────────┘

Key Concepts

Schema-Per-App Isolation

Each app gets its own PostgreSQL schema (e.g., app_app1). This provides:

  • Complete data isolation between apps
  • Independent migrations per app
  • Shared database infrastructure
PostgreSQL Database
├── app_app1/          # App1 schema
│   ├── users
│   ├── organizations
│   ├── courses
│   └── ...
├── app_app2/          # App2 schema
│   ├── users
│   ├── organizations
│   ├── dashboards
│   └── ...
└── app_app3/         # App3 schema
    ├── users
    ├── organizations
    ├── proofs
    └── ...
Self-Contained Apps

Each app is fully self-contained with its own:

  • Users and authentication
  • Organizations and memberships
  • Business data
  • API routes

No external dependencies (like CoreControl) are required. CoreControl integration is optional for SSO and centralized management.

Integration Pattern

Apps integrate with multiapp by implementing the AppBackend interface and providing a factory function that accepts an Ent client:

┌────────────────────────────────────────────────────────────────┐
│                     Deployment Modes                           │
├─────────────────────────┬──────────────────────────────────────┤
│   Standalone Mode       │        Multi-App Mode                │
│   (dedicated infra)     │        (shared infra)                │
├─────────────────────────┼──────────────────────────────────────┤
│   cmd/server/main.go    │   coreforge-multi server             │
│          │              │          │                           │
│          ▼              │          ▼                           │
│   NewServer(cfg)        │   multiapp.Backend                   │
│          │              │          │                           │
│          ▼              │          ▼                           │
│   NewServerWithOptions  │   NewServerWithEntClient             │
│   (creates own DB)      │   (uses schema-isolated DB)          │
│          │              │          │                           │
│          └──────────────┴──────────┘                           │
│                         │                                      │
│                         ▼                                      │
│              newServerInternal(cfg, client)                    │
│              (shared: JWT, router, authz, storage, routes)     │
└────────────────────────────────────────────────────────────────┘

Implementing AppBackend

Step 1: Refactor Existing Server

Extract shared initialization into an internal function:

// internal/api/server.go

// NewServerWithOptions - standalone mode, creates own DB
func NewServerWithOptions(cfg *config.Config, connectDB bool) (*Server, error) {
    var client *ent.Client
    if connectDB {
        // Create database connection
        db, err := sql.Open("postgres", cfg.Database.DSN())
        if err != nil {
            return nil, err
        }
        drv := entsql.OpenDB(dialect.Postgres, db)
        client = ent.NewClient(ent.Driver(drv))

        // Run migrations
        if err := client.Schema.Create(context.Background()); err != nil {
            return nil, err
        }
    }
    return newServerInternal(cfg, client)
}

// NewServerWithEntClient - multi-app mode, uses provided client
func NewServerWithEntClient(cfg *config.Config, client *ent.Client) (*Server, error) {
    return newServerInternal(cfg, client)
}

// newServerInternal - shared initialization logic
func newServerInternal(cfg *config.Config, client *ent.Client) (*Server, error) {
    // JWT service, router, middleware, authz, BFF, storage, routes...
    // All the shared setup code
}
Step 2: Create AppBackend Adapter
// internal/multiapp/backend.go

package multiapp

import (
    "github.com/grokify/myapp/ent"
    "github.com/grokify/myapp/internal/api"
    cfmultiapp "github.com/grokify/coreforge/multiapp"
)

type Backend struct {
    cfg    *config.Config
    deps   cfmultiapp.Dependencies
    db     *ent.Client
    server *api.Server
}

func NewBackend(cfg *config.Config) *Backend {
    return &Backend{cfg: cfg}
}

func (b *Backend) Slug() string { return "myapp" }
func (b *Backend) Name() string { return "My App" }

func (b *Backend) Routes(deps cfmultiapp.Dependencies) chi.Router {
    b.deps = deps

    // Create Ent client with schema isolation
    b.db = createEntClient(deps.DB)

    // Run migrations
    b.db.Schema.Create(context.Background())

    // Create server using existing API package
    server, _ := api.NewServerWithEntClient(b.cfg, b.db)
    b.server = server

    return server.Router().(chi.Router)
}

func (b *Backend) OnRegister(ctx context.Context, cfg *cfmultiapp.AppConfig) error {
    return nil
}

func (b *Backend) OnShutdown(ctx context.Context) error {
    return b.server.Close()
}
Step 3: Create Schema-Isolated Ent Client
func createEntClient(schemaDB *cfmultiapp.SchemaDB) *ent.Client {
    pool := schemaDB.Pool()
    config := pool.Config().ConnConfig.Copy()

    // Set search_path to app's schema
    if config.RuntimeParams == nil {
        config.RuntimeParams = make(map[string]string)
    }
    config.RuntimeParams["search_path"] = schemaDB.Schema() + ", public"

    // Create standard DB connection
    connStr := stdlib.RegisterConnConfig(config)
    db, _ := sql.Open("pgx", connStr)

    // Create Ent client
    drv := entsql.OpenDB(dialect.Postgres, db)
    return ent.NewClient(ent.Driver(drv))
}

Usage

Standalone Deployment (Unchanged)
# Existing command works as before
go run ./cmd/server
Multi-App Deployment
package main

import (
    "github.com/grokify/app1/internal/multiapp"
    cfmultiapp "github.com/grokify/coreforge/multiapp"
)

func main() {
    server, _ := cfmultiapp.NewServer(cfmultiapp.Config{
        Mode:        cfmultiapp.MultiAppMode,
        DatabaseURL: "postgres://localhost:5432/coreforge",
        RedisURL:    "redis://localhost:6379",  // optional
    })

    // Register apps
    server.RegisterApp(multiapp.NewBackend(nil))
    // server.RegisterApp(app2.NewBackend(nil))
    // server.RegisterApp(app3.NewBackend(nil))

    server.Run(":8080")
}
Testing Multi-App Mode
# Request to App1
curl -H "X-App-ID: app1" http://localhost:8080/api/courses

# Request to App2
curl -H "X-App-ID: app2" http://localhost:8080/api/dashboards

Context Flow

The multiapp framework provides several context values:

// In your handlers:
func (h *Handler) HandleRequest(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    // App context (from X-App-ID routing)
    appCtx := multiapp.AppContextFromContext(ctx)
    appCtx.AppID          // "app1"
    appCtx.DatabaseSchema // "app_app1"
    appCtx.Features       // ["auth", "tenancy"]

    // JWT claims (from auth middleware)
    claims := middleware.ClaimsFromContext(ctx)
    claims.PrincipalID    // user UUID
    claims.OrganizationID // org UUID

    // Full context (combines all)
    fc := multiapp.FullContextFromContext(ctx)
    fc.HasApp()           // true in multi-app mode
    fc.IsAuthenticated()  // true if JWT valid
}

Optional: CoreControl Integration

Apps can optionally integrate with CoreControl for:

  • Single Sign-On (SSO) across apps
  • Centralized user management
  • Cross-app analytics

This is enabled by setting a federation_id on users when they authenticate via CoreControl. See the CoreControl Integration Guide for details.

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

Constants

This section is empty.

Variables

View Source
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.

View Source
var ErrCacheMiss = errors.New("cache miss")

ErrCacheMiss is returned when a key is not found in the cache.

Functions

func AppIDFromContext

func AppIDFromContext(ctx context.Context) string

AppIDFromContext returns the app ID from context, or empty string if not present.

func AppIDFromSessionMetadata

func AppIDFromSessionMetadata(session *bff.Session) string

AppIDFromSessionMetadata extracts app ID from session metadata.

func AppLogger

func AppLogger(next http.Handler) http.Handler

AppLogger returns middleware that adds app context to structured logging. This should be used after appContextMiddleware.

func AppSessionMetadata

func AppSessionMetadata(appID, appSlug string) map[string]string

AppSessionMetadata returns metadata for storing app context in session. This can be added to bff.Session.Metadata for app tracking.

func AppSlugFromContext

func AppSlugFromContext(ctx context.Context) string

AppSlugFromContext returns the app slug from context, or empty string if not present.

func AppSlugFromSessionMetadata

func AppSlugFromSessionMetadata(session *bff.Session) string

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:

  1. appContextMiddleware (set by Server in multi-app mode)
  2. AuthMiddleware (validates JWT, sets claims)
  3. Your handlers

func DatabaseSchemaFromContext

func DatabaseSchemaFromContext(ctx context.Context) string

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

func GetSetting(ctx context.Context, key string) any

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

func HasAppContext(ctx context.Context) bool

HasAppContext checks if an app context is present in the context.

func HasFeature

func HasFeature(ctx context.Context, feature string) bool

HasFeature checks if a feature is enabled for the current app.

func InjectRLS

func InjectRLS() func(http.Handler) http.Handler

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

func RequireApp() func(http.Handler) http.Handler

RequireApp returns middleware that requires app context to be present. Use this for routes that must have an app context.

func RequireAuth

func RequireAuth(ctx context.Context) (uuid.UUID, error)

RequireAuth is a middleware helper that ensures the request is authenticated. Returns the principal ID if authenticated, or an error if not.

func RequireAuthentication

func RequireAuthentication() func(http.Handler) http.Handler

RequireAuthentication returns middleware that requires valid authentication.

func RequireFeature

func RequireFeature(feature string) func(http.Handler) http.Handler

RequireFeature returns middleware that requires a specific feature to be enabled.

func RequirePermission

func RequirePermission(permission string) func(http.Handler) http.Handler

RequirePermission returns middleware that requires a specific permission.

func RequireRole

func RequireRole(role string) func(http.Handler) http.Handler

RequireRole returns middleware that requires a specific role.

func SchemaFromAppSlug

func SchemaFromAppSlug(slug string) string

SchemaFromAppSlug returns the database schema name for an app slug. By convention, app schemas are named "app_{slug}".

func SessionKeyWithApp

func SessionKeyWithApp(appID, sessionID string) string

SessionKeyWithApp returns a session store key that includes the app ID. This ensures sessions are isolated per-app in shared session stores.

func ValidateAppAccess

func ValidateAppAccess(ctx context.Context) error

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

func ValidateAppFeature(ctx context.Context, feature string) error

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.

func WithRLSFromClaims

func WithRLSFromClaims(ctx context.Context) context.Context

WithRLSFromClaims adds RLS context from JWT claims. This bridges session/middleware claims to rls context.

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

func (db *AppAwareDB) GetSchemaSize(ctx context.Context, schemaName string) (int64, error)

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

func AppClaimsFromContext(ctx any) *AppClaims

AppClaimsFromContext extracts app-aware claims from context. If the request has both app context and JWT claims, it combines them.

func NewAppClaims

func NewAppClaims(claims *jwt.Claims) *AppClaims

NewAppClaims creates AppClaims from existing jwt.Claims.

func NewAppClaimsForApp

func NewAppClaimsForApp(claims *jwt.Claims, appID, appSlug string) *AppClaims

NewAppClaimsForApp creates AppClaims with app context.

func (*AppClaims) WithApp

func (c *AppClaims) WithApp(appID, appSlug string) *AppClaims

WithApp adds app context to the claims.

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

func (f *EntClientFactory) PgxConnWithSchema(ctx context.Context, schema string) (*pgx.Conn, error)

PgxConnWithSchema acquires a pgx connection with schema set. This is useful for raw pgx operations outside of Ent.

func (*EntClientFactory) StdDBForSchema

func (f *EntClientFactory) StdDBForSchema(ctx context.Context, schema string) (*sql.DB, error)

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

func (h *EntConnHook) SetSchemaFromContext(ctx context.Context, conn *sql.Conn) error

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) Get

func (c *MemoryCache) Get(ctx context.Context, key string) ([]byte, error)

Get retrieves a value from memory.

func (*MemoryCache) Set

func (c *MemoryCache) Set(ctx context.Context, key string, value []byte, ttlSeconds int) error

Set stores a value in 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) Close

func (c *RedisCache) Close() error

Close closes the Redis connection.

func (*RedisCache) Delete

func (c *RedisCache) Delete(ctx context.Context, key string) error

Delete removes a value from Redis.

func (*RedisCache) Get

func (c *RedisCache) Get(ctx context.Context, key string) ([]byte, error)

Get retrieves a value from Redis.

func (*RedisCache) Set

func (c *RedisCache) Set(ctx context.Context, key string, value []byte, ttlSeconds int) error

Set stores a value in 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

func (db *SchemaDB) AcquireConn(ctx context.Context) (*pgxpool.Conn, error)

AcquireConn acquires a connection with the schema's search_path set.

func (*SchemaDB) Begin

func (db *SchemaDB) Begin(ctx context.Context) (*SchemaTx, error)

Begin starts a transaction with the schema's search_path.

func (*SchemaDB) BeginTx

func (db *SchemaDB) BeginTx(ctx context.Context, opts pgx.TxOptions) (*SchemaTx, error)

BeginTx starts a transaction with options and the schema's search_path.

func (*SchemaDB) Exec

func (db *SchemaDB) Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)

Exec executes a query with the schema's search_path.

func (*SchemaDB) Pool

func (db *SchemaDB) Pool() *pgxpool.Pool

Pool returns the underlying connection pool.

func (*SchemaDB) Query

func (db *SchemaDB) Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)

Query executes a query and returns rows with the schema's search_path.

func (*SchemaDB) QueryRow

func (db *SchemaDB) QueryRow(ctx context.Context, sql string, args ...any) pgx.Row

QueryRow executes a query and returns a single row with the schema's search_path.

func (*SchemaDB) Schema

func (db *SchemaDB) Schema() string

Schema returns the schema name.

func (*SchemaDB) StdDB

func (db *SchemaDB) StdDB(ctx context.Context) (*sql.DB, error)

StdDB returns a *sql.DB wrapper for use with Ent or other SQL libraries. Note: This creates a new connection per call; consider caching if needed.

type SchemaTx

type SchemaTx struct {
	// contains filtered or unexported fields
}

SchemaTx is a transaction scoped to a schema.

func (*SchemaTx) Commit

func (tx *SchemaTx) Commit(ctx context.Context) error

Commit commits the transaction.

func (*SchemaTx) Exec

func (tx *SchemaTx) Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)

Exec executes a query in the transaction.

func (*SchemaTx) Query

func (tx *SchemaTx) Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)

Query executes a query and returns rows in the transaction.

func (*SchemaTx) QueryRow

func (tx *SchemaTx) QueryRow(ctx context.Context, sql string, args ...any) pgx.Row

QueryRow executes a query and returns a single row in the transaction.

func (*SchemaTx) Rollback

func (tx *SchemaTx) Rollback(ctx context.Context) error

Rollback rolls back the transaction.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server manages multiple app backends on shared or dedicated infrastructure.

func NewServer

func NewServer(cfg Config) (*Server, error)

NewServer creates a new multi-app server. The server does not require any external dependencies like CoreControl.

func (*Server) Apps

func (s *Server) Apps() []string

Apps returns the list of registered app slugs.

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.

func (*Server) Router

func (s *Server) Router() *chi.Mux

Router returns the underlying chi router for custom middleware.

func (*Server) Run

func (s *Server) Run(addr string) error

Run starts the HTTP server and blocks until shutdown.

func (*Server) Shutdown

func (s *Server) Shutdown(ctx context.Context) error

Shutdown gracefully shuts down the server.

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"
)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL