Grove

Dual Tag System

Define models with grove tags or use existing bun tags as fallback.

Grove supports a dual struct tag system: grove:"..." (primary) and bun:"..." (fallback). This enables zero-cost migration from bun while giving Grove models a dedicated namespace for extended features.

Tag Resolution Order

When Grove inspects a struct field, it resolves the tag in this order:

  1. grove:"..." — If present, this tag is used exclusively
  2. bun:"..." — If no grove tag exists, the bun tag is used as fallback
  3. Snake case — If neither tag exists, the field name is converted to snake_case
type User struct {
    grove.BaseModel `grove:"table:users,alias:u"`

    ID    int64  `grove:"id,pk,autoincrement"`     // grove tag wins
    Name  string `bun:"name,notnull"`               // bun fallback
    Email string                                     // snake_case: "email"
}

Table-Level Tags

Table options are set on the BaseModel field:

grove.BaseModel `grove:"table:users,alias:u"`
OptionDescription
table:nameTable name
alias:aTable alias for queries

Column-Level Tags

TagDescriptionExample
pkPrimary keygrove:"id,pk"
autoincrementAuto-incrementing columngrove:"id,pk,autoincrement"
notnullNOT NULL constraintgrove:"name,notnull"
uniqueUNIQUE constraintgrove:"email,unique"
default:valueDEFAULT valuegrove:"status,default:'active'"
type:sqltypeOverride SQL typegrove:"data,type:jsonb"
soft_deleteSoft delete timestampgrove:"deleted_at,soft_delete"
privacy:levelPrivacy classificationgrove:"ssn,privacy:pii"
-Skip fieldgrove:"-"
scanonlyRead-only fieldgrove:"computed_field,scanonly"

Relation Tags

type User struct {
    grove.BaseModel `grove:"table:users"`

    ID      int64    `grove:"id,pk"`
    Profile *Profile `grove:"rel:has-one,join:id=user_id"`
    Posts   []*Post  `grove:"rel:has-many,join:id=author_id"`
    Roles   []*Role  `grove:"rel:many-to-many,join:user_roles"`
}
RelationTagDescription
Has Onerel:has-one,join:pk=fkOne-to-one
Has Manyrel:has-many,join:pk=fkOne-to-many
Belongs Torel:belongs-to,join:fk=pkInverse of has-one
Many-to-Manyrel:many-to-many,join:join_tableThrough a join table

Privacy Tags

The privacy tag option classifies fields for the hook system:

type Customer struct {
    grove.BaseModel `grove:"table:customers"`

    ID    int64  `grove:"id,pk"`
    Name  string `grove:"name,notnull"`
    Email string `grove:"email,privacy:pii"`
    SSN   string `grove:"ssn,privacy:pii"`
    Notes string `grove:"notes,privacy:sensitive"`
}

Privacy hooks can use these classifications to redact, mask, or audit access to sensitive fields.

Migrating from Bun

Existing bun models work with Grove unchanged. Just swap the import:

// Before (bun)
type User struct {
    bun.BaseModel `bun:"table:users,alias:u"`
    ID   int64  `bun:"id,pk,autoincrement"`
    Name string `bun:"name,notnull"`
}

// After (grove) — bun tags still work
type User struct {
    grove.BaseModel `bun:"table:users,alias:u"`
    ID   int64  `bun:"id,pk,autoincrement"`
    Name string `bun:"name,notnull"`
}

You can then incrementally migrate tags to grove:"..." to access extended features like privacy annotations.

On this page