Skip to content

Database

Rewritten 2026-09-20. This page used to describe two separate connections (db.GetDB()/coreapi and db.GetZooDB()/johorzoo) and told new domains to pick one. That model was deleted on 2026-09-09 (Phase 5, commit 47fe98e and follow-ups) — db.GetZooDB() no longer exists anywhere in the codebase (grep -rn "GetZooDB" . across all of CoreAPI returns zero matches). If you're relying on cached knowledge of this page from before 2026-09-20, the connection model changed underneath it — re-read this page, don't skim it.

CoreAPI connects to one PostgreSQL database on startup. Your domain doesn't connect to it directly — it receives a connection passed in from main.go/ticketing.go and uses it without knowing where it came from.


One connection, two schemas

pkg/db/db.go opens a single connection, documented in that file's own header comment (treat it as the source of truth if this page ever drifts again):

DB → the coreapi database, holding both schemas:
     public   — CoreAPI-native tables (media, streamers, channels, devices...)
     johorzoo — ported ticketing tables (orders, customers, reports...)

There used to be a second connection, ZooDB, to a genuinely separate johorzoo database. Phase 5 made johorzoo a schema inside coreapi instead, so every domain — native or ticketing — now shares one pool and one transaction boundary. db.GetDB() is the only getter; there is nothing to choose between anymore.

Ticketing models declare their schema in their own TableName() method rather than relying on search_path:

func (TicketGroup) TableName() string { return "johorzoo.ticket_group" }

search_path is deliberately left at the Postgres default (not widened to include johorzoo) — with both schemas reachable from one connection, an unqualified table name would resolve by search order, and a future native-schema table sharing a ticketing table's name would silently start capturing its queries. Fully-qualified TableName() on every ticketing model avoids that entirely.

The DSN is built from the usual environment variables — DB_HOST, DB_PORT, DB_USER, DB_PASS, DB_NAME. There is no second set of TICKETING_DB_* variables anymore.


Assigning a database to your domain

There's only one connection to assign now — every Setup() call in main.go/ticketing.go receives db.GetDB():

// main.go — native domains
mediaroutes.Setup(v1, app, db.GetDB(), storage.GetClient())
distributionroutes.Setup(v1, db.GetDB())
playbackroutes.Setup(v1, db.GetDB())

// ticketing.go — ticketing domains, same connection
coreDB := db.GetDB()

The domain's Setup() in routes/wire.go receives it as *gorm.DB — a plain GORM connection with no knowledge of which schema it's about to touch:

// domain/your-domain/routes/wire.go

func Setup(r fiber.Router, db *gorm.DB) {
    cfg := yourdomain.ConfigFromEnv()
    h := handlers.NewHandler(db, cfg)
    registerRoutes(r, h)
}

It flows from there into NewHandler, where it's stored as h.db and used in every handler method:

// domain/your-domain/handlers/handler.go

type Handler struct {
    db  *gorm.DB
    cfg yourdomain.Config
}

func NewHandler(db *gorm.DB, cfg yourdomain.Config) *Handler {
    return &Handler{db: db, cfg: cfg}
}

Your handler methods just call h.db — no awareness of which schema is involved, no imports from pkg/db:

func (h *Handler) List(c *fiber.Ctx) error {
    var items []yourdomain.YourModel
    h.db.Find(&items)
    return response.OK(c, items)
}

If your model lives in johorzoo, give it a TableName() method that says so; if it's a native table, no TableName() override is needed and GORM's default (unqualified, resolving in public) is correct.


Schema migration — one mechanism, all domains

This also changed as part of the same wave. Every GORM model CoreAPI owns — native and ticketing alike — is registered in one place, registerModels() in models.go, and migrated through the same RegisterModels/AutoMigrate pass CoreAPI has always used for native domains:

// models.go
// One mechanism owns the whole schema. The ticketing block at the bottom
// (johorzoo schema) goes through the same AutoMigrate pass as media,
// distribution and everything else — no second policy, no separate tool.

Before 2026-09-09, ticketing tables came only from a production SQL dump (postgres-init/) with no migration path in CoreAPI at all — a ticketing model added after that dump had no way into an existing database. That gap is closed: add a field to a ticketing model today and it migrates the same way a native-domain field would. Run migration/schema-diff.sh after touching any ticketing model — it runs AutoMigrate against a copy of the live schema and diffs, so the diff is exactly what your change would do to production.

Things GORM's plain AutoMigrate still can't do on its own — dropping a column, adding a real FOREIGN KEY constraint, backfilling a new NOT NULL column against existing rows — are handled by small, idempotent, imperative functions that run on every boot (see pkg/db/ticketing_migrate.go's ensureTicketingForeignKeys/ensurePlatformProfileForeignKeys for the established pattern; dropped 18 legacy credential columns from ticket_group_integration_config, 2026-09-18, is a recent real example). This is the pattern to follow, not a one-off table drop, if you need AutoMigrate-incompatible schema surgery.

When to reach for pkg/db

You almost never should. Every domain receives its *gorm.DB already wired via Setup() — importing pkg/db directly from inside a domain is a sign something's wired wrong. The only files that call db.GetDB() are main.go, infra.go, and ticketing.go, at the root.