Skip to content

v0.6.0 Release Notes

Release Date: 2026-04-12

Highlights

  • Multi-App Platform: Run multiple SaaS apps on shared backend infrastructure with complete data isolation
  • Schema-Per-App Isolation: Each app gets its own PostgreSQL schema for database isolation
  • X-App-ID Routing: Header-based request routing to registered app backends

Added

Multi-App Server (multiapp/)

New package for running multiple apps on shared infrastructure:

  • MultiAppMode: Multiple apps share a server, routed by X-App-ID header
  • SingleAppMode: One app runs on dedicated infrastructure
  • Schema-per-app database isolation using PostgreSQL schemas (app_ prefix)
  • Redis and in-memory cache implementations with app-scoped prefixes
import "github.com/grokify/coreforge/multiapp"

// Create multi-app server
server, err := multiapp.NewServer(multiapp.Config{
    Mode:        multiapp.MultiAppMode,
    DatabaseURL: os.Getenv("DATABASE_URL"),
    RedisURL:    os.Getenv("REDIS_URL"),  // optional
})

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

// Run server
server.Run(":8080")

AppBackend Interface

Composable interface for app registration with lifecycle hooks:

type AppBackend interface {
    // Slug returns unique app identifier (used in X-App-ID header)
    Slug() string

    // Name returns human-readable app name
    Name() string

    // Routes returns the app's HTTP routes
    Routes(deps Dependencies) chi.Router

    // Migrations returns database migrations (optional)
    Migrations() []Migration

    // OnRegister is called after app registration
    OnRegister(ctx context.Context, cfg *AppConfig) error

    // OnShutdown is called during graceful shutdown
    OnShutdown(ctx context.Context) error
}

CoreForge Integration (multiapp/)

Integration helpers connecting multiapp with CoreForge modules:

  • Claims Helpers: JWT claims extraction and validation for multi-tenant context
  • Session Management: Multi-tenant session helpers with app-scoped storage
  • Ent Factory: Create schema-isolated Ent clients from multiapp database
  • HTTP Middleware: Authentication and tenant context propagation
// 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"

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

Example App Backend

Complete example demonstrating the AppBackend interface:

// multiapp/example/example_app.go
type ExampleBackend struct{}

func (b *ExampleBackend) Slug() string { return "example" }
func (b *ExampleBackend) Name() string { return "Example App" }

func (b *ExampleBackend) Routes(deps multiapp.Dependencies) chi.Router {
    r := chi.NewRouter()
    r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("OK"))
    })
    return r
}

Security

Generic 404 Responses

The multiapp middleware now returns generic 404 responses for missing or invalid X-App-ID headers. This prevents:

  • Information leakage about the routing mechanism
  • App ID enumeration attacks
  • Revealing which app IDs are valid
// Before: Detailed error messages
// "X-App-ID header required" (400)
// "app \"xyz\" not found" (404)

// After: Generic 404 for all cases
// 404 Not Found

Documentation

  • Multi-app architecture documentation with deployment patterns (docs/multiapp/overview.md)
  • AppBackend implementation guide with code examples
  • Schema-per-app isolation explained

Dependencies

  • github.com/mattn/go-sqlite3: 1.14.40 → 1.14.42
  • golang.org/x/crypto: 0.49.0 → 0.50.0

Migration Guide

Implementing AppBackend for Existing Apps

  1. Refactor server to accept external database:
// Before: Server creates its own database
func NewServer(cfg Config) (*Server, error) {
    db, _ := sql.Open("postgres", cfg.DatabaseURL)
    return newServerInternal(cfg, db)
}

// After: Add factory for external database
func NewServerWithDatabase(cfg Config, db *sql.DB) (*Server, error) {
    return newServerInternal(cfg, db)
}

func newServerInternal(cfg Config, db *sql.DB) (*Server, error) {
    // Shared initialization logic
}
  1. Create multiapp adapter package (multiapp/backend.go):
package multiapp

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

type Backend struct {
    server *server.Server
}

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

func (b *Backend) Slug() string { return "yourapp" }
func (b *Backend) Name() string { return "Your App" }

func (b *Backend) Routes(deps cfmultiapp.Dependencies) chi.Router {
    // Create server with schema-isolated database
    b.server, _ = server.NewServerWithDatabase(cfg, deps.DB.Pool())
    return b.server.Router()
}

func (b *Backend) OnShutdown(ctx context.Context) error {
    return b.server.Close()
}
  1. Move from internal/multiapp to multiapp (public package):
# multiapp package must be importable by other modules
mv internal/multiapp multiapp

Testing Multi-App Deployment

# Start multi-app server
go run ./cmd/multiapp-server

# Route requests with X-App-ID header
curl -H "X-App-ID: yourapp" http://localhost:8080/api/health
curl -H "X-App-ID: otherapp" http://localhost:8080/api/health

# Requests without X-App-ID return 404
curl http://localhost:8080/api/health
# 404 Not Found

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                           HTTP Request                          │
│                     X-App-ID: app1                         │
└─────────────────────────────────┬───────────────────────────────┘
┌─────────────────────────────────▼───────────────────────────────┐
│                          multiapp.Server                        │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │  appContextMiddleware                                     │  │
│  │  - Extracts X-App-ID header                               │  │
│  │  - Looks up registered app                                │  │
│  │  - Routes to app's router                                 │  │
│  └───────────────────────────────────────────────────────────┘  │
└─────────────────────────────────┬───────────────────────────────┘
          ┌───────────────────────┼───────────────────────┐
          │                       │                       │
          ▼                       ▼                       ▼
┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   App1     │     │   App2     │     │   App3    │
│   AppBackend    │     │   AppBackend    │     │   AppBackend    │
├─────────────────┤     ├─────────────────┤     ├─────────────────┤
│ Schema:         │     │ Schema:         │     │ Schema:         │
│ app_app1   │     │ app_app2   │     │ app_app3  │
└─────────────────┘     └─────────────────┘     └─────────────────┘

Semver Note

This is a minor version bump (v0.5.0 → v0.6.0) as it adds new features without breaking changes. All existing APIs remain backward compatible.