Overview
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, 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.
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 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. 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 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 - Migrations: numbered SQL files, embedded, with a project-owned
cmd/migratebinary