Decide on the protobuf Opaque API, and record the answer either way
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 9
- Forks
- 0
- Avg merge
- 3h 3m
- Merged PRs (30d)
- 509
Description
Raised by the owner, 2026-08-13, reading https://go.dev/blog/protobuf-opaque. This issue exists to produce a written decision, because a question this size gets re-proposed every few months and answering it twice is worse than answering it once.
What it is
A protoc-gen-go code generation mode. Generated structs stop exposing fields directly and expose accessors instead, which lets the implementation choose its own memory layout and forbids a class of mistakes: copying a message struct by value, comparing messages with ==, taking the address of a field, distinguishing unset from zero by reading a pointer.
Why it fits this repo in principle
This is a proto-first codebase by policy. CLAUDE.md's first rule is that types describing the system live in the schema rather than as hand-written Go structs, and behaviour attaches to generated types as methods. The Opaque API is the same idea pushed one level further: the generated shape becomes an implementation detail rather than a public struct layout. Nothing about our design argues against it.
What it would actually cost here
- Every construction site of every generated type, across both drivers, the server, the CLI, the LSP, flowtest and the examples harness.
- The plugin SDK and the five plugin modules compile against these types as separate modules, so their diffs land at the same time and the boundary has to stay coherent through it.
bufgeneration configuration changes, so the descriptorset pin and the generated-code drift checks move with it.- One very large mechanical diff, which is precisely the shape of change where a real defect hides in the noise, and the shape our review process is weakest against. The
flow fixcorruption history is the local evidence: two of those landed inside changes that looked mechanical.
What we would get today
Mostly latent value. The mistakes it forbids are ones we do not currently appear to make: nothing compares messages with == (comparisons go through proto.Equal or cmp.Diff with protocmp.Transform), and the schema is already the source of truth. So the immediate defect reduction is close to zero, and the benefit is optionality for the protobuf implementation plus protection against future mistakes.
The decision this issue wants
One of:
- Adopt now. Justified if we expect the memory or layout benefits to matter, or want the mistake class closed before the codebase gets larger.
- Adopt at a natural break, for example alongside #407's proto split, where a large generated diff is already expected and reviewers are already reading generated output carefully.
- Decline for now, with the reason recorded, so the next person reading the blog post finds this issue instead of re-deriving it.
My read, for whoever decides: option 2 or 3. The change is philosophically right and practically expensive, and its value does not decay while we wait, whereas the risk of a giant mechanical diff is highest right now, while several language surfaces (triggers, async, interpolation) are still settling. #407 is the natural moment if we want it.
Related, already answered
protocmp: already used correctly, four files, alwaysprotocmp.Transform()insidecmp.Diff. No gap, nothing to do.dynamicpb: unused and not wanted. Its purpose is building messages from descriptors when the generated type is absent; our plugin boundary deliberately resolves descriptors to validate against known types instead.protorange/protopath: evaluated in #515, which adopted the descriptor-driven idea as a completeness guard rather than as a value-walking traversal.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with CLAUDE.md, the buf generation configuration, the descriptorset pin, and the generated-code drift checks mentioned in the issue. Review the scope across the drivers, server, CLI, LSP, flowtest, examples harness, and plugin modules, then compare the proposed timing with #407. Done means recording a decision to adopt now, adopt at that break, or decline with its reason.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- documentation, tooling
- Issue type
- Documentation
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100