- Proto is the API. Services, RPCs and messages live in
proto/. Forge generates the Go and TypeScript stubs, the handler wiring, the frontend’s React Query hooks and the API dashboards from them. - SQL migrations are the schema. Tables live in
db/migrations/*.up.sql.forge generateapplies them to a real, throwaway Postgres and reads the schema back, then generates the Go structs and data access from what Postgres actually created. Anything Postgres accepts works:JSONB, arrays, generated columns,CHECKconstraints.
The layout
internal/workers/. Nothing needs registering in forge.yaml, which holds only the project’s identity and the occasional override.
Where logic goes
An RPC’s logic (validation, permission checks, database calls) goes in its method ininternal/handlers/<svc>/. There is no separate service layer behind every handler: the methods on *Service are the handlers, and they reach the database and other collaborators through s.deps. Return errors through svcerr.Wrap(err), which keeps any error text you did not write off the wire, so a database error can never leak a connection string to a caller.
When logic outgrows one method (a state machine several RPCs share, a calculation worth testing on its own, work a background worker repeats), move it into a package with forge scaffold package <name> and give the handler that package’s interface as a dependency. Declare interfaces where they are used, and keep them small.
Schema changes are always a new migration (forge db migration new <name>), never an edit to a generated struct. A query the generated CRUD cannot express goes in internal/db/<entity>_repo_ext.go, which forge creates once and never touches again.
Access control is your code. Every RPC requires a signed-in caller unless its proto says auth_required: false, but what a caller may see and change (which rows belong to whom) is a check you write in the handler. forge project audit reports RPCs that are authenticated but never look at who the caller is.
Generated files and yours
Every file forge writes belongs to one of three groups, and it says which in its own header:
A regenerated file carries
// Code generated by forge. DO NOT EDIT. and a hash of its own contents. If you edit one by hand, the next forge generate stops instead of silently undoing your change. Move the change to the file beside it that is yours, or, if the generated form genuinely cannot express what you need, take the file over with forge project disown <path> --reason "<why>". Deleting a written-once file is remembered too, and forge project rescaffold <path> brings it back.
internal/app/compose.go is the one hybrid: it is yours, but forge generate adds the constructor and dependency entries for new components while leaving every line you wrote alone.
The frontend is a starting point
The scaffolded frontend (a dashboard of entity tiles, a sidebar, a sign-in screen, and list, detail and edit pages per entity) exists to prove the stack is wired end to end. It is yours from the moment it is written, and replacing it with a real design is the expected next step. A deployed Next.js frontend is a static export, so pages use query parameters (/bookmarks/view?id=…) rather than dynamic [id] routes.
Finding things
Related topics
Adding a feature puts this layout to work. Agents can load thearchitecture, api, db and proto skills for the full conventions, which forge lint enforces.