ss-keel-gorm
ss-keel-gorm is the official relational persistence addon for Keel.
It provides database.GormRepository[T, ID], which implements the shared repository contract used by the runtime and application modules:
contracts.Repository[T, ID, httpx.PageQuery, httpx.Page[T]]Supported engines come from the real addon code in ss-keel-gorm/database:
- PostgreSQL
- MySQL
- MariaDB
- SQLite
- SQL Server
- Oracle
Current stable release: v1.7.0 (2026-04-22)
Browse this addon
Section titled “Browse this addon”Installation
Section titled “Installation”keel add gormOr manually:
go get github.com/slice-soft/ss-keel-gormBootstrap
Section titled “Bootstrap”When you run keel add gorm, the CLI creates cmd/setup_gorm.go and adds one line to cmd/main.go:
// cmd/setup_gorm.go — created by keel add gormpackage main
import ( "github.com/slice-soft/ss-keel-core/config" "github.com/slice-soft/ss-keel-core/core" "github.com/slice-soft/ss-keel-core/logger" "github.com/slice-soft/ss-keel-gorm/database")
// setupGorm initialises the database connection and registers a health checker.// SQLite is used by default so a fresh project can run locally without external infrastructure.func setupGorm(app *core.App, log *logger.Logger) *database.DBinstance { dbConfig := config.MustLoadConfig[database.Config]() dbConfig.Logger = log
db, err := database.New(dbConfig) if err != nil { log.Error("failed to initialize database: %v", err) } app.RegisterHealthChecker(database.NewHealthChecker(db)) return db}The following is injected into cmd/main.go:
db := setupGorm(app, appLogger)defer db.Close()This keeps initialization isolated from cmd/main.go. Each addon gets its own setup file.
Useful defaults from the addon:
Engine:sqliteDSN:./app.dbMaxOpenConns:25MaxIdleConns:5ConnMaxLifetime:30mConnMaxIdleTime:15mSSLMode:disableTimeZone:UTC
Generated configuration
Section titled “Generated configuration”When you install ss-keel-gorm with keel add gorm, the CLI appends these generated keys:
| application.properties | .env | Default | Purpose |
|---|---|---|---|
database.engine | DATABASE_ENGINE | sqlite | Database engine used by database.New(...). Supported values include postgres, mysql, mariadb, sqlite, sqlserver, and addon-registered dialectors. |
database.url | DATABASE_URL | ./app.db | Connection string or SQLite file path used by the generated setupGorm bootstrap. |
Generated snippet:
database.engine=${DATABASE_ENGINE:sqlite}database.url=${DATABASE_URL:./app.db}Official example
Section titled “Official example”The official examples repository includes ss-keel-examples/examples/08-gorm-postgres, which demonstrates:
database.New(...)db.AutoMigrate(...)database.NewHealthChecker(...)- CRUD routes backed by GORM
EntityBase
Section titled “EntityBase”ss-keel-gorm ships a ready-made EntityBase struct you can embed in any GORM entity to get ID, CreatedAt, and UpdatedAt with the correct GORM tags pre-configured:
type EntityBase struct { ID string `json:"id" gorm:"primaryKey"` CreatedAt int64 `json:"created_at" gorm:"autoCreateTime:milli"` UpdatedAt int64 `json:"updated_at" gorm:"autoUpdateTime:milli"`}CreatedAt and UpdatedAt store Unix milliseconds. The BeforeCreate GORM hook runs automatically before each insert and sets ID to a new UUID v4 if the field is empty — you do not need to generate IDs manually.
Example usage:
import "github.com/slice-soft/ss-keel-gorm/database"
type ProductEntity struct { database.EntityBase Name string Price float64}On repo.Create(ctx, &product), GORM calls BeforeCreate which generates a UUID for product.ID (if empty) and then sets CreatedAt and UpdatedAt to the current time in milliseconds.
Repository wrapper generated by the CLI
Section titled “Repository wrapper generated by the CLI”When you run keel generate repository users/product --gorm, the official template shape is:
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, }}This keeps GORM-specific behavior inside the module package while still exposing the generic repository methods.
CRUD behavior
Section titled “CRUD behavior”database.GormRepository[T, ID] implements:
repo.FindByID(ctx, id)repo.FindAll(ctx, httpx.PageQuery{Page: 1, Limit: 20})repo.Create(ctx, &entity)repo.Update(ctx, id, &entity)repo.Patch(ctx, id, &entity)repo.Delete(ctx, id)Behavior from the real implementation:
FindByIDreturnsnil, nilwhen the record does not existFindAllcounts total rows and returnshttpx.Page[T]UpdateusesSave— replaces all fields (HTTP PUT semantics)PatchusesUpdates— only writes non-zero fields (HTTP PATCH semantics)Deleterespects GORM soft-delete behavior when the model supports it
Custom queries
Section titled “Custom queries”Use DB() when the generic contract is not enough:
type UserRepository struct { *database.GormRepository[User, string]}
func (r *UserRepository) FindByEmail(ctx context.Context, email string) (*User, error) { var user User err := r.DB().WithContext(ctx).Where("email = ?", email).First(&user).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } return &user, err}Migrations and engine extensions
Section titled “Migrations and engine extensions”The addon also exposes:
db.Migration(...)db.MigrationWithError(...)database.RegisterDialector(...)for custom GORM-compatible engines
Health integration
Section titled “Health integration”database.NewHealthChecker(db) implements contracts.HealthChecker and exposes the dependency under GET /health as:
{ "database": "UP" }See Persistence for the official persistence overview.