Skip to content
SHC Docs

Migrations

This workflow runs in the ce repo, not here. The split moved ent/, migrations/ and atlas.hcl there, and the make migrate target moved with them — every command below is run from a ce checkout. This repo consumes ce as a pinned Go module and the daemon applies the embedded migrations at startup, so there is nothing schema-related to run on the shc side. File paths below are ce-repo-relative.

SHC uses Atlas (ariga/atlas) with Ent (entgo.io/ent) as the schema source. Tables are declared as Go structs under ent/schema/; Atlas reads those declarations directly (via the ent:// URL provider) and generates versioned migration files. The SHC daemon applies the generated migrations/*.sql at startup via the runner at core/db/migrate.go.

Atlas is a dev-time tool only — operators don’t need it installed on production hosts.

ent/
generate.go # go generate ./ent entrypoint
entc.go # codegen driver
schema/
<table>.go # one file per table (30 schemas)
<table>.go, <table>_create.go, … # generated typed client (don't edit)
atlas.hcl # src = "ent://ent/schema"
migrations/
20260520000000_initial.sql # bootstrap migration for new installs
atlas.sum # integrity manifest (checksums)
embed.go # //go:embed *.sql for the runtime

ent/schema/*.go is the source of truth. The Go schema definitions drive both the typed CRUD client (used by every module’s repo via core/entclient) AND the migration generation.

  1. Edit ent/schema/<table>.go (add / remove / change fields, edges, indexes).
  2. Run go generate ./ent to regenerate the typed client.
  3. Run make migrate NAME=<slug> — Atlas materializes the ent declarations into a fresh sqlite dev DB, diffs it against the current migration head, and writes migrations/<UTC-timestamp>_<slug>.sql plus an updated atlas.sum. (NAME defaults to auto if omitted.)
  4. Review the generated SQL, commit alongside the schema change.
Terminal window
# Edit schema
$EDITOR ent/schema/deployment.go
# Regenerate typed client
go generate ./ent
# Generate migration
make migrate NAME=add_deployment_priority
# Verify
sqlite3 .shc/state/shc.db ".schema"

The daemon applies migrations automatically on startup — there is no make target for applying. For ad-hoc local runs use atlas directly:

Terminal window
atlas migrate apply --env local --url "sqlite://${PWD}/.shc/state/shc.db"

Every ent/schema/<table>.go:

  • Type name = table CamelCase (mesh_tokenMeshToken, storage_provider_nodeStorageProviderNode).
  • Annotations() pins the SQL table name explicitly via entsql.Annotation{Table: "<name>"} — Ent pluralizes by default.
  • VARCHAR(N) UUID PKs: redeclare via field.String("id").MaxLen(N).Immutable().
  • VARCHAR(N) non-UUID PKs: use field.String("id").StorageKey("<col>").Immutable().
  • Field names match column names verbatim (snake_case); Ent derives Go field names from those.
  • Edges: parent declares edges.To("children", Child.Type) with Annotations(entsql.OnDelete(...)); child declares edges.From("parent", Parent.Type).Ref("children").Field("col").Unique().

The canonical exemplar lives in ent/schema/kv.go.

  • Forward-only — Atlas does not generate down migrations and SHC does not roll back in production. Restore from a backup instead.
  • Small migrations — one logical change per migration.
  • Data migrations — separate from schema migrations when possible. Schema migrations are SQL; data migrations belong in Go startup code (core/db/maintenance.go style).
  • No breaking changes — add columns as nullable first, migrate data, then add constraints in a follow-up migration.

rqlite replicates plain SQLite statements over raft, so the same ALTER TABLE caveats apply on both engines:

  • ADD COLUMN, CREATE INDEX, DROP INDEX — supported directly.
  • ALTER COLUMN, DROP COLUMN — require table recreation (create-new + copy + drop-old + rename pattern). Atlas’s declarative diff handles this for you automatically when you edit the Ent schema and run make migrate.