docs: agent-first "build your own middleware" authoring guide - #19
Conversation
commit: |
mandarini
left a comment
There was a problem hiding this comment.
This is really good!! Thank you for pulling it together.
Two things I think we should adjust before merge, two of which your own field test surfaced:
1. The Workers/getEnv timing caveat is missing, and the guide's advice currently sets authors up to hit it. §1 says to initialize clients in the outer (config) => stage, and rule 2 mandates getEnv, but on Cloudflare Workers getEnv returns undefined until the first request (bindings arrive per-request; src/core/runtime.ts documents this). An author who follows both instructions literally ships a middleware that breaks on Workers. Your withResend hit exactly this and worked around it with lazy first-request init (client ??= …), and its comment even says the guide's advice "does not hold on Cloudflare Workers." Since the premise here is that an agent follows the guide literally, I think this caveat plus the lazy-init pattern belongs in the guide itself. It would also give rule 2 a worked example, which it currently doesn't have anywhere in the doc. This is the one I think is most important.
2. The satisfies FetchHandler paragraph in §5 attributes pipeline's built-in behavior to the anchor. "It turns on ambient accumulation … and it turns on collision detection" is true for the hand-nested form (and §3's test anchor uses it correctly there), but not for the pipeline example it's attached to, pipeline does accumulation and collision/prereq checking itself, and since it already returns FetchHandler, the satisfies there is inert. I verified against source: with no anchor anywhere, the handler's ctx keys still type, a duplicate key still fails with the exact middleware-conflict: … string, and reversed prerequisites fail with the exact middleware-prereq: … string. §0's own snippet quietly agrees (it reads ctx.validatedBody with no anchor). It seems worth fixing because it propagates: the withResend tests and README both put the anchor on pipeline results, presumably carrying the belief along. Maybe something like: "with pipeline, accumulation and collision detection are built in; satisfies FetchHandler documents the export shape, and it's what enables both when you hand-nest instead of using pipeline."
Two optional nits, take or leave:
- The intro's "Every code block below is a complete file … Nothing is elided" is slightly overclaimed, I think. §0's destination snippet contains a literal
{ ... }, and thepackage.jsonblock can't carry a path label. "Every code block labeled with a path is a complete file" would be exact. - Rule 8's boundary could be one sentence sharper.
withResendthrows on a missing API key (reasonably, I think), but a strict reading of "MUST return aResponse… rather than throwing" forbids it. Something like: rule 8 is about rejecting requests; surfacing misconfiguration by throwing is fine, per §1's "errors that escaperunpropagate to the host."
On the notes-for-review items: I checked jsr.json myself and agree the docs-link gap predates this PR, fine to fix separately. The core README wording follow-up and the no-generics call both seem right to me.
Adds docs/authoring-guide.md — the full third-party authoring path, from defineMiddleware through publishing to composing a third-party middleware in the same pipeline array as the first-party entries. Written for coding agents as much as people: every code block is a complete file labeled with its path, so it can be written to disk and compiled with nothing inferred. Closes with a numbered MUST/NEVER block. The running example, withValidatedBody, is deliberately shaped like the shipped withFeatureFlag (validate mirrors evaluate, 400 mirrors 404), so its code follows verified first-party structure while contributing its own key — which keeps the composition and accumulation examples real rather than hypothetical. Two variants cover `In` prerequisites and the async function* response seam. Every example was typechecked and tested against the real package while writing, and the documented failure strings were confirmed by triggering them. Nothing new runs in CI. Splits the two audiences that src/middleware/README.md previously mixed: it now covers only adding a built-in to this repository, and third-party authoring lives in the new guide. Repoints the cross-links that referred to the old location.
…ttribution - Add a "Client init and getEnv timing" section to §1: on Workers, bindings arrive per request, so getEnv returns undefined in the outer (config) => stage. Documents the lazy first-request init pattern (client ??= …) and gives rule 2 its worked example. - §5: pipeline does accumulation and collision/prereq checking itself and already returns FetchHandler, so satisfies FetchHandler there is inert. Attribute those to pipeline; scope the anchor's effect to the hand-nested form. Both verified against src/core/runtime.ts and src/core/pipeline.ts.
Review nits:
- Intro: scope the completeness claim to path-labeled blocks; unlabeled ones
are fragments that elide with { ... }.
- Rule 8: scope it to rejecting *requests*; throwing on misconfiguration is
fine, since errors that escape run propagate to the host.
- §0: show the destination wired into fetch, and add the nested alternative
(no pipeline import; FetchHandler is a type, so a consumer composing only
third-party middleware needs no runtime import from the package).
- Terminology: first-party -> built-in throughout.
Alignment across the files this PR touches:
- README.md, src/core/README.md: pipeline accumulates and detects collisions
itself; the anchor's effect is scoped to the hand-nested form.
- with-feature-flag.ts: drop "initialize clients" from the outer-stage doc
comment, point at the getEnv timing constraint.
Verified: all 7 path-labeled blocks in the guide extract and compile against
the real source, and the guide's test file runs green (7/7).
71e20a9 to
c09fc7d
Compare
Two thin skills under skills/, following the @supabase/server precedent. They route to the existing docs rather than restating them, so there is no second copy to drift and nothing to update on a release: - supabase-middleware: trigger conditions, package disambiguation against @supabase/ssr and @supabase/server, the flat pipeline model, and a question -> doc routing table. - create-supabase-middleware: a routing shim over docs/authoring-guide.md, carrying only the shape gate and the built-in vs standalone decision. Ship docs/, skills/ and the colocated src READMEs in the npm tarball and on JSR so those routing targets resolve from node_modules, not just in-repo. Shipping docs/ also fixes the guide link not resolving on JSR, noted in #19. Symlink both into .claude/skills/ so agents working in this repo discover them; root skills/ stays canonical for packaging. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two thin skills under skills/, following the @supabase/server precedent. They route to the existing docs rather than restating them, so there is no second copy to drift and nothing to update on a release: - supabase-middleware: trigger conditions, package disambiguation against @supabase/ssr and @supabase/server, the flat pipeline model, and a question -> doc routing table. - create-supabase-middleware: a routing shim over docs/authoring-guide.md, carrying only the shape gate and the built-in vs standalone decision. Ship docs/, skills/ and the colocated src READMEs in the npm tarball and on JSR so those routing targets resolve from node_modules, not just in-repo. Shipping docs/ also fixes the guide link not resolving on JSR, noted in #19. Symlink both into .claude/skills/ so agents working in this repo discover them; root skills/ stays canonical for packaging. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds
docs/authoring-guide.md— the full third-party authoring path, fromdefineMiddlewarethrough publishing to composing a third-party middleware in the samepipelinearray as the first-party entries.Pulled forward from beta into launch 1: it's the hands-on artifact for the Vercel and Resend partner conversations, so it ships with the public alpha.
What's in it
Written for coding agents as much as for people. Every code block is a complete file labeled with its path — an agent can write it to disk and it compiles, with nothing inferred from surrounding context. It closes with a numbered MUST/NEVER block.
The arc: the destination first (your middleware beside
withCorsandwithFeatureFlag) → which form to write → the middleware → exports → tests →package.json→ composition. Then two variants:Inprerequisites, and theasync function*response seam.The running example,
withValidatedBody, is deliberately shaped like the shippedwithFeatureFlag—validatemirrorsevaluate, 400 mirrors 404,ctx.validatedBodymirrorsctx.featureFlag. Its code follows verified first-party structure while contributing its own key, which keeps the composition and accumulation examples real rather than hypothetical. It also demonstrates the buffered request: the middleware readsreq.json()and the handler reads the body again.Two facts that were previously undocumented are now stated:
@supabase/middlewarecan be a normal dependency, not a peer (contexts are marked viaSymbol.for, so duplicate copies interoperate), and the explicitMiddleware<…>annotation is what lets a package publish to JSR.Audience split
src/middleware/README.mdpreviously mixed third-party authoring with the "add a built-in to this repo" checklist. It now covers only the latter (74 lines: built-ins index + subpath wiring), and third-party authoring lives in the new guide. Cross-links insrc/core/README.md, both middleware READMEs,README.md,CONTRIBUTING.md, and one source comment are repointed.Verification
Doc-only — no new CI surface. Every example was checked against the real package while writing, using a scratch harness that aliases
@supabase/middlewareto source:middleware-prereq:/middleware-conflict:text the guide quotestypecheck,lint, 65 tests,build,smoke,attwgreen;typedocbuilds with 0 errors and no new warningsNotes for review
withTiming, not a generator rewrite ofwithValidatedBody— body validation is purely request-side, so rewriting it would have the guide break its own rule 5.src/core/README.mdhas it. That README says such a middleware "can't be a bare entry"; it can in fact be constructed — the error surfaces atsatisfies FetchHandlerand on a one-argument call. I left the core README's wording alone as out of scope; worth a follow-up.as unknown asdouble cast and turns the call curried, which breakswithFoo(config, handler)and stops it working as apipelineentry. One sentence recommends a concrete contribution instead.README.md,src/core/README.md, andtypedoc.jsonwere already Prettier-unformatted, so only the two files authored here were formatted. Anddocs/isn't injsr.json's publish include, so the README's guide link won't resolve on JSR — already true of the old link, happy to fix separately.