Introduction
gpsystem is 14 standalone Go modules plus a thin generator/framework that wires them together:
- 14 standalone modules, each its own Go module, versioned independently of the rest, usable on its own in any Go program:
errs(stackable errors),envconf(typed config),httperr(+httperr/validate),dbx(+dbx/pg,dbx/bunx,dbx/seed),logx(+logx/monolog),paginate,queue,events(+events/outbox,events/scheduler),auth(+auth/rbac,auth/policy),mail(+mail/smtp,mail/mjml),notify(+notify/database,notify/broadcast),storage(+storage/local,storage/memory),minio-storage-driver(the S3-compatible storage driver),telemetry(+telemetry/otelx,telemetry/sentryx). - A thin framework (
github.com/gp-system/framework): four chassis packages that compose the modules into a running service (applifecycle,serveron chi/net-http,workeron asynq,realtimeon centrifuge), plus a CLI (go tool gpsystem) that scaffolds 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 logger, what graceful shutdown looks like, how you model errors, how a transaction crosses repositories. gpsystem does that work once, for everyone: it pulls the Go ecosystem's established libraries together into a single, maintained composition, spread across the 14 modules above, 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 modules cover all the base functionality you meet on most sites, and the framework wires them into a running service:
- 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 framework; your projects upgrade by bumping a single version in go.mod.
What gpsystem is NOT
- A framework without a container. gpsystem calls itself a framework because it owns the process lifecycle, the conventions and the generators, not because it owns your code. There is no IoC container, no mandatory Module interface, no framework-owned lifecycle hook that a bootstrap discovers and calls. You write
main(); every dependency is wired by hand, in explicit code. The framework'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;
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 framework's principle: centralize, don't abstract. Access to external dependencies is concentrated in one module each, but their types flow freely through the public API:
- the register function of
server.Runhands you achi.Router: your handlers use the nativenet/httpAPI; dbx/pgreturnspgx.Rowsandpgxpool.Pool,dbx/bunxabun.DB;queueworks with*asynq.Task, and the worker's escape hatches (WithAsynqConfig,WithMux) hand you raw asynq.
Upstream documentation and Stack Overflow answers stay valid for you as a result, and there is no framework-specific wrapper API to learn. gpsystem departs from this in three places, each for a stated reason:
- the
storagemodule'sDriver/Disksplit hides the AWS SDK behind a small backend contract, so your application code doesn't have to import the AWS SDK for a file upload; telemetry/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, pgx/bun chosen by import, every module usable with none of the others. The Architecture page shows module by module what depends on what.
The ~15-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 := server.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
}, server.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 router API stays native. Your handlers receive
net/http'shttp.ResponseWriter/*http.Request(or, in a generated handler, the codegen's typed request/response);server.Runwires middleware and lifecycle but does not wrap routing or request handling. Whateverchiandnet/httpcan 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.
server.Runis 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 on top of chi. 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.