Skip to content

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

Open
tomaspozo wants to merge 1 commit into
mainfrom
tomas/sdk-1408-authoring-guide-build-your-own-middleware
Open

docs: agent-first "build your own middleware" authoring guide#19
tomaspozo wants to merge 1 commit 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.

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.
@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: 2005f0b

@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
a single flat array, every contribution typed on `ctx`:

```ts
pipeline(

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.

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 }),
  )
}

Comment thread docs/authoring-guide.md
})

// Type-level check, verified by `tsc`: the composed stack is a fetch entry.
const _anchored = withValidatedBody(

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.

I had to read these tests a few times over, but that might be necessary complexity.

Comment thread docs/authoring-guide.md
# 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

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 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.

Comment thread docs/authoring-guide.md
```

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

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.

Maybe first-party and 3rd-party is ambiguous here, we might say "Supabase-authored" or "built-in" instead.

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.

3 participants