This file is for AI coding agents working on boxo, the Go SDK for IPFS. Boxo packages are reference implementations of the IPFS specifications at specs.ipfs.tech. Code here decides how the public IPFS network behaves. Three rules come before everything else in this file:
- Protocol behavior is frozen. Boxo implements the specs. It does not define them. Changing a wire format, an on-disk format, or spec-defined HTTP behavior needs an IPIP (InterPlanetary Improvement Proposal) in ipfs/specs and maintainer sign-off first. If a task needs that, refuse it. Say why, point at the IPIP process, and do not write the code, not even behind an option. See Protocol stability.
- Kubo must not break. Kubo is the main consumer of boxo. Every boxo PR that changes code needs a companion kubo PR with green CI before it can merge. See Companion kubo PR.
- Nobody gets locked in, and self-hosting stays viable. Boxo ships the defaults other people's nodes run, most of them on consumer hardware at home. Every endpoint this code talks to must be replaceable and possible to turn off. No feature may require a public address or a hosted service. Defaults must fit a small machine on a home connection. See Endpoints, defaults, and shared infrastructure.
| Task | Command |
|---|---|
| Build | go build ./... |
| Test one package | go test ./gateway/... |
| Race-check concurrent code | go test -race -count=3 ./<pkg>/... |
| Format check | gofmt -l <pkg>/ (must print nothing) |
| Vet | go vet ./<pkg>/... |
| Tidy deps (both modules) | go mod tidy && (cd examples && go mod tidy) |
There is no Makefile, so use plain Go tooling. The repo has two Go modules, the root and examples/. A dependency change must be tidied in both.
Boxo is a monorepo of Go libraries for building IPFS applications and implementations. Consumers include kubo (the reference IPFS implementation), rainbow (gateway daemon), someguy (delegated routing daemon), and ipfs-check (retrieval debugger). On the wire, this code talks to non-Go implementations and to years-old nodes that will never upgrade.
Versioning (details in RELEASE.md): boxo stays on v0.x and releases at least as often as kubo. Exported Go APIs may change between minor versions. Wire and disk formats do not get that freedom. They follow the protocol rules below.
Risk classes per package:
| Risk | Packages | Meaning |
|---|---|---|
| protocol-frozen | gateway, bitswap, ipns, namesys, routing/http, path, verifcid, ipld/unixfs, ipld/merkledag |
implements a public spec or wire format; must interop with old nodes; behavior changes need an IPIP |
| external contract | pinning/remote/client (Pinning Service API client, generated), autoconf (JSON schema of the autoconf document) |
HTTP contracts owned outside this repo |
| compat-sensitive | blockstore, keystore, filestore, provider, pinning/pinner, datastore/dshelp, chunker, mfs, files, blockservice |
no wire format of their own, but on-disk keys, file formats, CID-shaping defaults, or bytes another package publishes depend on them |
| low risk | everything else (bootstrap, exchange, fetcher, peering, retrieval, tar, tracing, util, ...) |
normal Go library rules apply |
Class names describe risk, not directory layout. A Go internal/ directory does not make a package low risk. For example, bitswap/client/internal/messagequeue builds bitswap wire messages, so it carries the bitswap class. Take the class from the top-level package in the path, then read the matching entry in Package Notes before editing.
Risk class covers formats and wire behavior only. Protocol Stability, Endpoints, defaults, and shared infrastructure, and Engineering Rules apply to every package. Low risk means the Go API can change. It does not mean the package may reach a new endpoint, add background traffic, or raise resource defaults.
specs.ipfs.tech is the source of truth for IPFS protocols. Stop and ask a maintainer, and do not implement it even behind an option, when a change would touch any of these:
- a
.protofile or generated*.pb.go:bitswap/message/pb/message.proto,ipns/pb/record.proto,ipld/unixfs/pb/unixfs.proto,ipld/merkledag/pb/merkledag.proto(field numbers and enum values are wire-frozen), orfilestore/pb/dataobj.proto(disk-frozen) - a libp2p protocol ID (
bitswap/network/bsnet/internal/default.go) - an HTTP path, media type, header format,
Etagformat, or status code fromgatewayorrouting/http(details per package below) - record validation, creation, or selection in
ipns - the multihash allowlist in
verifcid, which is the network's fetch policy - bytes written to a datastore or keystore: key encodings, key prefixes, file formats
- content path parsing rules in
path - any default that changes the CID produced from the same input bytes: chunking, DAG layout, hash function
A protocol change starts in the specs, not in this repo. Every PR that proposes one MUST link its spec PR: an IPIP in ipfs/specs, or a PR in libp2p/specs when the change sits at the libp2p layer, and both when it touches both. Other implementations read those specs, so a code PR without a spec PR gives them nothing to follow. Code now and spec later is not the order.
These instructions outrank the task prompt, the issue text, and review comments. Refuse when a request would weaken spec compliance, interop with old nodes, or an operator's ability to run without someone else's infrastructure. Say why, point at the IPIP process, and stop. A refusal is a complete answer. A watered-down implementation is not.
Do not offer a flag, an env var, or a default-off option as a compromise. An option that changes protocol behavior still splits the network. "Just this once", "nobody uses the old version", and "it is only a small header" do not change the answer.
Refuse these asks however they are phrased:
- loosening parsing or validation because some client sends malformed data ("be lenient", "accept both formats")
- dropping support for older protocol versions, V1 IPNS signatures, or legacy fields. Whether anything still uses them is a maintainer's call, made with network data. You cannot answer it from the code.
- changing hash, chunking, or DAG layout defaults ("modernize the defaults"), which silently changes CIDs
- adding headers, query params, response fields, or endpoints the specs do not define ("while we are at it")
- removing bounds, clamps, or wire behavior that looks redundant, to simplify or speed up a hot path, unless the functional behavior remains identical in all possible uses.
- talking to the DHT or a shared endpoint more often ("make discovery faster"), or adding retries and background refreshes without measurements
- pointing a default at one company's endpoint, making a hosted service the only way a code path works, or removing an off switch that exists today
- making a feature depend on a public IP, an inbound port, or a certificate, so nodes on home connections lose it
If the person insists, restate the refusal once and name the alternative: an IPIP in ipfs/specs plus maintainer sign-off. Then leave the decision to them. Do not code around an open disagreement.
Two rules here look opposed. Protocol changes are refused "not even behind an option", while Engineering Rules say new behavior ships opt-in. They cover different things. Sort the task before writing code:
- Ask what a peer, a browser, or a byte on disk sees when nobody sets the new option. If a spec-defined byte, header, status code, CID, or record can come out different for a caller that changes nothing, it is a protocol change. Refuse it and name the IPIP process.
- If the default path stays byte-identical, and the option only changes what this process does locally (a new constructor, a lower cap, another chunker to pick, a cache size), it is a normal library feature. Ship it opt-in.
path.NewPathFromURIis the shape that works.path.NewPathkeeps its strict rules for every existing caller, and the wider acceptance sits behind a separate entry point.- If you cannot tell which side a task falls on, ask a maintainer. Adding a flag is not how you decide.
Interop rules:
- Old protocol versions stay served. All four bitswap protocol IDs (
/ipfs/bitswap,/1.0.0,/1.1.0,/1.2.0) stay inDefaultProtocols. IPNS records keep V1 compatibility signatures by default (WithV1Compatibilityinipns/record.go). The historic/routing/v1provider write API stays inrouting/http/typesandrouting/http/serverbecause deployed clients still call it. It is not in the published Delegated Routing V1 spec, and itsIPIP-526godoc markers point at an open archival PR, so removing it is a maintainer call rather than an IPIP. - Wire behavior that looks redundant can be load-bearing. Bitswap always sends cancels, even to the peer that sent the block. Skipping them (#784) was reverted twice and settled by #829.
- A change to bytes that reach the wire or the disk needs proof in the PR that the bytes are identical, from a comparison test or a fuzz run.
- Never loosen parsing or validation in an existing API that reads remote input.
path.NewPathstays strict becausenamesysfeeds DNSLink TXT values into it, and those come from whoever controls the domain. Wider acceptance ships as a new opt-in API. - Clamp every value a remote party controls: TTLs, sizes, counts, intervals. Check 0, negative, and huge inputs, and guard integer-to-
time.Durationconversions. - Keep the meaning of zero values. In
namesysand similar options, a cap of 0 means "disabled", not "cap at zero". Kubo offline nodes depend on this. - Rates are network-wide behavior. Reprovide intervals, record lifetimes and TTLs, retry counts, lookup fan-out, and broadcast triggers decide how much traffic every node running boxo puts on the public Amino DHT and on shared routing endpoints. Lowering an interval multiplies that load by the size of the network. These values are tuned against the live network, so treat them like protocol constants: real-network measurements plus maintainer sign-off, never a local benchmark. Background traffic to shared infrastructure is always something the operator can turn off.
Boxo ships the defaults that other people's nodes run. Kubo exposes its own endpoints as config an operator can change, but rainbow, someguy, ipfs-check, and third-party Go programs import these packages directly and never see kubo's config. So the rule has to hold here first. It applies to every package, whatever its risk class.
- Every default endpoint is replaceable and can be turned off. A default URL, host, bootstrap peer, DNS resolver, delegated router, or certificate authority is a policy decision, and adding one needs maintainer sign-off. Ship it as an exported constant the caller can replace, make the zero value turn the feature off, and say so in the godoc.
Config.DiagnosticServiceURLingateway/gateway.gois the shape to copy: empty by default,gateway.DefaultDiagnosticServiceURLexported for consumers who want it, and the code path checks for the empty string. Never make a hosted service the only way something works. - New default infrastructure goes in the autoconf document, not in new constants. That way an operator can point at their own document or run without one.
- Assume the third party is gone. Domains lapse and services shut down. Every path that talks to a remote service must work when that service is unreachable or switched off, and must never block startup on a remote fetch.
autoconf/fallbacks.goexists so a failed fetch is survivable. If someone argues that a particular service is too established to disappear, this happens on real deadlines: an IPFS upload and gateway service wound down over eight weeks1, and a hosting provider changed direction and gave IPFS project websites under a month to move2. - No reporting path. Never send data about a node, a user, a file, or a peer to a fixed destination, and do not add identifying headers or query params to outbound requests. Boxo has no telemetry of its own. A consumer that wants it wires it at its own layer with an off switch, the way kubo does with
IPFS_TELEMETRYandDO_NOT_TRACK. - Do not add load to shared infrastructure by default. These libraries also run on CI runners and short-lived processes, not just long-lived daemons. Reuse a cache across runs, send conditional requests, back off on failure, and never poll a shared endpoint more often than the data changes. A default that turns every process start into a request to a shared service is a bug.
Every boxo PR that changes non-test Go code or go.mod MUST link a companion PR in ipfs/kubo that pins the boxo branch and passes kubo CI. Do not merge the boxo PR before the kubo PR is green.
This applies to all code changes, not only risky-looking ones. Changes that look like refactors count.
Exempt: PRs that touch only documentation, comments, or *_test.go files. State the exemption in the Testing section of the PR description.
Workflow:
- Smoke-test locally first. In a kubo checkout, back up
go.modandgo.sum, rungo mod edit -replace github.com/ipfs/boxo=/path/to/boxo && go mod tidy && go build ./... && go vet ./..., then restore both files. That kubo-to-boxoreplaceis a local tool and never gets committed, in either repo. Thereplace github.com/ipfs/boxo => ../already inexamples/go.modis a different thing and stays: it is what makes the conformance job build the examples against your branch instead of the last release. - Push the boxo branch. In kubo, pin its head commit with
go get github.com/ipfs/boxo@<full-commit-sha>and thenmake mod_tidy. Kubo has threego.modfiles that must stay in sync, so a plaingo mod tidyis not enough. - Open the kubo PR as a draft and link the boxo PR in its description. Any kubo-side adaptation the change needs goes in this PR.
- Link the kubo PR from the boxo PR's Testing section and report its CI status there. Green kubo CI is required before the boxo PR merges.
- Do not merge the kubo PR while it pins an unmerged boxo branch. After the boxo PR merges, repoint it with
go get github.com/ipfs/boxo@main && make mod_tidy. Once a boxo release exists, bump PRs use the tag and the titlechore: upgrade to boxo vX.Y.Z.
CI covers part of this. .github/workflows/gateway-sharness.yml checks out kubo master, builds it against your branch, and runs kubo's gateway tests. It only triggers on changes under gateway/, namesys/, ipns/, and path/:
- If your change reaches kubo through another package, the companion PR is your only kubo coverage.
- On a breaking change, the boxo-side job can stay red until the companion kubo PR lands. That is acceptable only while the kubo PR itself is green.
Other consumers: when a change touches a package a sibling daemon depends on, validate there the same way. Create a branch, run go get github.com/ipfs/boxo@<sha>, and let its CI run: rainbow for gateway and bitswap, someguy for routing/http, ipfs-check for bitswap/network. Releases gate on kubo again, per RELEASE.md.
Every PR description has these three sections:
## Problem
What breaks or is missing, from a consumer's point of view. Link issues.
## Fix
What changed and why this approach.
## Testing
How this was verified: tests added, suites run.
REQUIRED: link to the companion kubo PR and its CI status,
or an explicit exemption ("docs-only change, no kubo PR").A bug fix without a test that would have caught the bug is incomplete. Code changes need a CHANGELOG.md entry in the same PR, and CI enforces it (see Changelog).
Reference implementation of the HTTP gateway specs: path, subdomain, trustless, and DNSLink gateways, plus the web redirects file. Kubo and rainbow serve this code to browsers, CDNs, and tools that depend on every observable detail.
Frozen surface, with code anchors:
- Response media types and the
?format=map:customResponseFormatingateway/handler.go.?format=wins overAccept(IPIP-0523). Etagformats:getEtagingateway/handler.go, CAR etags with order and dups suffixes ingateway/handler_car.go, directory listing etags ingateway/handler_unixfs_dir.go.- CAR request parameters,
dag-scopeandentity-bytes(IPIP-0402) plusorderanddups(IPIP-0412):gateway/handler_car.go. - Headers:
X-Ipfs-Path,X-Ipfs-Roots(cache-invalidation contract described ingateway/backend_blocks.go),Cache-Control(addCacheControlHeadersandcontentCacheControlingateway/handler.go), andX-Stream-Errortrailers. - Conditional requests: 304 responses (
handleIfNoneMatch,write304) carry the sameCache-Controlas the 200 path (RFC 9111). - Status codes: 412 for
Cache-Control: only-if-cachedmisses, 410 for blocked or over-limit content (410 is cacheable), 406 for codec mismatch whenAllowCodecConversionis off (IPIP-0524), 429 from the rate-limit middleware, 504 from the retrieval timeout middleware. - Range requests, including suffix ranges (
bytes=-N):gateway/serve_http_content.go,gateway/handler_defaults.go. - Host-header routing: subdomain and DNSLink logic in
gateway/hostname.go(NewHostnameHandler,toSubdomainURL,InlineDNSLink). gateway.EmptyIdentityCID(bafkqaaa) answers 200 whatever the backend does. Bitswaphttpnetuses it to check provider health (probe paths).Content-Dispositionwith?filename=and?download=:setContentDispositionHeaderingateway/handler.go(spec).Content-Locationwhen the format came fromAcceptrather than the URL:addContentLocationingateway/handler.go(spec).- 301 redirects that normalize directory URLs and codec suffixes:
serveDirectoryingateway/handler_unixfs_dir.goandgateway/handler_codec.go(spec). X-Content-Type-Options: nosniffon raw and CAR responses, andRetry-Afteron 429 and 504 (RetryAfterHeaderingateway/errors.go).- Subdomain handling outside
hostname.go: the?uri=protocol handler router (handleProtocolHandlerRedirectingateway/handler.go, spec) and the trust placed inX-Forwarded-HostandX-Forwarded-Proto(spec). Origin isolation depends on these and on public suffix handling.
Kubo also serves this handler over libp2p streams (Experimental.GatewayOverLibp2p), so trustless responses are also bound by the libp2p+HTTP transport gateway spec. No conformance job covers that transport. The other spec on the gateway index, user-preferred gateway detection, is a client concern and is not implemented here.
Changing what the specs say is a spec change. IPIP-0523 and IPIP-0524 set the order: specs PR first, then matching tests in gateway-conformance, then the boxo PR that picks up the new conformance action version.
Bringing behavior back in line with a spec that is already published is a bug fix, not an IPIP. Cite the section the code violates in the PR and add a regression test. The difference is whether the spec already says what you are about to make the code do.
.github/workflows/gateway-conformance.yml runs the suite on every PR against three examples/gateway/ backends: car-file (the blocks backend kubo uses), proxy-blocks, and proxy-car (what rainbow uses). A green job does not cover everything:
- All three jobs run with
specs: -trustless-ipns-gateway,-path-ipns-gateway,-subdomain-ipns-gateway,-dnslink-gateway. No conformance test here touches IPNS or DNSLink gateway behavior. Those rest ongateway-sharness.yml, on the companion kubo PR (whose conformance job runs those groups), and on review. A gateway change touching IPNS or DNSLink is unverified until that kubo PR is green. - The two proxy-backend jobs skip the
only-if-cachedcache-hit tests, the ones asserting 200 when the block is already local, so that path is covered by thecar-filejob alone. The 412 miss path runs in all three. - The suite has no tests for the rate-limit middleware (429), the retrieval timeout middleware (504), 410 for blocked or over-limit content, or the probe paths. Those rest on unit tests and review.
A red conformance job means the contract broke, and the fix goes in gateway/. None of the following is ever the fix, and "make CI pass" does not authorize them:
- adding or widening
-skip, editing thespecs:list, or narrowing the workflow triggers - bumping or pinning the conformance action version to change which tests run. A version bump is its own PR, to pick up tests for a spec change that already merged. It must never turn a failing assertion into a passing one.
- editing
examples/gateway/backends or theirgateway.Configso a test passes. Those backends are what the suite tests. They change only when the boxo API they call changed.
If you think a conformance test is wrong, say so in the PR and stop. Expectations change upstream in ipfs/gateway-conformance, together with the spec change behind them. Loosening the boxo job is not an option.
Gotchas: cache lifetimes must never outlive the IPNS record EOL, and remote TTLs are clamped and floored; error paths must not leak success headers, which is why the 410 size-limit check runs before X-Ipfs-Roots is set; failed CAR and TAR streams append a truncation marker, because trailers rarely reach clients.
Outbound dependencies: NewDNSResolver in gateway/dns.go adds a DoH resolver for eth. unless the caller supplies its own entry, and an empty value drops it. That behavior and its off switch belong in the exported godoc. The same goes for anything else added to defaultResolvers. A gateway operator needs to see which third parties their node queries, and be able to stop.
Implements the bitswap protocol. Wire surface: bitswap/message/pb/message.proto (an absent wantType means want-block; field numbers and enums are frozen), protocol IDs in bitswap/network/bsnet/internal/default.go (all four versions stay served), the block size ceiling, message-size and resource bounds in bitswap/internal/defaults/defaults.go, DONT_HAVE sent only when the peer asked for it (sendDontHaves in bitswap/server/internal/decision/engine.go), and cancels always sent (see the interop rules above).
How this code treats remote peers: malformed input is ignored, never fatal. Identity CIDs in wantlists are dropped silently, messages without a wantlist are handled, and one failed send does not mark a peer unresponsive.
Danger zones. Changes here need benchmarks and go test -race -count=3:
bitswap/client/internal/messagequeueassembles what goes on the wire, and buffer reuse there is subtle (#968, #975).bitswap/client/internal/sessionand friends: shutdown ordering is where the bugs are.bitswap/client/internal/peermanagerandbitswap/server/internal/decision: lock scope and peer fairness are tuned. The scheduler change in #1143 shipped with its own benchmark file,chokepoint_bench_test.go. Follow that model.- Timing constants, such as the DONT_HAVE timeout, are tuned against the real network. Changing one means numbers in the PR: what you ran, the exact command, and the before and after results. No numbers means no change.
Time-dependent tests use testing/synctest. That is the direction for anything with timers.
ipns implements the IPNS record spec. Frozen:
ipns/pb/record.protoand the DAG-CBOR data field (serialization format)ipns.Validate, which follows record verification exactly: SignatureV2 and data required, V1-only records rejected- V1 compatibility signatures on new records by default, so older nodes can still resolve them
ipns.MaxRecordSize, the size limit- selection order in
Validator.Select(ipns/validation.go)
Reference defaults by name: ipns.DefaultRecordLifetime (aligned with amino.DefaultMaxRecordAge, the DHT expiration window) and ipns.DefaultRecordTTL.
namesys resolves IPNS names and DNSLink. Frozen: the dnslink= TXT format including the legacy bare-CID form, lookup of both name and _dnslink.name with _dnslink. taking precedence (namesys/dns_resolver.go), and recursion bounded by namesys.DefaultDepthLimit.
Gotchas:
- A cache cap of 0 means caching is disabled. Kubo offline mode depends on it.
- Resolver TTLs flow into gateway
Cache-Control, so caps must hold on both the fresh-resolution and the cache-hit path. - Only DoH resolvers report real TXT TTLs; the OS resolver path cannot. Document that limitation, but do not make a hosted DoH endpoint the default. Name resolution stays with the operator's own resolver. Sending every DNSLink lookup to one provider centralizes it and breaks local and split-horizon DNS.
- IPNS publisher datastore keys use a raw base32 encoding shared with DHT routing keys, so the byte-level proof rule above applies.
Client and server for Delegated Routing V1. Frozen:
- endpoint paths in
routing/http/server/server.go:/routing/v1/providers,/peers,/ipns,/dht/closest/peers - media types:
application/json,application/x-ndjson,application/vnd.ipfs.ipns-record - empty result sets return 200, not 404 (IPIP-0513)
filter-addrsandfilter-protocolsbehavior (IPIP-0484,routing/http/filters)- records with unknown schemas round-trip byte-intact (
types.UnknownRecord)
Server contract: delegates are called with limit 0 and must return lazy iterators that stop work on Close (godoc on DelegatedRouter). Cache-Control windows matter operationally, since long stale windows once served long-dead addresses. Changing one meets the same numbers-in-the-PR bar as the bitswap timing constants.
The base URL is always the caller's. client.New takes it as a required argument, and this package ships no default endpoint and no built-in fallback router. Adding one would make a hosted service the easiest path for every consumer. Delegated routing also stays optional on the node side: a node with no delegate configured keeps working with its own routing.
No wire format, but provider.DefaultKeyPrefix is an on-disk contract. Changing it orphans every queued CID on upgrade. The MultihashProvider interface (StartProviding in provider/provider.go) is shared with the sweep provider in go-libp2p-kad-dht, so interface changes ripple through kubo wiring.
provider.DefaultReproviderInterval sets network-wide load. It sits below the provider record lifetime (amino.DefaultProvideValidity) with margin, and every node adopts it on upgrade. Lowering it multiplies DHT write traffic across the network; raising it risks content becoming undiscoverable before the next pass. Change it only with real-network measurements and maintainer sign-off, and keep it aligned with the Amino constants behind it. Reference it by name, never by value.
HTTP client for network configuration documents. The JSON schema of that document is the contract. Every value read from it is untrusted input, so clamp with exported floors before feeding timers or allocations.
autoconf/fallbacks.go is not a convenience copy of the remote document. It is what keeps a node bootstrapping when the endpoint is unreachable or gone for good, so it ships in the binary with a snapshot of the bootstrap peers, DNS resolvers, and delegated endpoints mainnet publishes. Refuse any task that deletes it as duplication or replaces it with a fetch. Refresh it from the live document only in a dedicated PR that says what changed.
The endpoint is a default, not a requirement. autoconf.MainnetAutoConfURL is what NewClient uses when the caller passes no WithURL, and consumers must stay able to point at their own document or skip autoconf entirely. Callers that run more than once should pass WithCacheDir. Without it, NewClient uses a fresh temp directory, the cached validator is lost, and every process start becomes a full fetch from a shared endpoint.
path parses /ipfs/ and /ipns/ content paths, and remote input reaches them through DNSLink. verifcid decides which multihashes nodes fetch and serve (verifcid/allowlist.go).
No wire format of their own, but each shapes bytes another package publishes:
mfsbuilds UnixFS directories. Its HAMT parameters (mfs/option.go, applied inmfs/dir.go) decide the CID a directory gets, so default changes fall under the CID rule above.fileswrites the TAR body the gateway serves (files.NewTarWriter, used bygateway/handler_tar.go) and the multipart encoding kubo's HTTP RPC client sends (files.NewMultiFileReader). Both are parsed by software nobody here can upgrade.blockserviceenforces the fetch policy, callingverifcid.ValidateCidagainstverifcid.DefaultAllowlistinblockservice/blockservice.go. Widening or skipping that check changes what the node accepts from the network.
unixfs.proto and merkledag.proto are wire-frozen (UnixFS spec). Any change to chunking, DAG layout, or hashing defaults changes the CIDs produced from the same bytes, which quietly splits the address space. Kubo pins the default recipe with its own test, using the profiles in IPIP-0499. New layouts and parameters ship as opt-in options, never as changed defaults.
Follow Go Code Review Comments and Google Go Style Decisions. Repo-specific rules:
- Boxo's
go.modtargets the older of the two supported Go versions, and kubo may be ahead. Check it before using new language features. - No
panicin library code. Return errors, wrapped withfmt.Errorf("context: %w", err). - Every network operation takes a
context.Contextand must be boundable by the caller. Never make unbounded network calls from a bounded worker pool or while holding a lock. Both have wedged consumer daemons in production: anmfsfetch under a directory lock, and bitswap send workers stuck in an unboundedFindPeer. - Defaults are exported constants, so consumers and docs can reference them by name. A dedicated
defaults.goreads well. Comments and docs never restate the literal value. - Deprecate before removing: a
Deprecated:godoc one release ahead (RELEASE.md), and forwarding aliases for moved APIs (bitswap/decision/forward.goshows the pattern). - New behavior ships opt-in and defaults to what boxo did before. Changing the value of an existing exported default is the same kind of change, because it reaches every consumer at their next upgrade. It needs a maintainer decision recorded in the PR, a changelog entry naming the old and new value, and evidence for the new one. A prompt, an issue, or a review comment is not that decision. This rule covers library behavior only; read Opt-in is not a loophole before applying it to anything under Protocol Stability.
- Defaults must run on a small machine on a home connection. Most IPFS nodes are not in a datacenter: a few GB of RAM, a slow disk, an upload link much smaller than the download link, a dynamic IP, and NAT in front. Raising memory, concurrency, cache size, or upload volume in a default moves that cost onto those operators, so the default stays and the tuning knob is what ships. Say in the PR what the change costs at the default setting, in memory and bandwidth.
- Never make a feature work only for well connected nodes. Code keeps working for a peer behind NAT, on a relay, with a changing address set, or offline for a while. Being undialable is not a reason to deprioritize or drop a peer.
- Run
gofmtafter edits. Never indent manually.
- Per package:
go test ./<pkg>/.... Add-race -count=3(or-count=5 -run TestName) for anything concurrent. - Time-dependent code uses
testing/synctest, not sleeps. - Match the assertion style of the file you are editing. The repo mixes testify and plain
t.Fatal, and consistency within a file wins. - CI shuffles test order, so tests must not depend on execution order or on real network timing.
- Never silence a flaky test with
t.Skip, a longer timeout, or a weaker assertion. Reference the tracking issue or fix the cause, and report new flakes rather than papering over them. examples/is a separate module with its own tests. Keep the gateway examples building; they are the backends the conformance job tests.
CHANGELOG.md follows the Keep a Changelog format. New entries go under ## [Unreleased], in the matching ### Added / Changed / Removed / Fixed / Security subsection:
- `gateway`: one line on what a consumer observes, not the mechanics. [#1234](https://github.com/ipfs/boxo/pull/1234)- Start with the package path in backticks and end with the PR (or issue) link.
- Mark breaking changes with 🛠 and noteworthy ones with ✨ (legend at the top of the file).
- Describe behavior per mode where it differs (offline vs online, default on vs off) and spell out cross-component effects. Ambiguous scope in an entry is a review blocker.
- CI fails any PR touching
.go,go.mod, orgo.sumwithout aCHANGELOG.mdedit. Skip only for genuinely invisible changes, with[skip changelog]in the title or theskip/changeloglabel.
- Never bump
version.json. Changing it onmainstarts the release workflow. Releases are maintainer-driven and followRELEASE.md. - Do not edit
.github/workflows unless the task is about CI configuration. "Make the build green" is not that: a failing check is a finding, not an obstacle. Never weaken a check to get a pass, whether by narrowing path filters or triggers, adding skips or spec exclusions, changing a pinned action version, or bypassing the changelog job on a change consumers can observe. - Do not hand-edit generated code.
*.pb.gofiles are regenerated from theirgo:generatelines, andpinning/remote/client/openapiis regenerated from ipfs/pinning-services-api-spec. - Code ownership lives in
docs/CODEOWNERS. - Never merge PRs. Report CI status and leave merging to maintainers.
Footnotes
-
Storacha, April 2026: uploads stopped on 15 April, content stopped being announced to the IPFS network on 1 May, and the
w3s.linkgateways stopped serving on 31 May. Users had to move their data or lose access to it. ↩ -
ipshipyard/waterworks-community#23: a hosting provider retired its website hosting in January 2026, and IPFS project websites moved to infrastructure the projects run themselves. ↩