docs: agent-first "build your own middleware" authoring guide - #19
docs: agent-first "build your own middleware" authoring guide#19tomaspozo wants to merge 1 commit into
Conversation
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.
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.
| a single flat array, every contribution typed on `ctx`: | ||
|
|
||
| ```ts | ||
| pipeline( |
There was a problem hiding this comment.
The destination might be better demonstrated with the fetch call inline:
export default {
fetch: pipeline(
[withCors({}), withFeatureFlag({ ... }), withValidatedBody({ ... })],
async (_req, ctx) => Response.json({ data: ctx.validatedBody.data }),
)
}| }) | ||
|
|
||
| // Type-level check, verified by `tsc`: the composed stack is a fetch entry. | ||
| const _anchored = withValidatedBody( |
There was a problem hiding this comment.
I had to read these tests a few times over, but that might be necessary complexity.
| # Build your own middleware | ||
|
|
||
| This guide walks the full path: from `defineMiddleware` to publishing your own | ||
| package, to composing it in the same `pipeline` array as the first-party |
There was a problem hiding this comment.
This guide never shows the nested syntax as a destination favouring pipeline(), which might be by design.
It could be worth an additional demonstration because it does save you
import { pipeline } from "@supabase/middleware"which could be an entire package dependency on it's own.
| ``` | ||
|
|
||
| There is no registry to join and no plugin interface to implement. A middleware | ||
| is a function produced by `defineMiddleware`; first-party and third-party |
There was a problem hiding this comment.
Maybe first-party and 3rd-party is ambiguous here, we might say "Supabase-authored" or "built-in" instead.
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.