Database

Overview

The dbx package, the database-agnostic contract your code depends on, and the two implementations you choose by import.

dbx is the kit's database-agnostic contract. It defines exactly two things: Config, which describes a PostgreSQL connection, and the Transactor interface services depend on to span multiple repositories in one transaction. It has no database driver dependency of its own: the actual implementation lives in a subpackage, and you pick one by importing it:

  • dbx/pg: the pgx-native stack (the default): pool construction, the DBTX query executor, and a Transactor backed by pgx.Tx.
  • dbx/bunx: an optional adapter for projects that prefer the bun query builder, backed by bun.Tx.

Only one is ever linked into a given binary. Your service and repository layers depend on dbx.Transactor and dbx.Config, never on the concrete implementation. That is why a service's code is identical to the letter whether pgx or bun runs underneath.

import "github.com/gp-system/gpsystem/dbx"

Config

dbx.Config describes a single PostgreSQL connection. You compose it into the application's config struct under a prefix:

type Config struct {
    DB dbx.Config `envPrefix:"DB_"`
}

which maps to the following environment variables (a generated project's .env.example lists exactly these):

Env variableFieldDefault
DB_HOSTHostlocalhost
DB_PORTPort5432
DB_USERUserrequired
DB_PASSWORDPasswordrequired
DB_NAMEDatabaserequired
DB_SSLMODESSLModedisable
DB_MAX_CONNSMaxConns10

The DSN() method renders the config as a postgres:// connection URL. It is consumed by pg.NewPool and by the generated cmd/migrate binary alike. Loading is done by envconf, with .env support.

There is no intermediate config file: the env variables load straight into a typed struct, and a missing required value fails at startup, not on the first query.

Named connections

A module's default connection, cfg.DB composed under DB_*, covers the common case: one project, one database, one connector. When a module genuinely needs a second connection (an analytics warehouse, a legacy database, a read replica reached through its own pool), the add db generator adds it: its own dbx.Config under DB_<NAME>_*, its own pgx pool, and <Name>DB / <Name>Transactor fields on the module's Dependencies, independent of the default connection and of each other.

The connector (pgx or bun) is chosen per connection, not per project: a module can mix a pgx-native default with a bun-backed named connection, or the reverse. This follows directly from the Transactor boundary above: WithinTransaction on one connection never spans another, so there is no cross-connection transaction to keep consistent, and no reason to force every connection onto the same stack.

type Config struct {
    DB          dbx.Config `envPrefix:"DB_"`
    DBAnalytics dbx.Config `envPrefix:"DB_ANALYTICS_"`
}

add surface --db binds the module's shared repository skeleton to a named connection, splitting the repository/ directory into repository/<name>/. Without it the module keeps the flat layout. cmd/migrate only ever targets the default connection.

Transactor

type Transactor interface {
    WithinTransaction(ctx context.Context, fn func(ctx context.Context) error) error
}

This is the single interface application code manages transactions through. The transaction is carried by the context, so every database call inside fn, through dbx/pg's DB or dbx/bunx's resolvers, joins it automatically, without the repositories knowing about it. Commit on nil, rollback on error or panic; a nested call joins the outer transaction. The full semantics and the pattern are covered on Transactions.

The transaction travels in the context, not in a global connection manager.

Which one to pick: pgx or bun?

The choice is per project and made at generation time (--db pgx|bun, default pgx); the manifest records it and every later generator follows it: see new project.

dbx/pg (pgx)dbx/bunx (bun)
Writing querieshand-written SQL (Exec/Query/QueryRow)bun query builder (NewSelect, NewInsert, …) + raw SQL escape hatch
Returned typespgconn.CommandTag, pgx.Rows, pgx.Rowbun.IDB, bun models, sql.Result
sqlcdirectly compatible (DBTX is identical)in database/sql mode, through the Conn resolver
Transaction carrierpgx.Tx in the contextbun.Tx in the context
Good whenyou want full control over the SQL, minimal layersstruct-based models and query-builder comfort

Rule of thumb: if you like writing SQL, stay with pgx: fewer layers, fewer surprises. If you want a query builder with struct-based models, bun will feel familiar, but it is not an ActiveRecord/Eloquent-style ORM, and that is worth knowing up front.

Deliberately no ORM abstraction

The kit's principle applies here too: centralize, don't abstract. dbx does not define a common query interface over the two stacks. dbx/pg returns pgx.Rows, dbx/bunx returns bun.IDB. The upstream pgx and bun documentation, examples and Stack Overflow answers apply to your project unchanged. The common denominator is deliberately minimal: the connection description (Config) and the transaction boundary (Transactor), because those two are what the service layer needs to depend on.

Schema management follows from this too: there is no schema builder, and migrations are plain, embedded SQL files.

Pages in this section

  • pgx: the default stack: pool, DBTX, DB, repositories
  • bun: the optional query builder adapter
  • Transactions: the Transactor pattern in depth, with the PlaceOrder example
  • Migrations: numbered SQL files, embedded, with a project-owned cmd/migrate binary
Copyright © 2026