Skip to main content

Models & database

Nucleus's data layer is SQL-first by design. There is no ORM, no auto-magical query builder. pkg/db wraps database/sql with production-quality defaults; pkg/model adds metadata, registry and a generic CRUD operator on top.

Defining a model

type Article struct {
ID int64 `db:"id,primary" json:"id"`
Title string `db:"title" json:"title" validate:"required,min=3"`
Body string `db:"body" json:"body"`
AuthorID int64 `db:"author_id,fk=users.id" json:"author_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}

Tag conventions:

TagMeaning
db:"name"Column name.
db:"name,primary"Primary key.
db:"name,fk=table.column"Foreign key.
db:"name,index"Single-column index.
db:"name,index=composite"Composite index.
db:"-"Ignore.
validate:"…"go-playground/validator rules.
admin:"…"Admin panel hints (label, ordering, …).

Registering a model

import "github.com/jcsvwinston/nucleus/pkg/model"

a.Models.Register(&Article{})

The registry drives:

  • the embedded admin panel (pkg/admin),
  • the generic CRUD operator,
  • metadata-aware migration scaffolding (nucleus generate migration),
  • the JSON schema endpoint that the admin UI consumes at boot.

SQL-first migrations

Migrations are plain SQL files under migrations/:

migrations/
├── 0001_initial.up.sql
├── 0001_initial.down.sql
├── 0002_add_articles_index.up.sql
└── 0002_add_articles_index.down.sql

The CLI applies them in lexicographic order:

nucleus migrate
nucleus migrate status
nucleus migrate down --steps 1

nucleus generate migration <name> writes a stub for you. nucleus generate migration --from-model Article produces a CREATE TABLE based on the registered model metadata — useful as a starting point, not as the final word.

Querying

pkg/db exposes the underlying *sql.DB plus thin helpers. There is no custom query builder:

rows, err := a.DB.Query(ctx,
`SELECT id, title FROM articles WHERE author_id = ? ORDER BY id DESC`,
authorID)
if err != nil {
return err
}
defer rows.Close()

For repository-style code, pkg/db ships Scan/ScanAll helpers and a Tx wrapper that propagates context and panics safely.

Multi-tenant queries

When multi_tenant.enabled: true is set, App.DatabaseForRequest(r) resolves the database — and the tenant scope — for the current request. The admin panel auto-filters by tenant; for application code, you scope queries explicitly:

db := a.DatabaseForRequest(r)
tenantID := router.TenantFrom(r.Context())
rows, _ := db.Query(ctx, `SELECT * FROM articles WHERE tenant_id = ?`, tenantID)

Health and telemetry

pkg/db registers health checks and emits OpenTelemetry spans for every query when OTel is enabled. The admin panel surfaces the connection pool, the migration plan and the latest health status under /admin/system.