dbx

Overview

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

dbx is a standalone Go module (github.com/gp-system/dbx): a database-agnostic contract, not a driver. 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. The root package 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. dbx works the same way with or without the kit, in any Go program; the gpsystem kit's new project/new module generators simply pick one of the two subpackages for you and wire it into the generated main.go.

Install

go get github.com/gp-system/dbx@v0.1.0 # the kit itself uses this tag
go get github.com/gp-system/dbx@latest

Go 1.25+. The root dbx package depends only on errs; everything driver-specific lives behind its own import, so the dependency you actually pull in depends on which subpackage you use:

Import pathAdds
github.com/gp-system/dbxerrs only: Config, Transactor, no driver
github.com/gp-system/dbx/pg+ pgx v5, otelpgx
github.com/gp-system/dbx/bunx+ bun, bun/dialect/pgdialect, bunotel
github.com/gp-system/dbx/seederrs only, same as the root package
github.com/gp-system/dbx/seed/bunx+ bun (via dbx/bunx)

A project that only ever imports dbx/pg never links bun into its binary, and vice versa.

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. A second, independent connection composes the same way, under its own prefix, loaded with envconf.LoadPrefixed:

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

which reads DB_ANALYTICS_HOST, DB_ANALYTICS_USER, and so on, entirely independent of the first connection's pool, transaction and error handling. In a generated project the add db generator wires this pattern for you: its own dbx.Config under DB_<NAME>_*, its own pgx pool, and <Name>DB / <Name>Transactor fields on the module's Dependencies.

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 below: 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. add surface --db binds a module's shared repository skeleton to a named connection; 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) when you use the kit's generators; the manifest records it and every later generator follows it: see new project. Used standalone, without the kit, the choice is simply which subpackage you import.

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
  • Seeders: idempotent baseline data with dbx/seed and dbx/seed/bunx
  • Migrations: numbered SQL files, embedded, with a project-owned cmd/migrate binary
Copyright © 2026