Overview
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, theDBTXquery executor, and aTransactorbacked bypgx.Tx.dbx/bunx: an optional adapter for projects that prefer the bun query builder, backed bybun.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 path | Adds |
|---|---|
github.com/gp-system/dbx | errs 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/seed | errs 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 variable | Field | Default |
|---|---|---|
DB_HOST | Host | localhost |
DB_PORT | Port | 5432 |
DB_USER | User | required |
DB_PASSWORD | Password | required |
DB_NAME | Database | required |
DB_SSLMODE | SSLMode | disable |
DB_MAX_CONNS | MaxConns | 10 |
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.
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 queries | hand-written SQL (Exec/Query/QueryRow) | bun query builder (NewSelect, NewInsert, …) + raw SQL escape hatch |
| Returned types | pgconn.CommandTag, pgx.Rows, pgx.Row | bun.IDB, bun models, sql.Result |
| sqlc | directly compatible (DBTX is identical) | in database/sql mode, through the Conn resolver |
| Transaction carrier | pgx.Tx in the context | bun.Tx in the context |
| Good when | you want full control over the SQL, minimal layers | struct-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
Transactorpattern in depth, with thePlaceOrderexample - Seeders: idempotent baseline data with
dbx/seedanddbx/seed/bunx - Migrations: numbered SQL files, embedded, with a project-owned
cmd/migratebinary