원클릭으로
proto
Proto file reference — annotations, CRUD conventions, field rules, and common mistakes.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Proto file reference — annotations, CRUD conventions, field rules, and common mistakes.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Visual-design discipline for forge frontends — brief the work before building (never invent an aesthetic without user input), declare the design system before coding, lean on the component library, use restrained color/type systems, never hand-draw complex SVGs, and verify visually before declaring done.
Write Next.js frontends — generated hooks, component library, Tailwind v4, visual verification, and Connect RPC clients.
Outbound boundary translators. One adapter per third-party system / queue / storage backend; narrow interface, vendor-neutral callers.
Write Connect RPC handlers — proto-driven codegen, the thin-translation handler pattern (validate, extract auth, convert proto↔internal, call service, wrap errors via `svcerr.Wrap`), middleware, and testing. Business logic lives in `internal/handlers/<svc>/contract.go`, never in handlers.
Forge project conventions and architecture — project structure, generated vs hand-written code, the generate pipeline, proto annotations, contracts, wiring, and naming.
Use-case orchestrators that compose two or more adapters/services. Deps are interfaces only — designed for unit tests with all-mock collaborators.
| name | proto |
| description | Proto file reference — annotations, CRUD conventions, field rules, and common mistakes. |
This skill is the annotation + naming reference. The structural conventions
that drive codegen (one service per file, CRUD method shapes) are
enforced reactively by forge lint --conventions — this skill is the
proactive companion.
Proto is the wire truth: services, RPCs, and messages. It is NOT the
schema language — the database schema lives in db/migrations/ (see the
db skill). The two halves meet at entity detection, below.
proto/services/<service>/v1/<service>.proto # Service definition (RPCs + messages)
proto/forge/v1/forge.proto # Forge annotations (vendored, never edited)
proto/shared/v1/types.proto # Cross-service shared messages (optional)
One service per .proto file. The package path is <project>.services.<service>.v1.
Cross-service references go through proto/shared/v1/types.proto — never
import another service's proto.
Enforced by: forge lint --conventions → forgeconv-one-service-per-file.
forge generate && forge lint && forge build
Rebuilds gen/ (Go stubs, TypeScript clients, mocks, wiring) and verifies
the build. Generated code is overwritten — fix issues in .proto, never in
gen/.
forge generate is the canonical entry — not buf generate alonebuf generate only emits the .pb.go / _pb.ts stubs. The forge
descriptors, schema-driven ORM, mocks, frontend hooks, CRUD wiring, and
bootstrap codegen are produced by forge generate's post-buf passes.
Running buf generate by itself leaves those projections stale.
Always run forge generate — it invokes buf generate internally.
Proto files in Forge projects are not published APIs with external
consumers. Edit freely — add, rename, remove fields, RPCs, and messages.
Mark removed fields as reserved; never reuse field numbers.
import "forge/v1/forge.proto";
service TaskService {
option (forge.v1.service) = { name: "tasks" version: "v1" };
rpc CreateTask(CreateTaskRequest) returns (CreateTaskResponse) {
option (forge.v1.method) = { auth_required: true };
}
}
| Method option | Type | Purpose |
|---|---|---|
auth_required | bool | Require authentication |
idempotent | bool | Mark as idempotent (safe to retry) |
timeout | Duration | Server-side timeout: timeout: { seconds: 30 } |
idempotency_key | bool | Expect Idempotency-Key header |
errors | string list | Declared Connect error codes ("NotFound", "InvalidArgument") |
(forge.v1.entity) and (forge.v1.field) are retired and ignored.
The option definitions remain in forge/v1/forge.proto only as deprecated
tombstones so legacy protos keep compiling; forge generate prints a
one-line notice for any message still carrying them. Entities are
projections of the applied db/migrations/ schema now — there is no
proto-side schema declaration. If your project still has annotated entity
messages (or a proto/db/ directory), see the
migrations/proto-entities-to-schema-truth skill for the flip.
An entity exists when both halves exist:
db/migrations/ has the matching table —
pluralized snake_case of the entity name (storage truth).CRUD RPCs without a table generate honest nothing (Unimplemented stubs, no
pages, no ORM). Tables without CRUD RPCs are plain schema for hand-written
code. forge add entity scaffolds both halves in one step; the messages
and RPCs it writes into the service proto are yours afterwards — the wire
contract evolves independently of the schema.
Use these exact prefixes. For matching methods (with the matching table),
forge generates the per-RPC op constructors and the
<entity>ToProto / <entity>FromProto conversions in
handlers_crud_ops_gen.go (Tier-1, regenerated every run) and scaffolds a
thin delegation into the user-owned handlers_crud.go
(return crud.HandleCreate(s.crudCreateItemOp())(ctx, req)):
| RPC Name | Generated behavior |
|---|---|
Create<Entity> | Insert via ORM |
Get<Entity> | Select by ID |
List<Entities> | Paginated list with filters |
Update<Entity> | Update via ORM |
Delete<Entity> | Delete (or soft-delete when the table has deleted_at) |
The generated conversions map the intersection of wire fields and columns by name: a wire-only field never reaches the DB, a column-only field never leaks onto the wire. Add wire-only fields freely.
Hand-written handler methods always win — the generator skips anything
you've already implemented. To customize a generated CRUD RPC, replace
the delegation body in handlers_crud.go directly (the file is yours;
new CRUD RPCs are appended, existing content is never modified).
When a request/response shape deliberately deviates from these
conventions (a list keyed by ticker+limit instead of AIP-158, say),
forge scaffolds an Unimplemented stub into handlers_crud.go carrying a
forge:custom-read-shape: <reason> comment (including the observed
field list). That is the system working, not an error — the custom
shape is a domain decision and the body is yours to implement, composing
the pkg/crud/pkg/orm helpers (cursor encode/decode, WhereEq/
WhereILikeAny filters, column-allowlisted order-by). forge generate
prints one warning line per stub it scaffolds, and forge audit reports
each under crud_stubs until the body lands (the RPC returns
CodeUnimplemented until then). Markers written by older forge versions
spell it FORGE_CRUD_SHAPE_MISMATCH; audit recognizes both for one
release.
message ListTasksRequest {
int32 page_size = 1;
string page_token = 2;
optional string search = 3; // ILIKE across the table's text columns
optional bool done = 4; // Exact-match filter — must name a real column
string order_by = 5;
bool descending = 6;
}
message ListTasksResponse {
repeated Task tasks = 1;
string next_page_token = 2;
int32 total_count = 3;
}
Filter fields must be optional — otherwise generated code can't
distinguish "not set" from zero values.
search / query / q are the fuzzy-search filters: they span the
table's non-PK text columns via orm.WhereILikeAny. Any other filter
field must name a real column of the entity's table — forge generate
fails loudly otherwise. A user-supplied order_by is validated against
the table's declared-column allowlist (<Entity>Columns); an undeclared
column returns InvalidArgument, not a silent no-op.
enum TaskStatus {
TASK_STATUS_UNSPECIFIED = 0; // Always required — prefix with enum name
TASK_STATUS_PENDING = 1;
TASK_STATUS_ACTIVE = 2;
}
0 and named <ENUM_NAME>_UNSPECIFIED.snake_case (created_at, org_id); proto messages / RPCs / services stay PascalCase. For the full Go ↔ proto ↔ TS ↔ forge.yaml casing table, see Naming conventions in architecture.The protos above are the ones your service owns. For an RPC owned by
another service or repo (you're a client of it, not its implementer), the
remote proto is the single source of truth — import it and generate only a
CLIENT. Pull it in via a pinned task protos copy or a buf BSR dependency;
regenerate the client when the upstream version bumps.
Never hand-copy the remote .proto into your proto/services/ and let forge
scaffold a server for it. That produces a dead handler package: every method
is CodeUnimplemented, Deps holds only Logger/Config with no domain
collaborators, and the wiring boots a service nobody calls. If you find
yourself staring at that shape — an all-Unimplemented handler for RPCs you
never meant to serve — you meant to import the upstream proto and generate a
client, not own the server.
(forge.v1.method) / (forge.v1.service) needs import "forge/v1/forge.proto";.<ENUM>_UNSPECIFIED.TASK_STATUS_ACTIVE, not ACTIVE. Proto enums share a namespace.optional.reserved, never reuse.proto-split.proto/shared/v1/types.proto.db/migrations/; if a CRUD RPC has no matching table, nothing is generated. Use forge add entity (or write the migration) first..proto file. Enforced by forgeconv-one-service-per-file.db/migrations/ declares the schema. Entity = CRUD RPCs + matching table.optional.reserved. Never reuse a number.proto/shared/v1/types.proto.forge generate && forge lint after every proto edit.gen/ — generated code is overwritten.proto-split.service-layer.api.db.migrations/proto-entities-to-schema-truth.