The shop sample application
The documentation's examples come from one coherent project: shop is a webshop backend with two surfaces, a worker binary, events and a scheduled job. You don't need to build it to read the docs, but every page references this project's files, so the details add up to one coherent whole.
What it models
- The
apisurface (public): product listing with cursor pagination, placing an order (PlaceOrder), with validation, a policy check and a transaction. - The
adminsurface: product CRUD and image upload to object storage, behindRequireRole("admin"). - The
orderPlacedevent: dispatched from the order-placement transaction, reaching the queue through the outbox. - The
sendOrderConfirmationlistener: runs in the worker, sends an MJML e-mail. - The
nightlySalesReportjob: runs every night, replica-safe.
How it's generated
The entire scaffold comes from the CLI: with exactly these commands:
go tool gpsystem new project shop --module-path github.com/acme/shop \
--dir ./shop # --engine chi and/or --db bun also available
cd shop
go tool gpsystem new module shop # api surface, by default
go tool gpsystem add surface shop admin
go tool gpsystem add event shop orderPlaced
go tool gpsystem add listener shop orderPlaced sendOrderConfirmation --queue mail
go tool gpsystem add job shop nightlySalesReport --cron "0 3 * * *"
go tool gpsystem new migration create_products_and_orders
mise run generate # tsp → OpenAPI → server code → build
The shape of the project
Every file below is exactly what the generator + your business logic produce, click through the tree. The api surface mounts at the module root (/api/v1/shop/...), admin under its own prefix (/api/v1/admin/shop/...), see add surface. The shared ordering rule lives in the module's core/ package and the persistence in the module-level repository/; the api surface's service/ is a thin seam over them. The generated // gpsystem:* anchor comments are mostly omitted for readability; in your own project leave them in place, later add surface / add event / add listener calls insert at those points.
package main
import (
"context"
"log"
"github.com/gofiber/fiber/v3"
"github.com/gp-system/gpsystem/dbx/pg"
"github.com/gp-system/gpsystem/envconf"
"github.com/gp-system/gpsystem/events/outbox"
"github.com/gp-system/gpsystem/server/fiberx"
"github.com/acme/shop/internal/modules/shop"
"github.com/acme/shop/internal/platform/config"
)
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),
Dispatcher: outbox.NewDispatcher(outbox.NewStore(pg.NewDB(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)
}
}
The cmd/shop/main.go above uses the Fiber engine (the default). With --engine chi, only the HTTP entry point and the wiring inside register.go's Register<Surface> functions change; everything else in the tree is identical:
package main
import (
"context"
"log"
"github.com/go-chi/chi/v5"
"github.com/gp-system/gpsystem/dbx/pg"
"github.com/gp-system/gpsystem/envconf"
"github.com/gp-system/gpsystem/events/outbox"
"github.com/gp-system/gpsystem/server/chix"
"github.com/acme/shop/internal/modules/shop"
"github.com/acme/shop/internal/platform/config"
)
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),
Dispatcher: outbox.NewDispatcher(outbox.NewStore(pg.NewDB(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)
}
}
See the Fiber and chi pages for the two register.go wiring variants.
Follow the order
The path of a POST /api/v1/shop/orders request through the system, step by step, with the page that documents each hop:
| # | What happens | Documented at |
|---|---|---|
| 1 | The generated code binds and validates the request | Validation |
| 2 | The enforcer middleware evaluates the orders.place permission and policy | Policies |
| 3 | The service delegates to the module core; inside WithinTransaction: order insert + stock decrement + outbox row | Transactions, Outbox |
| 4 | The relay forwards the orderPlaced event to the Redis queue | Queue |
| 5 | The worker fans out to listeners; sendOrderConfirmation sends the e-mail | Worker, Events, Mail |
| 6 | The whole path is a single trace, from the HTTP request to the e-mail | OpenTelemetry |
| 7 | On failure: the errs chain → problem+json to the client, stack to Sentry | Error model, Sentry |
Next step: your first module, the same journey, sitting at your own machine.