new project
gpsystem new project <name> --module-path <go-module-path> [flags]
This is how the shop sample app was created too:
gpsystem new project shop --module-path github.com/acme/shop --dir ./shop
Flags
| Flag | Required | Default | Meaning |
|---|---|---|---|
--module-path | yes | none | the project's Go module path: baked into go.mod and every generated import |
--dir | no | ./<name> | target directory |
--engine | no | fiber | HTTP engine of the generated backend: fiber or chi |
--db | no | pgx | database stack of the generated backend: pgx or bun |
--kit-replace | no | none | write a replace directive into go.mod pointing the kit at a local checkout (a relative path is made absolute); see Installation |
The global --dry-run (print the plan, write nothing) and --force (overwrite existing files) flags work here too, as on every subcommand.
The project <name> must be lowercase letters/digits starting with a letter. It seeds TypeSpec namespaces and appears in generated configs.
--module-path as permanent: every generated import builds on it, and every later generator writes it into new files. Changing it afterwards means rewriting all imports, so choose it up front.Engine and database choice
Both decisions are per-project and final: the manifest records them (engine:, db:), and every later generator (new module, add surface, add handler, upgrade templates) follows them: you pass the flag only once, here.
What differs per engine
Business code (handler/service/repository) is identical for both engines; the difference concentrates in three places:
- The module entry point (
cmd/<module>/main.go, generated bynew module): with Fiber it'sfiberx.Run(...)andapp.Group("/api/v1"); with chi it'schix.Run(...), a sub-router, andr.Mount("/api/v1", api)(see the server lifecycle and the two engine pages: Fiber, chi). - The oapi-codegen template set (
api/oapi-codegen/templates/): for Fiber it's the kit's full Fiber v3 template set (upstream oapi-codegen doesn't know v3); for chi it's only a minimal strict override, with routing coming from oapi-codegen's built-inchi-servertemplates. - The generated
gen/package and the registration wiring: theRegister<Surface>function inserted byadd surfacetakes afiber.Routeror achi.Router, and the enforcer/validator middleware chain is engine-specific too. The strict handler interface you implement is the same for both engines (context.Context+ request object), which is what keeps business code portable.
Manifests generated before this flag existed (with a router: field, or none at all) keep working: --engine used to be called --router, before the server / server/fiberx / server/chix split.
What differs per database
--db decides the stack under the repository layer; the pool is pgx in both cases:
// cmd/<module>/main.go: deps init
pool := pg.MustNewPool(ctx, cfg.DB)
deps := shop.Dependencies{
DB: pg.NewDB(pool),
Transactor: pg.NewTransactor(pool),
}
// register.go
type Dependencies struct {
DB *pg.DB
Transactor dbx.Transactor
}
// cmd/<module>/main.go: deps init
pool := pg.MustNewPool(ctx, cfg.DB)
bunDB := bunx.Open(pool) // bun query builder over the pgx pool
deps := shop.Dependencies{
DB: bunDB,
Transactor: bunx.NewTransactor(bunDB),
}
// register.go
type Dependencies struct {
DB *bun.DB
Transactor dbx.Transactor
}
The same difference shows up in cmd/worker/main.go and in the outbox store wired by add event (outbox.NewStore(pg.NewDB(pool)) ↔ outbox/bunx.NewStore(bunDB)). The repository templates emit SQL with pgx and query-builder calls with bun (see pgx and bun).
What it generates
gpsystem.yamlmanifest (name, module path,engine,db, kit version, template checksum)go.modwith the kit dependency andtooldirectives (gpsystem, oapi-codegen), so every teammate runs the same CLI version- TypeSpec workspace:
spec/typespec/{main.tsp, shared/errors.tsp, shared/paginator.tsp, lib/policy.tsp},tspconfig.yaml(pinned to OpenAPI 3.0, output toapi/openapi/),package.jsonwith a pinned TypeSpec compiler - The kit's oapi-codegen templates at
api/oapi-codegen/templates/(per engine, see above) internal/platform/config/config.go: composed env config (Server+DB+Worker+Outbox), loaded with envconfcmd/worker/main.go: the background-processing binary (worker);cmd/migrate/main.go: the migration runnermigrations/: embedded, timestamp-prefixed goose SQL, starting with20200101000000_init.sql+20200101000100_outbox.sql(the outbox table)mise.tomltasks (setup,spec,generate,dev,dev:deps,restart,dev:down,worker,migrate,logs,build,test,lint),Dockerfile(multi-stage, see below),.dockerignore,compose.yml(the full dev stack, see below),.env.example,.golangci.yml,.gitignore,README.md
The HTTP-side entry point (cmd/<module>/main.go) is created not by new project but by the first new module: one binary per module, and new module inserts its compose service too.
The compose dev stack
The generated compose.yml is a full, containerized dev stack (zero published ports): postgres, redis, mailpit, rustfs (S3-compatible object storage), and the migrate/worker/module binaries, all built from the project's Dockerfile, all exposed via Traefik labels on the external proxynet network (dual web/websecure routers, tls.certresolver=letsEncrypt).
Prerequisite: docker network create proxynet (one-time, shared across the workspace), docker compose ≥ 2.17 + BuildKit (for additional_contexts and the cache mounts).
Dockerfile: four stages
base:golang:1.25-alpine, just theWORKDIR.development: what compose builds (target: development); a host-matched UID/GID user (USER_ID/GROUP_IDbuild args from.env), the source bind-mounted (.:/src),CMD ["sh", "-c", "exec go run ./cmd/${CMD}"]. No source copy, nogo mod downloadat build time: after a code change,docker compose restart <service>is enough,go runrecompiles from the mounted source; the module caches (gomodcache/gocache) live on named volumes so the restart-recompile stays fast.build:ARG CMD+go build ./cmd/${CMD}, with cache mounts; the prod image is built from this (docker build --target production --build-arg CMD=<x> .).production:alpine:3.22,USER nobody, just the compiled binary.
--kit-replace in compose
A go.mod replace directive is invisible to the build context: two separate mechanisms handle it:
developmenttarget: the kit checkout is bind-mounted too, at the same absolute path thego.modpoints at (<kit-checkout>:<kit-checkout>:ro), a runtime mount, no build-time copy; editing the kit only needs a restart too.build/productiontarget: composeadditional_contexts: {kit: <path>}, the Dockerfile'sbuildstage doesCOPY --from=kit / <path>beforego mod download.
Neither block is generated for a non-replace (published-kit) project.
Module routing: path-based, not subdomain
The module service's Traefik rule is Host(`${APP_HOST}`) && PathRegexp(`^/api/v1/([a-z0-9-]+/)?<module>(/|$)`); this covers both the api surface's /api/v1/<module> and every other surface's /api/v1/<surface>/<module> route, so add surface never needs to touch it.
After generation
cd <name>
go mod tidy # resolves deps + tool directives
mise run setup # TypeSpec toolchain (npm)
go tool gpsystem new module <first-module>
mise run generate # tsp → OpenAPI → server code → build
cp .env.example .env # set USER_ID/GROUP_ID to id -u / id -g
docker network create proxynet # one-time
mise run dev # build + start the full dev stack
From here the CLI runs as the version pinned into the project (go tool gpsystem), and the manifest tells it which engine and database to generate for.