Skip to content

docs: agent-first "build your own middleware" authoring guide - #19

Merged
mandarini merged 3 commits into
mainfrom
tomas/sdk-1408-authoring-guide-build-your-own-middleware
Aug 11, 2026
Merged

docs: agent-first "build your own middleware" authoring guide#19
mandarini merged 3 commits into
mainfrom
tomas/sdk-1408-authoring-guide-build-your-own-middleware

Conversation

@tomaspozo

Copy link
Copy Markdown
Member

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.

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 withCors and withFeatureFlag) → which form to write → the middleware → exports → tests → package.json → composition. Then two variants: In prerequisites, and the async function* response seam.

The running example, withValidatedBody, is deliberately shaped like the shipped withFeatureFlagvalidate mirrors evaluate, 400 mirrors 404, ctx.validatedBody mirrors ctx.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 reads req.json() and the handler reads the body again.

Two facts that were previously undocumented are now stated: @supabase/middleware can be a normal dependency, not a peer (contexts are marked via Symbol.for, so duplicate copies interoperate), and the explicit Middleware<…> annotation is what lets a package publish to JSR.

Audience split

src/middleware/README.md previously 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 in src/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/middleware to source:

  • All six file-blocks typecheck, and are byte-identical to the blocks in the guide (machine-checked after Prettier reformatted it, not just before)
  • 11 example tests pass, including the body-reread guarantee
  • The documented failure strings were confirmed by actually triggering them — wrong ordering and duplicate keys produce exactly the middleware-prereq: / middleware-conflict: text the guide quotes
  • typecheck, lint, 65 tests, build, smoke, attw green; typedoc builds with 0 errors and no new warnings

Notes for review

  • The seam variant is withTiming, not a generator rewrite of withValidatedBody — body validation is purely request-side, so rewriting it would have the guide break its own rule 5.
  • The prerequisite guarantee is stated more precisely than src/core/README.md has it. That README says such a middleware "can't be a bare entry"; it can in fact be constructed — the error surfaces at satisfies FetchHandler and on a one-argument call. I left the core README's wording alone as out of scope; worth a follow-up.
  • No generics section. The generic-contribution version compiles but needs an as unknown as double cast and turns the call curried, which breaks withFoo(config, handler) and stops it working as a pipeline entry. One sentence recommends a concrete contribution instead.
  • Pre-existing, untouched: README.md, src/core/README.md, and typedoc.json were already Prettier-unformatted, so only the two files authored here were formatted. And docs/ isn't in jsr.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.

@tomaspozo
tomaspozo requested a review from a team August 7, 2026 02:53
@tomaspozo
tomaspozo requested review from a team as code owners August 7, 2026 02:53
@pkg-pr-new

pkg-pr-new Bot commented Aug 7, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@supabase/middleware@19

commit: c09fc7d

@mandarini mandarini left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 the package.json block 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. withResend throws on a missing API key (reasonably, I think), but a strict reading of "MUST return a Response … rather than throwing" forbids it. Something like: rule 8 is about rejecting requests; surfacing misconfiguration by throwing is fine, per §1's "errors that escape run propagate 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.

@mandarini mandarini assigned mandarini and tomaspozo and unassigned mandarini Aug 7, 2026
Comment thread docs/authoring-guide.md Outdated
Comment thread docs/authoring-guide.md
Comment thread docs/authoring-guide.md Outdated
Comment thread docs/authoring-guide.md Outdated
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).
@mandarini
mandarini force-pushed the tomas/sdk-1408-authoring-guide-build-your-own-middleware branch from 71e20a9 to c09fc7d Compare August 11, 2026 09:55
@mandarini
mandarini merged commit 92dd4cc into main Aug 11, 2026
5 checks passed
@mandarini
mandarini deleted the tomas/sdk-1408-authoring-guide-build-your-own-middleware branch August 11, 2026 10:02
mandarini added a commit that referenced this pull request Aug 11, 2026
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>
mandarini added a commit that referenced this pull request Aug 11, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants