Migrations
CoreAPI uses GORM's AutoMigrate to create and update database tables on startup. There are two migration paths depending on which database your tables live in.
Which path do I use?
| Database | Tables | How to migrate |
|---|---|---|
coreapi |
media, streamers, channels, devices... | RegisterModels + RunMigrationMain |
johorzoo |
orders, customers, reports... | SQL dump files in docker/postgres/ |
If you're building a new CoreAPI domain, you use RegisterModels. You never touch the johorzoo SQL files unless you're working on the ticketing system.
How it works — CoreAPI tables
On every startup, main.go runs this sequence:
db.RegisterModels(
&mediamodels.Media{},
&mediamodels.UploadToken{},
&distributionmodels.Streamer{},
)
db.Init() // connects to DB, runs platform migrations
db.RunMigrationMain() // runs AutoMigrate for your registered models
RegisterModels must be called before db.Init(). Order matters.
What you touch
Two files — your domain's models.go and main.go.
1. domain/your-domain/models.go
Define your struct with GORM tags. Always implement TableName().
package yourdomain
import "gorm.io/gorm"
type YourModel struct {
gorm.Model // adds ID (uint), CreatedAt, UpdatedAt, DeletedAt (soft delete)
Name string `gorm:"type:text;not null"`
Status string `gorm:"type:varchar(20);default:'active';index"`
// pointer = nullable column
Description *string `gorm:"type:text"`
Width *int `gorm:"type:int"`
}
func (YourModel) TableName() string { return "your_models" }
2. main.go
Register your models before db.Init():
db.RegisterModels(
&existingmodels.ExistingModel{},
&yourdomainmodels.YourModel{}, // ← add here
)
db.Init()
db.RunMigrationMain()
Import your domain root package with an alias:
import (
yourdomainmodels "mashup.castis.io/playtelly/CoreAPI/domain/your-domain"
)
gorm.Model vs manual Base vs UUID
// Option A — gorm.Model (recommended for most new domains)
// Gives you: ID (uint autoincrement), CreatedAt, UpdatedAt, DeletedAt (soft delete)
type Media struct {
gorm.Model
Title string `gorm:"type:text;not null"`
}
// Option B — manual Base struct (used in distribution, playback)
// Same as gorm.Model but defined explicitly — use when you want control
type Streamer struct {
Base // ID uint, CreatedAt, UpdatedAt, DeletedAt
Name string `gorm:"type:varchar(100);not null"`
}
// Option C — UUID primary key (used in platform models)
type Organization struct {
ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
CreatedAt time.Time
UpdatedAt time.Time
Name string `gorm:"type:varchar(255);not null"`
}
Nullable vs non-nullable
Use a pointer for nullable columns, a value type for non-nullable:
// Non-nullable — zero value stored if empty ("", 0, false)
Name string `gorm:"type:text;not null"`
Status string `gorm:"type:varchar(20);default:'active'"`
// Nullable — NULL stored if not set
Description *string `gorm:"type:text"`
StartDate *time.Time `gorm:"type:date"`
Width *int `gorm:"type:int"`
GORM tag reference
// Column types
`gorm:"type:text"`
`gorm:"type:varchar(255)"`
`gorm:"type:jsonb;default:'{}'"`
`gorm:"type:uuid;default:gen_random_uuid()"`
`gorm:"type:timestamptz"`
// Constraints
`gorm:"not null"`
`gorm:"default:'active'"`
`gorm:"default:true"`
`gorm:"check:status IN ('active','inactive')"`
// Indexes
`gorm:"index"`
`gorm:"uniqueIndex"`
`gorm:"uniqueIndex:idx_name"` // named composite unique index
What AutoMigrate does and doesn't do
Does: create tables that don't exist, add new columns, add new indexes.
Does not: drop columns you remove from the struct, rename columns, change column types.
For destructive changes (drop, rename), write a manual SQL migration and run it against the container directly — or, for a fast-iterating/disposable environment, see the hard-reset option below instead of hand SQL.
Hard-resetting a domain's tables
For an environment where the schema is still churning heavily and
existing rows aren't worth preserving across a deploy (e.g. staging,
pre-launch) — pkg/db has:
func HardResetTables(tables ...string) error
Drops the given tables (CASCADE, so anything FK'd to them goes too) if
they exist. Call it before RunMigrationMain() — the next
AutoMigrate then recreates each table fresh from whatever the Go
struct currently says, rather than trying (and sometimes failing) to
ALTER an existing one into the new shape.
Gate every call behind an env var that defaults off — this is destructive by design. The one live example (media domain):
// main.go, after db.Init(), before db.RunMigrationMain()
if os.Getenv("MEDIA_HARD_RESET") == "true" {
db.HardResetTables(
"media_slides", "media_playlist_items", "media_playlists",
"transcoding_profile_versions", "transcoding_profiles",
"upload_tokens", "media",
)
}
Scoped to exactly the table names passed in — no other domain is
touched. Follow the reset with your domain's own seed.Run call so the
now-empty tables get fresh seed data back immediately; see
pkg/seed for the seeding side of this, including
a caveat about its separate (unconditional, not gated) prune step.
Danger
Never set the gating env var to true anywhere the affected tables'
data needs to survive a deploy. This is a full wipe of every listed
table, every time the flag is on — there's no partial/selective mode.
Do not add new models to RunMigrations() in pkg/db/migrate.go
That function still runs at startup for existing platform models and is in the process of being migrated. New domain models go in RegisterModels in main.go only — this keeps domain models owned by their domain package, not by pkg/db.