new project
This is the entry point into gpsystem: it generates a whole consumer project from scratch, and every other CLI command runs inside a project it created.
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 |
--db | no | pgx | database stack of the generated backend: pgx or bun |
The global --dry-run (print the plan, write nothing) and --force (overwrite existing files) flags work here too, as on every subcommand. Two more flags (--replace, --replace-dev) exist for developing gpsystem itself; see Contributor mode below, they are not part of ordinary project setup.
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.Database choice
The database stack is a per-project, final decision: the manifest records it (db:), and every later generator (new module, add surface, add handler) follows it: you pass --db only once, here. A single module can still add further, independent connections with a different connector via add db.
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,db, framework version, template checksum)go.modwith the framework and the standalone modules it needs asrequires, plustooldirectives (gpsystem, oapi-codegen), so every teammate runs the same CLI version and the same set of published module versions- 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 framework's oapi-codegen templates at
api/oapi-codegen/templates/: a single, chi-based set (upstream oapi-codegen's built-inchi-servertemplates plus the framework's strict-handler overrides) 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,logs:all,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, valkey, 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.
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.
Contributor mode: local module replaces
Everything above assumes the ordinary path: the framework and its 14 standalone modules are plain public Go modules, and new project requires none of them locally, go mod tidy resolves published versions from the module proxy. If you're developing gpsystem itself, or need a generated project to build against unpublished changes to one of the standalone modules, two additional flags let new project write go.mod replace directives at local checkouts instead:
| Flag | Meaning |
|---|---|
--replace <name>=<path> | repeatable; add a replace for one specific module (framework, errs, or any module name in the framework's module registry) |
--replace-dev <dir> | shorthand for a workspace layout where the framework, errs and every registry module are checked out as sibling directories under <dir>: equivalent to passing --replace for each of them individually |
All target paths are resolved relative to --dir, so the resulting go.mod, compose.yml and Dockerfile carry only relative paths, never anything machine-specific: the generated project stays portable as long as its position relative to the local checkouts doesn't change. Without either of these flags, new project generates a project with a plain require on the published version of every module it needs, no replace at all: this is the default, and the only path a consumer project should ever use.
The S3 storage driver, github.com/gp-system/minio-storage-driver, is a plain registry module like every other entry: --replace-dev expects it at its own sibling checkout, <dir>/minio-storage-driver, and --replace minio-storage-driver=<path> points it somewhere else explicitly. (It used to be a second module nested inside the storage checkout, with a replace derived from storage's own; that special case is gone.)
The generated compose.yml/Dockerfile follow the same replace map: for every module with a replace in effect, the development target bind-mounts that checkout at a fixed in-container path (a runtime mount, no build-time copy, so editing the checkout only needs a docker compose restart), and the build/production target adds a compose additional_contexts entry that the Dockerfile's build stage COPY --from=<module>s in before go mod download. A project with no replaces at all gets neither block.
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 database stack to generate for.
Logs
By default, mise run logs (short alias mise run log) follows only the
project's own module containers: the postgres, valkey, mailpit and
rustfs output is excluded. The filter is computed at run time from
compose.yml (every service with a build key that isn't a one-shot,
i.e. not restart: "no"), so adding a module later
(go tool gpsystem new module) needs no manual upkeep.
mise run logs # only the app modules (auth, chat, store, ...)
mise run logs auth chat # only the given services
mise run logs:all # every service, backing services included