Skip to content

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.