A new service
proto/services/billing/v1/billing.proto, the handler directory internal/handlers/billing/, a subcommand on your binary, a test harness entry, and wires the service into cmd/<binary>/main.go. Nothing needs adding to the deploy files: in every environment, the binary’s server command already serves every service on one port. Name services after a domain thing (billing, order), and scaffold several at once with forge scaffold service billing invoices.
A new entity
Mark a message in the service’s proto with// forge:entity, and run bare forge scaffold:
optional), enums, validation (buf.validate rules) and markers such as forge:read-only (clients can read but never write the field) or forge:secret (stored but never returned). forge project annotations prints the full vocabulary.
Once an entity exists, its migration is yours: forge never edits it, and later schema changes are new migrations.
A custom RPC
Anything that is not plain create/read/update/delete is a custom RPC. Declare it in the service block and its messages, then scaffold it:internal/handlers/billing/rpc_issue_invoice.go, returning Unimplemented until you fill it in, along with a test row that fails as soon as you implement it, prompting you to write a real assertion. Forge never invents an RPC from a message pair: if the service block does not name it, it does not exist.
An RPC named exactly Create<Entity>, Get<Entity>, Update<Entity>, Delete<Entity> or List<Entities> is wired as CRUD. If one of those needs custom behaviour (creating a task also creates its dependencies, say), put // forge:custom above it in the proto and it is scaffolded as a custom RPC instead.
Every RPC requires a signed-in caller unless it is annotated auth_required: false. What the caller may do is checked in your handler; read who they are with middleware.GetUser(ctx).
A schema change
db/migrations/<timestamp>_add_invoice_due_date.up.sql with a fresh UTC version, so parallel branches never collide. Write plain Postgres DDL. There is no down migration: forge rolls forward, and a bad migration is fixed by another one. Because a deploy runs the migration while the previous release is still serving, mark whether that release survives it (--backward-compatible "<why>" or --breaks-rollback "<why>"); forge lint --migration-safety flags hazards such as adding a NOT NULL column to a populated table.
Then run forge generate to update the generated structs, and add the field to the proto message if clients should see it. forge env up applies pending migrations locally; deployed environments apply them before any new pod serves.
Generate, lint, test
Run this after any proto or migration change, and before you call anything done:forge generatere-derives everything from your protos and applied migrations. It never touches your handlers, migrations or other owned files, and it does almost nothing when nothing changed, so run it whenever in doubt.forge lintchecks Go, proto and frontend code, migration safety, architecture conventions and edits to generated files. It fixes formatting in place first.forge lint --quietprints only failures, with the verdict on the last line.task testruns the Go tests with the race detector, against a real Postgres, plus every frontend’s tests. While iterating,task test:short -- ./internal/handlers/billing/...runs one package’s fast tests, cached.task ciruns the whole pull-request gate locally: generated-code check, lint, build, vulnerability scan, environment rendering, unit and integration tests.
contract.go, and handler tests use a table-driven harness, so tests rarely need hand-written fakes.
Development data
forge env up seeds an empty dev database with rows derived from your schema, consistent across foreign keys and within every constraint. Text it cannot infer reads as sample_<column>_<n> until you teach it your domain’s words in db/seeds/vocab.yaml. forge db seed status shows what was seeded and forge db reset drops, recreates, migrates and reseeds the dev database.
Background work
server process as your services, locally and when hosted. forge scaffold also creates adapters (outbound clients for a third-party API), webhooks, frontends, packages, binaries and Kubernetes operators; forge scaffold --help lists them.
Configuration
Settings are declared once, inproto/config/v1/config.proto, with their environment variable and default. forge generate turns them into a typed config your code receives, and per-environment values go in deploy/kcl/<env>/. A field marked sensitive: true becomes a secret reference with no place in git to paste a value; set it with forge secret set.
Related topics
Environments & deploys for shipping the change, and Troubleshooting when generate or lint stops you. Agents can load theapi, db, proto, testing and services skills for the full conventions.