Introduction
gpsystem is a service toolkit for building modular Go backends. It has two halves:
- A library (
github.com/gp-system/gpsystem): the service chassis, application lifecycle, an HTTP layer on Fiber v3 or chi, telemetry, an error model, validation, configuration, database access with context-carried transactions, background processing (queue, events, scheduler, worker), JWT auth, RBAC and a policy layer, object storage, e-mail, pagination. - A CLI (
go tool gpsystem): generators that scaffold projects, modules, surfaces, handlers, events, listeners and jobs, including their TypeSpec contract files.
Why it exists
The Go ecosystem's strength is its small, independent, well-tested libraries. The tradeoff is that every project makes the same ten decisions from scratch: which HTTP engine, which logger, what graceful shutdown looks like, how you model errors. gpsystem does that work once, for everyone: it pulls the Go ecosystem's established libraries together into a single, maintained composition, instead of writing its own in their place.
Coming from a PHP background? The Coming from Laravel and Coming from Symfony pages map the two worlds concept by concept.
The kit covers all the base functionality you meet on most sites:
- RBAC: role- and permission-based access control
- A policy layer for when RBAC isn't enough: per-request ownership and state checks
- Graceful shutdown: orderly termination that drains in-flight requests
- Scheduling: cron and intervals, replica-safe
- Background workers: an asynq-based processing binary
- Scalability: the transactional outbox and the scheduler's leader election behave correctly across replicas
- Transactions: context-carried, cross-repository transaction handling
- Network file storage: S3-compatible object storage (AWS, MinIO, RustFS)
- Detailed metrics: HTTP, database and queue instrumentation over OTLP
- Monolog-style dev logging: the readable console log you know from PHP, during development
- Sentry: error tracking with issue grouping by
errscode - OpenTelemetry: traces, metrics and logs with one call
- A validator: struct-tag based request validation with RFC 9457 error responses
- Typed configuration: env vars loaded into structs, with
.envsupport - Stackable error handling: errors that compose like
fmt.Errorf, carrying stacks, error codes and client-safe messages - Migrations: embedded SQL with its own
cmd/migratebinary - Code generation: typed server code from a TypeSpec contract
Each is written and tested once in the kit; your projects upgrade by bumping a single version in go.mod.
What gpsystem is NOT
- Not a framework. There is no IoC container, no mandatory Module interface, no kit-owned lifecycle hook that a bootstrap discovers and calls. You write
main(); every dependency is wired by hand, in explicit code. The kit's one inversion is the process lifecycle (Run(ctx, cfg, register)): it signals, shuts down, flushes, and never touches business wiring. See Architecture for exactly where that boundary sits. - Not a platform. gpsystem doesn't own your infrastructure: no proprietary deploy model, no cloud, no runtime;
fiber.App,chi.Routerand the pgx pool remain yours. - Not an ORM. There is no Eloquent- or Doctrine-style simulation; the repository layer writes SQL (pgx) or uses a query builder (bun), with explicit queries.
- Generated code is yours. What the CLI scaffolds, you edit like any other code, except for the narrow, deliberately regenerated zone (the TypeSpec→OpenAPI→oapi-codegen transport stubs under
gen/), which you don't hand-edit but drive by changing the spec.
It does not hide your dependencies
The kit's principle: centralize, don't abstract. Access to external dependencies is concentrated in one kit package each, but their types flow freely through the public API:
- the register function of
fiberx.Runhands you a*fiber.App, that ofchix.Runachi.Router: your handlers use the native API; dbx/pgreturnspgx.Rowsandpgxpool.Pool,dbx/bunxabun.DB;queueworks with*asynq.Task, and the worker's escape hatches (WithAsynqConfig,WithMux) hand you raw asynq.
This is deliberate: upstream documentation and Stack Overflow answers stay valid for you, and there is no kit-specific wrapper API to learn. The kit departs from this in three places, each for a stated reason:
- the
storage.Storeinterface hides the AWS SDK, so your application code doesn't have to import the AWS SDK for a file upload; sentryxwraps sentry-go: Sentry is switchable via env (SENTRY_DSNempty = fully off), and a binary that doesn't use it never links it;- the
mailMailerinterface fronts go-mail and mjml-go, so tests can swap in an in-memory mailer.
The parts are replaceable and omittable: dbx/pg works without server (say, in a CLI tool), Sentry and OTel can be switched off via env, the engine is chosen per project, pgx/bun by import. The Architecture page shows package by package what depends on what.
The ~20-line main.go
A module's entry point is this (graceful shutdown, telemetry, error rendering and validation already wired):
func main() {
cfg := envconf.MustLoad[config.Config]()
ctx := context.Background()
pool := pg.MustNewPool(ctx, cfg.DB)
err := fiberx.Run(ctx, cfg.Server, func(app *fiber.App) error {
api := app.Group("/api/v1")
deps := shop.Dependencies{
DB: pg.NewDB(pool),
Transactor: pg.NewTransactor(pool),
}
shop.RegisterApi(api, deps)
shop.RegisterAdmin(api, deps)
return nil
}, fiberx.WithCloser("pgxpool", func(context.Context) error {
pool.Close()
return nil
}))
if err != nil {
log.Fatal(err)
}
}
func main() {
cfg := envconf.MustLoad[config.Config]()
ctx := context.Background()
pool := pg.MustNewPool(ctx, cfg.DB)
err := chix.Run(ctx, cfg.Server, func(r chi.Router) error {
api := chi.NewRouter()
deps := shop.Dependencies{
DB: pg.NewDB(pool),
Transactor: pg.NewTransactor(pool),
}
shop.RegisterApi(api, deps)
shop.RegisterAdmin(api, deps)
r.Mount("/api/v1", api)
return nil
}, chix.WithCloser("pgxpool", func(context.Context) error {
pool.Close()
return nil
}))
if err != nil {
log.Fatal(err)
}
}
This is the entry point of the shop sample application. The documentation's examples build on that project throughout.
What stays in your hands
- The engine API stays native. On Fiber your handlers receive
fiber.Ctx, on chinet/http;fiberx.Run/chix.Runwire middleware and lifecycle but do not wrap routing or request handling. Whatever the engine can do, your app can do. - Generated code is yours. The scaffold lands in your repo and you edit it like any other code. The generators never overwrite an existing file and only touch marked anchor points (
// gpsystem:*). There is no runtime interpreting your project. - Thin by design.
fiberx.Run/chix.Runare a composition ofsignal.NotifyContext, listen, drain and an ordered cleanup list (the engine-agnosticapplifecycle), exactly the code you'd otherwise write per service, maintained in one place.
The shape of a consumer project
Every module gets its own entry point (cmd/<module>/main.go) and runs as its own process. Deploy them separately or together, as you like. The API contract is spec-first: TypeSpec compiles to OpenAPI 3.0, from which a strict, typed server interface is generated (for Fiber and chi alike). Your handler implements a generated interface, so drifting from the spec is a compile error.
Where to next
- Coming from a PHP background? The Coming from Laravel and Coming from Symfony pages map the two worlds concept by concept.
- The shop sample application shows how it all comes together as one project.
- Or dive straight in with Installation.