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)
Browse this addon
Section titled “Browse this addon”Installation
Section titled “Installation”keel add mongoOr manually:
go get github.com/slice-soft/ss-keel-mongoBootstrap
Section titled “Bootstrap”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 mongopackage 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:27017ConnectTimeout:10sPingTimeout:2sDisconnectTimeout:5sServerSelectionTimeout:5sMaxPoolSize:25MaxConnIdleTime:15m
Generated configuration
Section titled “Generated configuration”When you install ss-keel-mongo with keel add mongo, the CLI appends these generated keys:
| application.properties | .env | Default | Purpose |
|---|---|---|---|
mongo.uri | MONGO_URI | mongodb://localhost:27017 | MongoDB server URI used by the generated setupMongo bootstrap. |
mongo.database | MONGO_DATABASE | app | Database name used by mongo.New(...) and the generated repository wiring. |
Generated snippet:
mongo.uri=${MONGO_URI:mongodb://localhost:27017}mongo.database=${MONGO_DATABASE:app}Official example
Section titled “Official example”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, andOnCreate()/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.
EntityBase
Section titled “EntityBase”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:
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 timestampentity.OnUpdate() // sets only UpdatedAt; CreatedAt is left unchangedThe generated repository calls these automatically — OnCreate() before inserting and OnUpdate() before updating.
Repository wrapper generated by the CLI
Section titled “Repository wrapper generated by the CLI”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:
keel generate repository users/product --mongoThe 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.CRUD behavior
Section titled “CRUD behavior”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:
FindByIDreturnsnil, nilwhen no document existsFindAllpaginates the collection and returnshttpx.Page[T]Updateapplies$setto all non-ID fields — replaces the full document (HTTP PUT semantics)Patchapplies$setonly to the fields explicitly provided in the patch document (HTTP PATCH semantics)Deleteremoves one document by repository ID
Mongo-native helpers
Section titled “Mongo-native helpers”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"})ID strategies
Section titled “ID strategies”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 }),)Health integration
Section titled “Health integration”mongo.NewHealthChecker(client) exposes the dependency in GET /health as:
{ "mongodb": "UP" }When to use it
Section titled “When to use it”- Use
ss-keel-mongofor document-first modules. - Use it when filter-based queries and nested BSON fields are part of the domain model.
- Use it alongside
ss-keel-gormwhen different modules need different persistence models.
See Persistence for the official persistence overview.