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.