CLI Reference

new project

Generate a complete consumer project skeleton, with engine and database choices.
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

FlagRequiredDefaultMeaning
--module-pathyesnonethe project's Go module path: baked into go.mod and every generated import
--dirno./<name>target directory
--enginenofiberHTTP engine of the generated backend: fiber or chi
--dbnopgxdatabase stack of the generated backend: pgx or bun
--kit-replacenononewrite 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.

Treat --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 by new module): with Fiber it's fiberx.Run(...) and app.Group("/api/v1"); with chi it's chix.Run(...), a sub-router, and r.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-in chi-server templates.
  • The generated gen/ package and the registration wiring: the Register<Surface> function inserted by add surface takes a fiber.Router or a chi.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
}

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.yaml manifest (name, module path, engine, db, kit version, template checksum)
  • go.mod with the kit dependency and tool directives (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 to api/openapi/), package.json with 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 envconf
  • cmd/worker/main.go: the background-processing binary (worker); cmd/migrate/main.go: the migration runner
  • migrations/: embedded, timestamp-prefixed goose SQL, starting with 20200101000000_init.sql + 20200101000100_outbox.sql (the outbox table)
  • mise.toml tasks (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 the WORKDIR.
  • development: what compose builds (target: development); a host-matched UID/GID user (USER_ID/GROUP_ID build args from .env), the source bind-mounted (.:/src), CMD ["sh", "-c", "exec go run ./cmd/${CMD}"]. No source copy, no go mod download at build time: after a code change, docker compose restart <service> is enough, go run recompiles 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:

  • development target: the kit checkout is bind-mounted too, at the same absolute path the go.mod points at (<kit-checkout>:<kit-checkout>:ro), a runtime mount, no build-time copy; editing the kit only needs a restart too.
  • build/production target: compose additional_contexts: {kit: <path>}, the Dockerfile's build stage does COPY --from=kit / <path> before go 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.

Copyright © 2026