Skip to content

ss-keel-mongo

ss-keel-mongo is the official MongoDB persistence addon for Keel.

It implements the same shared repository contract used by ss-keel-gorm:

contracts.Repository[T, ID, httpx.PageQuery, httpx.Page[T]]

On top of that contract, it exposes Mongo-native capabilities such as filter queries, direct collection access, and custom ID conversion.

Current stable release: v1.7.0 (2026-04-22)

Terminal window
keel add mongo

Or manually:

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

When you run keel add mongo, the CLI creates cmd/setup_mongo.go and adds one line to cmd/main.go:

// cmd/setup_mongo.go — created by keel add mongo
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-mongo/mongo"
)
// setupMongo initialises the MongoDB client and registers a health checker.
func setupMongo(app *core.App, log *logger.Logger) *mongo.Client {
mongoConfig := config.MustLoadConfig[mongo.Config]()
mongoConfig.Logger = log
mongoClient, err := mongo.New(mongoConfig)
if err != nil {
log.Error("failed to initialize MongoDB: %v", err)
}
app.RegisterHealthChecker(mongo.NewHealthChecker(mongoClient))
return mongoClient
}

The following is injected into cmd/main.go:

mongoClient := setupMongo(app, appLogger)
defer mongoClient.Close()

This keeps initialization isolated from cmd/main.go. Each addon gets its own setup file.

Useful defaults from the addon:

  • URI: mongodb://localhost:27017
  • ConnectTimeout: 10s
  • PingTimeout: 2s
  • DisconnectTimeout: 5s
  • ServerSelectionTimeout: 5s
  • MaxPoolSize: 25
  • MaxConnIdleTime: 15m

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

application.properties.envDefaultPurpose
mongo.uriMONGO_URImongodb://localhost:27017MongoDB server URI used by the generated setupMongo bootstrap.
mongo.databaseMONGO_DATABASEappDatabase name used by mongo.New(...) and the generated repository wiring.

Generated snippet:

mongo.uri=${MONGO_URI:mongodb://localhost:27017}
mongo.database=${MONGO_DATABASE:app}

The official examples repository includes ss-keel-examples/examples/13-mongo, which demonstrates:

  • mongo.New(...)
  • mongo.NewRepository[Note, string](...)
  • mongo.NewHealthChecker(...)
  • CRUD routes with pagination, EntityBase, and OnCreate() / OnUpdate()

The repository wrapper below comes from the official keel Mongo repository template, and the runtime bootstrap in this page comes from the real ss-keel-mongo API.

ss-keel-mongo ships a ready-made EntityBase struct you can embed in any document entity to get ID, CreatedAt, and UpdatedAt with the correct BSON tags pre-configured:

mongo.EntityBase
type EntityBase struct {
ID string `json:"id" bson:"_id,omitempty"`
CreatedAt int64 `json:"created_at" bson:"created_at,omitempty"`
UpdatedAt int64 `json:"updated_at" bson:"updated_at,omitempty"`
}

Example usage:

import "github.com/slice-soft/ss-keel-mongo/mongo"
type ProductEntity struct {
mongo.EntityBase
Name string `bson:"name"`
Price float64 `bson:"price"`
}

Unlike GORM, Mongo does not auto-populate CreatedAt/UpdatedAt. EntityBase ships two helpers for this:

entity.OnCreate() // sets both CreatedAt and UpdatedAt to the current Unix millisecond timestamp
entity.OnUpdate() // sets only UpdatedAt; CreatedAt is left unchanged

The generated repository calls these automatically — OnCreate() before inserting and OnUpdate() before updating.

The Mongo template generates a separate internal document type (ProductMongoDocument) that keeps string UUID IDs and separates the domain entity from the stored BSON representation. The entity and the Mongo document are kept separate so the domain layer never depends on Mongo-specific details.

That wrapper is generated with:

Terminal window
keel generate repository users/product --mongo

The generated shape is:

// ProductMongoDocument is the internal Mongo representation.
// It is never exposed outside the repository.
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,
}
}
// Create stamps timestamps via OnCreate and persists the UUID-backed document as-is.
func (r *ProductRepository) Create(ctx context.Context, entity *ProductEntity) error {
entity.OnCreate()
document := toProductMongoDocument(entity)
if err := r.repo.Create(ctx, &document); err != nil {
return err
}
entity.ID = document.ID
return nil
}
// Update replaces all fields of the document (HTTP PUT semantics).
func (r *ProductRepository) Update(ctx context.Context, id string, entity *ProductEntity) error {
entity.OnUpdate()
// trims the incoming ID, converts to document, delegates to r.repo.Update
...
}
// Patch applies a partial update — only the fields set in entity are written (HTTP PATCH semantics).
func (r *ProductRepository) Patch(ctx context.Context, id string, entity *ProductEntity) error {
entity.OnUpdate()
normalizedID, err := normalizeProductID(id)
if err != nil {
return err
}
document := toProductMongoDocument(entity)
return r.repo.Patch(ctx, normalizedID, &document)
}
// FindByID, FindAll, and Delete follow the same document↔entity conversion pattern.

mongo.MongoRepository[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 no document exists
  • FindAll paginates the collection and returns httpx.Page[T]
  • Update applies $set to all non-ID fields — replaces the full document (HTTP PUT semantics)
  • Patch applies $set only to the fields explicitly provided in the patch document (HTTP PATCH semantics)
  • Delete removes one document by repository ID

mongo.MongoRepository[T, ID] exposes extra methods beyond the shared contract. Add custom methods to your repository wrapper to call them:

func (r *ProductRepository) FindByEmail(ctx context.Context, email string) (*ProductEntity, error) {
doc, err := r.repo.FindOneByFilter(ctx, bson.M{"email": email})
if err != nil || doc == nil {
return nil, err
}
entity := toProductEntity(*doc)
return &entity, nil
}
func (r *ProductRepository) FindByCountry(ctx context.Context, country string) ([]ProductEntity, error) {
docs, err := r.repo.FindMany(ctx, bson.M{"profile.country": country})
// ...
}
// For full control, access the raw collection directly:
coll := r.repo.Collection()
cursor, err := coll.Find(ctx, bson.M{"profile.country": "CO"})

You can customize the ID field or conversion logic:

mongo.NewRepository[User, string](
client,
"users",
mongo.WithIDField[User, string]("slug"),
mongo.WithIDConverter[User, string](func(id string) (interface{}, error) {
return strings.ToLower(id), nil
}),
)

mongo.NewHealthChecker(client) exposes the dependency in GET /health as:

{ "mongodb": "UP" }
  • Use ss-keel-mongo for document-first modules.
  • Use it when filter-based queries and nested BSON fields are part of the domain model.
  • Use it alongside ss-keel-gorm when different modules need different persistence models.

See Persistence for the official persistence overview.