An opinionated, batteries-included Go web framework built on Fiber for server-side rendered applications.
Note: This module is under active development and APIs may change.
- SSR-first - Server-side rendering with Go templates
- Multiple databases - SQLite (with WAL) or PostgreSQL support
- Session management - Secure cookie-based sessions with HMAC signing
- Background jobs - Simple job dispatcher for async processing
- Structured logging - JSON/text logging with log rotation
- Middleware - Rate limiting, concurrency control, security headers
go get github.com/karloscodes/cartridgeNewSSRApp is the high-level factory for SSR applications with SQLite:
package main
import (
"time"
"github.com/karloscodes/cartridge"
"myapp/web"
)
func main() {
app, err := cartridge.NewSSRApp("myapp",
cartridge.WithAssets(web.Templates, web.Static),
cartridge.WithSession("/login"),
cartridge.WithRoutes(func(s *cartridge.Server) {
s.Get("/", homeHandler)
s.Get("/users", usersHandler)
}),
)
if err != nil {
panic(err)
}
if err := app.MigrateDatabase(myMigrator); err != nil {
panic(err)
}
if err := app.Run(); err != nil {
panic(err)
}
}
func homeHandler(ctx *cartridge.Context) error {
return ctx.Render("home", fiber.Map{"title": "Welcome"})
}NewInertiaApp is for Inertia.js applications (React/Vue SPA with server-side routing). It handles Inertia dev mode, embedded assets, cross-origin APIs, and background workers:
package main
import (
"github.com/karloscodes/cartridge"
"myapp/web"
)
func main() {
app, err := cartridge.NewInertiaApp(
cartridge.InertiaWithConfig(cfg),
cartridge.InertiaWithStaticAssets(web.Assets()),
cartridge.InertiaWithRoutes(mountRoutes),
cartridge.InertiaWithWorker(jobsManager),
cartridge.InertiaWithSession("/login"),
cartridge.InertiaWithCrossOriginAPI(), // For analytics/public APIs
)
if err != nil {
panic(err)
}
if err := app.Run(); err != nil {
panic(err)
}
}Key differences from NewSSRApp:
- Uses
inertia.SetDevMode(true)in development (re-reads Vite manifest) InertiaWithCrossOriginAPI()configures SecFetchSite for cross-origin requestsInertiaWithWorker()for custom BackgroundWorker implementations- No template engine (Inertia renders React/Vue components)
NewApplication is the lower-level constructor for full control over dependencies. Use this when you need PostgreSQL, a custom database manager, or non-SSR applications:
package main
import (
"log/slog"
"github.com/karloscodes/cartridge"
"github.com/karloscodes/cartridge/database"
"github.com/karloscodes/cartridge/postgres"
)
func main() {
// Create your own dependencies
logger := slog.Default()
// Use PostgreSQL
dbManager := database.NewManager(
postgres.NewDriver(),
&database.Config{
DSN: "host=localhost user=app dbname=myapp",
MaxOpenConns: 25,
MaxIdleConns: 5,
Postgres: database.PostgresOptions{
SSLMode: "disable",
Timezone: "UTC",
},
},
logger,
)
// Create application with custom dependencies
app, err := cartridge.NewApplication(cartridge.ApplicationOptions{
Config: myConfig, // implements cartridge.Config interface
Logger: logger,
DBManager: dbManager, // implements cartridge.DBManager interface
RouteMountFunc: func(s *cartridge.Server) {
s.Get("/", homeHandler)
s.Post("/api/items", createItemHandler)
},
})
if err != nil {
panic(err)
}
if err := app.Run(); err != nil {
panic(err)
}
}Both NewSSRApp and NewInertiaApp support embedded assets for single-binary deployment:
// web/embed.go
package web
import (
"embed"
"io/fs"
)
//go:embed dist/assets
var assetsFS embed.FS
// Assets returns embedded static assets (JS, CSS, images)
func Assets() fs.FS {
sub, _ := fs.Sub(assetsFS, "dist/assets")
return sub
}
//go:embed templates
var templatesFS embed.FS
// Templates returns embedded HTML templates (SSR only)
func Templates() fs.FS {
return templatesFS
}Behavior:
- Production: Assets served from embedded
fs.FS(no external files needed) - Development: Assets served from disk for hot-reload with Vite
Cartridge supports multiple databases through a pluggable driver interface.
SQLite is the default for NewSSRApp. It uses WAL mode and immediate transactions for optimal concurrency:
import "github.com/karloscodes/cartridge/sqlite"
dbManager := sqlite.NewManager(sqlite.Config{
Path: "storage/app.db",
MaxOpenConns: 1, // SQLite works best with 1 connection
MaxIdleConns: 1,
BusyTimeout: 5000, // ms
EnableWAL: true, // Write-Ahead Logging (default: true)
TxImmediate: true, // Immediate transaction locks (default: true)
Logger: logger,
})For PostgreSQL, use the generic database manager with the PostgreSQL driver:
import (
"github.com/karloscodes/cartridge/database"
"github.com/karloscodes/cartridge/postgres"
)
dbManager := database.NewManager(
postgres.NewDriver(),
&database.Config{
DSN: "host=localhost port=5432 user=app password=secret dbname=myapp",
MaxOpenConns: 25,
MaxIdleConns: 5,
Postgres: database.PostgresOptions{
SSLMode: "prefer", // disable, prefer, require
Timezone: "UTC",
SearchPath: "public", // optional schema
},
},
logger,
)Implement the database.Driver interface for other databases:
type Driver interface {
Name() string
Open(dsn string) gorm.Dialector
ConfigureDSN(dsn string, cfg *Config) string
AfterConnect(db *gorm.DB, cfg *Config, logger *slog.Logger) error
Close(db *gorm.DB, logger *slog.Logger) error
SupportsCheckpoint() bool
Checkpoint(db *gorm.DB, mode string) error
}Cartridge reads configuration from environment variables with the app name as prefix:
MYAPP_ENV=production # development, production, test
MYAPP_PORT=8080
MYAPP_SESSION_SECRET=xxx # Required in production
MYAPP_LOG_LEVEL=info
MYAPP_DATA_DIR=storageapp, err := cartridge.NewSSRApp("myapp",
cartridge.WithConfig(cfg), // Custom configuration
cartridge.WithAssets(tmpl, static), // Embedded templates and static files
cartridge.WithTemplateFuncs(myFuncs), // Custom template functions
cartridge.WithErrorHandler(handler), // Custom error handler
cartridge.WithSession("/login"), // Enable session management
cartridge.WithJobs(2*time.Minute, p1), // Background job processors
cartridge.WithRoutes(mountRoutes), // Route mounting
)app, err := cartridge.NewInertiaApp(
cartridge.InertiaWithConfig(cfg), // Config (required, implements FactoryConfig)
cartridge.InertiaWithStaticAssets(fs), // Embedded assets (production only)
cartridge.InertiaWithDBManager(dbMgr), // Custom DB manager (optional)
cartridge.InertiaWithRoutes(mountRoutes), // Route mounting
cartridge.InertiaWithWorker(worker), // Custom BackgroundWorker
cartridge.InertiaWithJobs(interval, p1), // Job processors with interval
cartridge.InertiaWithSession("/login"), // Enable session management
cartridge.InertiaWithCrossOriginAPI(), // Allow cross-origin requests
cartridge.InertiaWithPageTitle("My App"), // HTML page title
cartridge.InertiaWithCatchAllRedirect("/"), // SPA fallback redirect
)// Create a migrator with your models
migrator := cartridge.NewAutoMigrator(
&User{},
&Post{},
&Comment{},
)
// Run migrations (connects, migrates, checkpoints WAL for SQLite)
if err := app.MigrateDatabase(migrator); err != nil {
panic(err)
}// In your login handler
func loginHandler(ctx *cartridge.Context) error {
// Validate credentials...
session := ctx.Ctx.Locals("session").(*cartridge.SessionManager)
if err := session.SetSession(ctx.Ctx, userID); err != nil {
return err
}
return ctx.Redirect("/dashboard")
}
// Protected routes use session middleware
authConfig := &cartridge.RouteConfig{
CustomMiddleware: []fiber.Handler{session.Middleware()},
}
s.Get("/dashboard", dashboardHandler, authConfig)Jobs run on a fixed interval and process batches of work:
// Implement the Processor interface
type EmailProcessor struct{}
func (p *EmailProcessor) ProcessBatch(ctx *cartridge.JobContext) error {
ctx.Logger.Info("processing pending emails")
var pending []Email
if err := ctx.DB.Where("sent_at IS NULL").Find(&pending).Error; err != nil {
return err
}
for _, email := range pending {
// Send email...
ctx.DB.Model(&email).Update("sent_at", time.Now())
}
return nil
}
// Register processors with interval
app, _ := cartridge.NewSSRApp("myapp",
cartridge.WithJobs(2*time.Minute, &EmailProcessor{}, &WebhookProcessor{}),
)Cartridge uses interfaces for dependency injection, making it easy to swap implementations:
// Config abstracts runtime configuration
type Config interface {
IsDevelopment() bool
IsProduction() bool
IsTest() bool
GetPort() string
GetPublicDirectory() string
GetAssetsPrefix() string
}
// DBManager abstracts database connection management
type DBManager interface {
GetConnection() *gorm.DB
Connect() (*gorm.DB, error)
}MIT License - see LICENSE file for details.