Getting Started

Introduction

What gpsystem is, who it's for, and what it leaves in your hands.

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 (app lifecycle, server on chi/net-http, worker on asynq, realtime on 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:

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.Router and 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.Run hands you a chi.Router: your handlers use the native net/http API;
  • dbx/pg returns pgx.Rows and pgxpool.Pool, dbx/bunx a bun.DB;
  • queue works 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 storage module's Driver/Disk split 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/sentryx wraps sentry-go: Sentry is switchable via env (SENTRY_DSN empty = fully off), and a binary that doesn't use it never links it;
  • the mail Mailer interface 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):

cmd/shop/main.go
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's http.ResponseWriter/*http.Request (or, in a generated handler, the codegen's typed request/response); server.Run wires middleware and lifecycle but does not wrap routing or request handling. Whatever chi and net/http 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. server.Run is a composition of signal.NotifyContext, listen, drain and an ordered cleanup list (the engine-agnostic app lifecycle), 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

Copyright © 2026