Skip to content

Persistence

Keel persistence is addon-based. The runtime does not ship a built-in database layer. Instead, applications compose official persistence addons and inject them into the modules that need them.

Today the official persistence integrations are:

AddonBackendImplementsExtra capabilities
ss-keel-gormPostgreSQL, MySQL, MariaDB, SQLite, SQL Server, Oraclecontracts.Repository[T, ID, httpx.PageQuery, httpx.Page[T]]GORM access, SQL engines, migration helpers, DB health checker
ss-keel-mongoMongoDBcontracts.Repository[T, ID, httpx.PageQuery, httpx.Page[T]]Filter queries, direct collection access, custom ID conversion, Mongo health checker

The official examples currently cover:

  • ss-keel-examples/examples/08-gorm-postgres for relational persistence with ss-keel-gorm
  • ss-keel-examples/examples/13-mongo for document persistence with ss-keel-mongo

GORM integration from the official examples repo

Section titled “GORM integration from the official examples repo”

ss-keel-examples/examples/08-gorm-postgres demonstrates the official relational addon wiring:

dbInstance, err := database.New(database.Config{
Engine: database.EnginePostgres,
Host: dbHost,
Port: dbPort,
User: dbUser,
Password: dbPassword,
Database: dbName,
SSLMode: dbSSLMode,
})
if err != nil {
log.Error("failed to connect to database: %v", err)
}
app.RegisterHealthChecker(database.NewHealthChecker(dbInstance))

That example also shows:

  • db.AutoMigrate(...)
  • CRUD routes backed by GORM
  • /health exposing database state

When you want module-level repositories instead of calling the driver or ORM directly from handlers, the official CLI templates generate wrappers like these.

GORM template shape from keel:

type ProductEntity struct {
database.EntityBase
Name string `json:"name"`
}
type ProductRepository struct {
*database.GormRepository[ProductEntity, string]
log *logger.Logger
}
func NewProductRepository(log *logger.Logger, db *database.DBinstance) *ProductRepository {
return &ProductRepository{
GormRepository: database.NewGormRepository[ProductEntity, string](db),
log: log,
}
}

Mongo template shape from keel:

The Mongo template generates a separate internal document type (ProductMongoDocument) that keeps the domain entity isolated from BSON details while preserving Keel’s string UUID IDs across every backend. Timestamps are stamped automatically via entity.OnCreate() / entity.OnUpdate() in each CRUD method.

type ProductMongoDocument struct {
ID string `bson:"_id,omitempty"`
CreatedAt int64 `bson:"created_at"`
UpdatedAt int64 `bson:"updated_at"`
Name string `bson:"name"`
}
type ProductRepository struct {
repo *mongo.MongoRepository[ProductMongoDocument, string]
log *logger.Logger
}
func NewProductRepository(log *logger.Logger, client *mongo.Client) *ProductRepository {
return &ProductRepository{
repo: mongo.NewRepository[ProductMongoDocument, string](client, "product"),
log: log,
}
}

Those wrappers are generated via:

Terminal window
keel generate repository users/product --gorm
keel generate repository users/product --mongo

Both addons also ship an EntityBase struct you can embed in any entity to get ID, CreatedAt, and UpdatedAt with the correct tags already set. See ss-keel-gorm and ss-keel-mongo for details.

The following is a neutral example based on the official APIs. It shows how one service can compose both persistence addons without making either one part of the core runtime:

app := core.New(core.KConfig{ ... })
mongoClient, err := mongo.New(mongo.Config{ ... })
if err != nil {
appLogger.Error("failed to connect to mongodb: %v", err)
return
}
app.RegisterHealthChecker(mongo.NewHealthChecker(mongoClient))
app.Use(catalog.NewModule(appLogger, mongoClient))
sqlDB, err := database.New(database.Config{
Engine: database.EnginePostgres,
DSN: config.GetEnvOrDefault("DATABASE_URL", ""),
Logger: appLogger,
})
if err != nil {
appLogger.Error("failed to connect to database: %v", err)
return
}
app.RegisterHealthChecker(database.NewHealthChecker(sqlDB))
app.Use(billing.NewModule(appLogger, sqlDB))

This keeps the architecture clean:

  • the core runtime owns composition
  • addons own persistence implementations
  • each module receives only the dependency it needs

Projects created with keel new start with the CLI scaffold:

my-app/
├── cmd/
│ └── main.go
├── internal/
│ └── modules/
│ └── starter/
│ ├── module.go
│ ├── controller.go
│ ├── service.go
│ └── dto.go
├── go.mod
├── keel.toml
├── application.properties
├── .env
├── .env.example
└── .gitignore

As the application grows, persistence-enabled modules follow the same internal/modules/<name>/... structure, regardless of whether the repository is backed by GORM or Mongo.

Terminal window
keel add gorm
keel add mongo
keel generate repository users/product --gorm
keel generate repository billing/audit-log --mongo

keel add installs and wires the addon. keel generate repository uses the official repository template for the selected backend.

  • Use ss-keel-gorm for relational models and SQL-first persistence.
  • Use ss-keel-mongo for document-first persistence and Mongo-native query patterns.
  • Use both in the same service when different modules have different storage models.

See Architecture for the ecosystem layout and ss-keel-gorm / ss-keel-mongo for addon-specific details.