Documentation
¶
Overview ¶
Package multiapp provides infrastructure for running multiple apps on shared or dedicated backend infrastructure. Each app implements the AppBackend interface and has its own users, organizations, and data, in its own schema.
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
SystemAuth 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 ApplyOwnerRole(ctx context.Context, db TxBeginner, owner string, password PasswordSource) error
- func ApplyRoleProvisioning(ctx context.Context, db TxBeginner, p RoleProvisioning, ...) error
- func AuthMiddleware(cfg MiddlewareConfig) (func(http.Handler) http.Handler, error)
- func CheckDatabaseIsolation(ctx context.Context, apps []AppDatabaseLogin, ownerRoles ...string) error
- 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 Migrate(ctx context.Context, migrationDatabaseURL string, backends ...AppBackend) error
- func OwnerRoleSQL(owner string) ([]string, error)
- 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 SetRolePassword(ctx context.Context, db Execer, role string, password PasswordSource) error
- 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, error)
- 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 AppDatabase
- type AppDatabaseLogin
- type AppSession
- type Cache
- type Config
- type DBOption
- 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 Execer
- type FullContext
- type IsolationError
- type IsolationViolation
- type MemoryCache
- type MiddlewareConfig
- type Migration
- type PasswordSource
- 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 RoleProvisioning
- type SchemaDB
- func (db *SchemaDB) AcquireConn(ctx context.Context) (*pgxpool.Conn, error)deprecated
- 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
- func (s *Server) Apps() []string
- func (s *Server) GetApp(slug string) (AppBackend, bool)
- func (s *Server) RegisterApp(backend AppBackend) error
- func (s *Server) Router() *chi.Mux
- func (s *Server) Run(addr string) error
- func (s *Server) Shutdown(ctx context.Context) error
- func (s *Server) VerifyDatabaseIsolation(ctx context.Context) error
- type ServerMode
- type TxBeginner
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.
"sf_principals",
"sf_organizations",
"sf_principal_memberships",
"sf_app_roles",
"sf_resource_grants",
}
DefaultSharedReadTables are the shared SystemForge identity tables an app reads when it authorizes requests in process against the identity database: who a principal is, which organizations it belongs to, and the app roles and resource grants it holds (the tables authz mode rbac reads). RoleProvisioning grants SELECT on them by default, when SharedReadTables is nil.
Tables that hold secrets or drive sign-in are deliberately absent and must stay with the identity service's own login: credentials, OAuth app secrets, authorization codes and tokens, principal (API key) token hashes, SCIM tokens, login states and sessions, invites, signup and external identities.
var ErrAppDatabaseLoginRequired = errors.New("multiapp: app has no database login of its own")
ErrAppDatabaseLoginRequired is returned when an app is registered without a database login of its own and the shared login is not allowed.
var ErrAppSchemaMissing = errors.New("multiapp: app schema does not exist")
ErrAppSchemaMissing is returned by RegisterApp when the server runs no migrations (no MigrationDatabaseURL) and the app's schema does not exist or its login cannot see it.
var ErrCacheMiss = errors.New("cache miss")
ErrCacheMiss is returned when a key is not found in the cache.
var ErrInProcessMigrationsNotAllowed = errors.New("multiapp: MigrationDatabaseURL puts the migration (owner) login in the server process")
ErrInProcessMigrationsNotAllowed is returned by NewServer when MigrationDatabaseURL is set without AllowInProcessMigrations.
var ErrJWTServiceRequired = errors.New("multiapp: MiddlewareConfig.JWTService is required")
ErrJWTServiceRequired is returned by AuthMiddleware without a MiddlewareConfig.JWTService.
ErrSharedDatabaseLogin is returned when a pool connected with one app's login is asked to serve another app's schema.
NoSharedReadTables is a SharedReadTables value that grants no shared table.
"sf_client_assertion_jtis",
"sf_credentials",
"sf_external_identities",
"sf_invites",
"sf_login_sessions",
"sf_login_states",
"sf_manager_claims",
"sf_manager_invites",
"sf_oauth_app_secrets",
"sf_oauth_auth_codes",
"sf_oauth_tokens",
"sf_principal_tokens",
"sf_scim_tokens",
"sf_signup_invites",
"sf_signup_waitlist",
}
RestrictedSharedTables are the shared SystemForge tables an app login is never granted, even when SharedReadTables names them: they hold credentials, secrets, tokens, sign-in sessions and state, invitations and claim codes, and external identities. RoleProvisioning refuses a SharedReadTables entry listed here.
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 ApplyOwnerRole ¶ added in v0.21.0
func ApplyOwnerRole(ctx context.Context, db TxBeginner, owner string, password PasswordSource) error
ApplyOwnerRole creates the migration (owner) login on db and sets its password from password when it is not nil.
func ApplyRoleProvisioning ¶ added in v0.21.0
func ApplyRoleProvisioning(ctx context.Context, db TxBeginner, p RoleProvisioning, password PasswordSource) error
ApplyRoleProvisioning runs p's SQL in one transaction on db, which must be an admin (or migration-capable) login, then sets the role's password from password when it is not nil. Leave password nil for roles that sign in without one, such as IAM database authentication.
func AuthMiddleware ¶
AuthMiddleware returns middleware that validates access tokens and sets authentication context. This works with the app context middleware.
It requires DPoP-bound tokens with a valid proof (see MiddlewareConfig.DPoP); unbound bearer tokens are refused unless DPoP.AllowBearer is set. It returns an error without a JWT service or a replay cache.
The middleware chain should be:
- appContextMiddleware (set by Server in multi-app mode)
- AuthMiddleware (validates the token and its proof, sets claims)
- Your handlers
func CheckDatabaseIsolation ¶ added in v0.21.0
func CheckDatabaseIsolation(ctx context.Context, apps []AppDatabaseLogin, ownerRoles ...string) error
CheckDatabaseIsolation verifies that each app login in apps (those with a Pool) is confined to its own schema:
- it is not a superuser and has neither BYPASSRLS nor CREATEROLE;
- it is not the login of another app, does not own any app schema, and is not a member of another app's role, of an owner role (the owners of the app schemas and ownerRoles, the migration logins), of a superuser or BYPASSRLS role, or of pg_read_all_data or pg_write_all_data;
- it has USAGE on its own schema;
- it has neither USAGE nor CREATE on another app's schema, and reading a table there, creating a table there, and SET ROLE to another app's role or an owner role all fail with permission denied (the probes run in transactions that are rolled back);
- it cannot create objects in schema public, which every app's search_path includes;
- its default search_path does not name or reach another app's schema.
It returns an *IsolationError listing every violation, or another error when the check itself could not run.
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 Migrate ¶ added in v0.21.0
func Migrate(ctx context.Context, migrationDatabaseURL string, backends ...AppBackend) error
Migrate creates the schemas of backends and runs their Migrations with the migration (owner) login at migrationDatabaseURL. It is the production way to migrate: run it as a separate step before the servers start, from a process given the owner's secret (for example the app binary's own migrate command, run as a one-off task), and leave Config.MigrationDatabaseURL empty in the servers. Every migration runs on every call, so migrations must be idempotent.
func OwnerRoleSQL ¶ added in v0.21.0
OwnerRoleSQL returns statements that create the migration (owner) login if it is missing: LOGIN, with no superuser, role creation, database creation, replication or RLS bypass. Set its password with SetRolePassword.
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 SetRolePassword ¶ added in v0.21.0
SetRolePassword sets role's password from password. The statement carries a SCRAM-SHA-256 verifier computed here, never the password, so server statement logs do not record it; errors never include it either.
func ValidateAppAccess ¶
ValidateAppAccess reports whether the request has an app context. It does not check that the caller's token belongs to that app: the app context comes from the X-App-ID header. Bind tokens to apps with MiddlewareConfig.AppAudiences.
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 is a connection pool for one database login, serving app schemas over it.
A pool connected with an app's own login serves that app's schema only: ForSchema, the Ent helpers and EntConnHook refuse a second schema, since switching schemas inside one login (search_path, or SET LOCAL ROLE, which SQL in the transaction can reset) does not keep apps apart. Open the pool of the migration (owner) login with AsMigrationLogin, and a pool every app shares, for local development only, with AllowSharedLogin. See "Database isolation" in the package README.
func NewAppAwareDB ¶
func NewAppAwareDB(databaseURL string, opts ...DBOption) (*AppAwareDB, error)
NewAppAwareDB opens a connection pool for databaseURL and checks that it connects.
func NewAppAwareDBWithConfig ¶ added in v0.21.0
func NewAppAwareDBWithConfig(ctx context.Context, config *pgxpool.Config, opts ...DBOption) (*AppAwareDB, error)
NewAppAwareDBWithConfig opens a connection pool from config, for example one whose BeforeConnect sets a short-lived IAM authentication token as the password, and checks that it connects.
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, error)
ForSchema returns a SchemaDB scoped to a specific schema. The name is used in SQL, so it must be a plain identifier (lowercase letters, digits, and underscores); anything else is refused. Unless the pool was opened with AsMigrationLogin or AllowSharedLogin, it serves one schema only, and asking for a second returns ErrSharedDatabaseLogin.
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 SystemAuth) 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 AppDatabase ¶ added in v0.21.0
type AppDatabase struct {
// URL is a PostgreSQL connection string with the app's own
// credentials, for example read from a secret manager at startup.
URL string
// PoolConfig, when set, is used instead of URL: for example a config
// whose BeforeConnect sets a short-lived IAM authentication token as
// the password.
PoolConfig *pgxpool.Config
}
AppDatabase is an app's own database login.
type AppDatabaseLogin ¶ added in v0.21.0
type AppDatabaseLogin struct {
// Slug names the app in violations.
Slug string
// Schema is the app's schema.
Schema string
// Pool is connected with the app's own login. Leave it nil for an app
// on the shared development login: its schema is still checked as one
// the other apps must not reach, but its login is not checked.
Pool *pgxpool.Pool
}
AppDatabaseLogin is one app's schema and, when the app has a database login of its own, the pool connected with it.
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
// AppDatabases holds each app's own database login, keyed by app
// slug. RegisterApp refuses an app without one unless
// AllowSharedDatabaseLogin is set. Each login gets its own pool,
// serves its app's schema only, and must pass the isolation check.
AppDatabases map[string]AppDatabase
// MigrationDatabaseURL is the migration (owner) login, for running
// migrations in the server process: RegisterApp then uses it to create
// each app's schema and run the app's Migrations. It is never handed
// to app code. It is accepted only with AllowInProcessMigrations (or
// AllowSharedDatabaseLogin). Leave it empty in production and run
// Migrate as a separate step, such as a one-off task given the owner's
// secret; RegisterApp then expects each app's schema to exist, refuses
// an app whose schema is missing (ErrAppSchemaMissing), and does not
// run Migrations.
MigrationDatabaseURL string
// AllowInProcessMigrations lets the server hold the migration login,
// MigrationDatabaseURL, and create schemas and run migrations when an
// app registers. Any code that takes over the process can then use
// the owner's privileges on every app schema. For local development
// only; the server logs a warning when it is set.
AllowInProcessMigrations bool
// MigrationRole names the migration (owner) role when
// MigrationDatabaseURL is empty, so the isolation check can verify
// that no app login is a member of it or can act as it. It holds no
// credentials. With MigrationDatabaseURL, the role is read from that
// login and this field is ignored. Optional, but set it in
// production.
MigrationRole string
// use the MigrationDatabaseURL login, shared by every such app. The
// database then does not keep those apps apart: each can read and
// write the others' tables. It needs MigrationDatabaseURL and implies
// AllowInProcessMigrations, since the apps on it already hold the
// owner login. For local development only; the server logs a warning
// when it is set.
AllowSharedDatabaseLogin bool
// SkipIsolationCheck turns the database isolation check off. It is
// accepted only together with AllowSharedDatabaseLogin. Apps on the
// shared login are never checked as subjects either way.
SkipIsolationCheck bool
// 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.
Every app connects to the database with a login of its own, given in AppDatabases, in single-app and multi-app mode alike. Schema and table changes run with a separate migration (owner) login, as a deploy step of their own (Migrate) in production, so the server process never holds the owner's credentials. See "Database isolation" in the package README.
type DBOption ¶ added in v0.21.0
type DBOption func(*dbOptions)
DBOption configures an AppAwareDB.
func AllowSharedLogin ¶ added in v0.21.0
func AllowSharedLogin() DBOption
AllowSharedLogin lets app code use one pool, and so one database login, for several app schemas. The database then does not keep the apps apart: any of them can read and write the others' tables with a schema-qualified name. Use it for local development only.
func AsMigrationLogin ¶ added in v0.21.0
func AsMigrationLogin() DBOption
AsMigrationLogin marks the pool as the migration (owner) login, which creates every app schema and runs migrations in them, so it may serve any number of schemas. Never hand it to app code.
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 opens a dedicated, unpooled pgx connection with schema set, for raw pgx operations outside of Ent; close it when done. The schema name must be a plain identifier; anything else is refused, and so is a second schema on a factory over an app's own login.
func (*EntClientFactory) StdDBForSchema ¶
StdDBForSchema returns a *sql.DB configured for a specific schema. Every connection of the returned pool has search_path set to the app's schema, and it connects with the factory's login, so pass the AppAwareDB of the app's own login: like AppAwareDB.ForSchema, a factory over an app login serves one schema only. The schema name must be a plain identifier; anything else is refused.
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 sets an application's schema on database/sql transactions, from the AppContext in the request context.
func NewEntConnHook ¶
func NewEntConnHook(db *AppAwareDB) *EntConnHook
NewEntConnHook creates a new Ent connection hook. db is the AppAwareDB of the login the transactions run with: like AppAwareDB.ForSchema, the hook refuses an app context naming a second schema unless db was opened with AllowSharedLogin, so one app's login never serves another app's requests.
func (*EntConnHook) SetSchemaFromContext
deprecated
SetSchemaFromContext sets the search_path of conn to the schema of the AppContext in ctx, for the whole session.
Deprecated: the setting stays on the connection after it returns to the database/sql pool, so later use of that connection by code that does not run the hook reads and writes this application's tables. Use SetSchemaInTx, whose search_path ends with the transaction.
func (*EntConnHook) SetSchemaInTx ¶ added in v0.21.0
SetSchemaInTx scopes tx to the schema of the AppContext in ctx with SET LOCAL search_path, which ends with the transaction, so the connection goes back to the pool without it. Call it first in every transaction an application runs.
type Execer ¶ added in v0.21.0
type Execer interface {
Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
}
Execer runs a statement. pgx.Tx, *pgx.Conn and *pgxpool.Pool satisfy it.
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 SystemAuth, 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 SystemAuth 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 IsolationError ¶ added in v0.21.0
type IsolationError struct {
Violations []IsolationViolation
}
IsolationError lists the isolation violations found. Its message names apps, roles and schemas, never a password or connection string.
func (*IsolationError) Error ¶ added in v0.21.0
func (e *IsolationError) Error() string
type IsolationViolation ¶ added in v0.21.0
IsolationViolation is one way an app's login reaches beyond its schema.
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 (required).
JWTService *jwt.Service
// RequireAuth requires authentication for all requests.
// If false, requests without credentials are allowed through.
RequireAuth bool
// SetRLSContext automatically sets RLS context from JWT claims.
SetRLSContext bool
// AppAudiences binds a token to the app a request is routed to: when the
// request has an app context, the token must carry one of the audiences
// returned for that app, so a token minted for one app cannot be replayed
// against another by changing the X-App-ID header. Default: the app's
// AppID, so mint tokens with jwt.TokenOptions{Audience: []string{appID}}.
AppAudiences func(app *AppContext) []string
// AllowCrossAppTokens turns app binding off: a request with an app
// context accepts any token addressed to the JWT service's own
// Config.Audience, whichever app it was minted for. Set it only when a
// token is meant to be valid in every app.
AllowCrossAppTokens bool
// DPoP configures DPoP (RFC 9449), which AuthMiddleware requires by
// default: a token must be bound to a key (cnf.jkt) and come with the
// DPoP scheme and a fresh proof by that key for the request, carrying
// the token's hash. DPoP.ReplayCache is required (NewRedisReplayCache
// when several instances serve the apps). Set DPoP.BaseURL to the
// external base URL clients use, including any path prefix a proxy
// strips: the proof's htu must equal it plus the request path as this
// server sees it. DPoP.AllowBearer is the explicit opt-in that also
// accepts unbound bearer tokens; a DPoP-bound token is never accepted
// as Bearer.
DPoP middleware.DPoPConfig
// 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 PasswordSource ¶ added in v0.21.0
PasswordSource returns a role's password, for example from a secret manager or the environment. It is called once, when the password is set.
func PasswordFromEnv ¶ added in v0.21.0
func PasswordFromEnv(name string) PasswordSource
PasswordFromEnv reads the password from the environment variable name. An unset or empty variable is an error.
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 RoleProvisioning ¶ added in v0.21.0
type RoleProvisioning struct {
// Schema is the app's schema, normally SchemaFromAppSlug(slug).
// Required.
Schema string
// Role is the app's login role. Required.
Role string
// OwnerRole is the migration login: it owns Schema and creates its
// tables, and the app role receives privileges on the tables it
// creates through default privileges. It must differ from Role, and
// the login that applies the SQL must be able to act as it (a
// superuser, or a member of OwnerRole). Required.
OwnerRole string
// Database, when set, is granted CONNECT to Role.
Database string
// ReadOnly limits Role to SELECT on its schema's tables. By default
// it gets SELECT, INSERT, UPDATE and DELETE.
ReadOnly bool
// Default "public".
SharedSchema string
// only). Nil, the default, means DefaultSharedReadTables; a non-nil
// empty slice (NoSharedReadTables) grants none, for an app that does
// not read the identity tables. A non-empty list replaces the
// default, so append to DefaultSharedReadTables to add tables. Tables
// that do not exist yet are skipped, so apply the SQL again after the
// identity migrations create them.
SharedReadTables []string
// MemberOf lists roles Role is made a member of, for example rds_iam
// for IAM database authentication on Amazon RDS. Never list another
// app's role or OwnerRole: the isolation check rejects it.
MemberOf []string
}
RoleProvisioning describes the database login of one app: a role that can log in, use its own schema and nothing else of the other apps.
func (RoleProvisioning) SQL ¶ added in v0.21.0
func (p RoleProvisioning) SQL() ([]string, error)
SQL returns the statements that provision the app's login, in order. They are idempotent, so they can be applied again, for example after new shared tables appear. They never contain a password; ApplyRoleProvisioning sets one.
The statements:
- create Role (if missing) with LOGIN NOINHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS NOREPLICATION, and reset LOGIN, NOINHERIT, NOCREATEDB and NOCREATEROLE on an existing one (changing SUPERUSER or BYPASSRLS needs a superuser, so the isolation check reports those instead);
- create Schema owned by OwnerRole and revoke PUBLIC's access to it;
- grant Role USAGE on Schema and table, sequence and function privileges on what exists, plus default privileges on what OwnerRole creates later;
- revoke CREATE on schema public from PUBLIC and EXECUTE on OwnerRole's future functions from PUBLIC;
- grant SELECT on the shared tables (SharedReadTables, by default DefaultSharedReadTables), and set Role's search_path to Schema and SharedSchema.
type SchemaDB ¶
type SchemaDB struct {
// contains filtered or unexported fields
}
SchemaDB provides database operations scoped to a specific schema.
func (*SchemaDB) AcquireConn
deprecated
AcquireConn acquires a connection with the schema's search_path set for the whole session.
Deprecated: the setting stays on the connection after Release, so the pool can hand it, with this schema, to unrelated code. Use Begin, whose search_path ends with the transaction.
func (*SchemaDB) Exec ¶
Exec executes a statement with the schema's search_path, in its own transaction.
func (*SchemaDB) Query ¶
Query executes a query and returns rows with the schema's search_path. The rows hold a transaction open until they are closed (Close, or Next returning false), so always close them.
func (*SchemaDB) QueryRow ¶
QueryRow executes a query and returns a single row with the schema's search_path. The transaction ends when the row is scanned.
func (*SchemaDB) StdDB ¶
StdDB opens a *sql.DB for use with Ent or other database/sql libraries, connected with the same login as this SchemaDB and with search_path set to its schema on every connection. Each call opens a new pool; open it once per app and close it when done. Usage with Ent:
db, err := deps.DB.StdDB(ctx)
if err != nil {
return err
}
client := ent.NewClient(ent.Driver(entsql.OpenDB(dialect.Postgres, db)))
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 SystemAuth.
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. It connects with the app's own database login, creates the app's schema and runs its migrations with the migration login when in-process migrations are enabled (otherwise it requires the schema to exist), and checks that the app's login is confined to its schema; any failure refuses the app.
func (*Server) Run ¶
Run verifies database isolation, then starts the HTTP server and blocks until shutdown.
func (*Server) VerifyDatabaseIsolation ¶ added in v0.21.0
VerifyDatabaseIsolation runs CheckDatabaseIsolation over every registered app with its own login. RegisterApp and Run call it; call it yourself before serving if you serve Router without Run. It returns nil when SkipIsolationCheck is set.
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. |