new module
go tool gpsystem new module <name> [--surface <name>]
Run inside a project (the CLI walks up to gpsystem.yaml). The module name must be lowercase letters/digits: it becomes a Go package name, directory name and URL segment. The same rule applies to the first surface's name (--surface, default api; the old --domain flag still works as a deprecated alias).
What it generates
cmd/<name>/main.go: the ~20-line entry point callingfiberx.Run(orchix.Runfor--engine chiprojects)internal/modules/<name>/register.go:Dependenciesstruct + one exportedRegister<Surface>(router, deps)function per surface (e.g.RegisterApi,RegisterWeb)- One first surface (default
api) undersurfaces/<surface>/: a handler implementing the generated strict interface (its sampleListcalls a minimalservice/seam), apolicy/registry stub, plus a kept-emptyhttp/mapper/directory (.gitkeep) for your own code - The module-level shared skeletons:
core/(home of the rules, entities and sentinel errors shared by the surfaces, with anErrNotFoundexample) andrepository/(the module's single persistence layer); the first surface stamps these, lateradd surfacecalls skip them spec/typespec/modules/<name>/:<surface>.tspwith a sample operation +models/<surface>.tspapi/oapi-codegen/<name>-<surface>.yamlcodegen config wired to the project's template set- The module's app service in
compose.yml: built from the project'sDockerfile(target: development), wired withdepends_onon postgres/redis/migrate, dual Traefik routers with path-based routing (see new project)
Anchor insertions
main.tsp← module imports (gpsystem:modules)cmd/<name>/main.go← the<name>.Register<Surface>(api, deps)call (gpsystem:registrations)register.go← a per-surfaceRegister<Surface>function + imports (rendered, then goimports-formatted)compose.yml← the module service at thegpsystem:servicesanchor
Compose backfill on an older project
In a project freshly generated by new project, compose.yml and Dockerfile already exist, so new module only inserts the module service. On an older project (generated before the compose stack was introduced), new module backfills what it can:
- Creates
Dockerfileand.dockerignoreif they're missing. - Inserts the
mailpit/rustfs/rustfs-initbacking services at thegpsystem:servicesanchor (idempotent, re-running does not duplicate them). - If
compose.ymlhas a postgrespg_isreadyhealthcheck, the module service is inserted at the anchor too; if not (aservice_healthydependency on a healthcheck-less postgres would break compose), the module service's YAML is only printed to the terminal for manual insertion. - The top-level
networks:/volumes:section (proxynet, rustfsdata, gomodcache, gocache) has no anchor either, so it's also only printed; insert it by hand. - If
.env.examplestill hasDB_HOST=localhost, a warning line flags that the app now runs inside compose andDB_HOSTshould becomepostgres.
If the backfill's printed-manual-insertion fallbacks pile up (no pg_isready healthcheck, missing networks:/volumes:), it's often simpler to run add compose --force and get a clean compose.yml rebuilt from the manifest instead of patching the old one by hand.
After generation
mise run generate # tsp → OpenAPI → strict server code → build
go run ./cmd/<name>
The skeleton has no persistence yet: build the shared rules in the module's core/ package and the persistence in the module-level repository/, and call them from the handler through the surface's service/ seam.