Skip to content

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)

Terminal window
keel add gorm

Or manually:

Terminal window
go get github.com/slice-soft/ss-keel-gorm

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 gorm
package 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: sqlite
  • DSN: ./app.db
  • MaxOpenConns: 25
  • MaxIdleConns: 5
  • ConnMaxLifetime: 30m
  • ConnMaxIdleTime: 15m
  • SSLMode: disable
  • TimeZone: UTC

When you install ss-keel-gorm with keel add gorm, the CLI appends these generated keys:

application.properties.envDefaultPurpose
database.engineDATABASE_ENGINEsqliteDatabase engine used by database.New(...). Supported values include postgres, mysql, mariadb, sqlite, sqlserver, and addon-registered dialectors.
database.urlDATABASE_URL./app.dbConnection string or SQLite file path used by the generated setupGorm bootstrap.

Generated snippet:

database.engine=${DATABASE_ENGINE:sqlite}
database.url=${DATABASE_URL:./app.db}

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

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:

database.EntityBase
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.

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.

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:

  • FindByID returns nil, nil when the record does not exist
  • FindAll counts total rows and returns httpx.Page[T]
  • Update uses Save — replaces all fields (HTTP PUT semantics)
  • Patch uses Updates — only writes non-zero fields (HTTP PATCH semantics)
  • Delete respects GORM soft-delete behavior when the model supports it

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
}

The addon also exposes:

  • db.Migration(...)
  • db.MigrationWithError(...)
  • database.RegisterDialector(...) for custom GORM-compatible engines

database.NewHealthChecker(db) implements contracts.HealthChecker and exposes the dependency under GET /health as:

{ "database": "UP" }

See Persistence for the official persistence overview.