npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@supabase-labs/middleware-openfeature

v0.1.0

Published

Typed OpenFeature flag resolution as a @supabase/middleware middleware. Resolves every declared flag per request and contributes the values at ctx.flags.

Readme

@supabase-labs/middleware-openfeature

Typed OpenFeature flag resolution as a @supabase/middleware middleware.

Declare your flags with their mandatory defaults, hand it any OpenFeature server client, and every declared flag is resolved concurrently per request and lands at ctx.flags, typed from the defaults.

import { OpenFeature } from '@openfeature/server-sdk'
import { withOpenFeature } from '@supabase-labs/middleware-openfeature'

const client = OpenFeature.getClient()

export default {
  fetch: withOpenFeature(
    { client, flags: { betaCheckout: false, theme: 'light', maxItems: 10 } },
    async (_req, ctx) => {
      ctx.flags.betaCheckout // boolean
      ctx.flags.theme // string
      ctx.flags.maxItems // number
      return Response.json(ctx.flags)
    },
  ),
}

Install

npm install @supabase-labs/middleware-openfeature @openfeature/server-sdk

@openfeature/server-sdk is yours, not ours. This package depends only on @supabase/middleware and the zero-dependency @openfeature/core, and accepts the client structurally — so it never pulls @openfeature/server-sdk's node:events into your bundle, and you stay free to pick your own SDK version.

Requires TypeScript >= 5.4. The published types use NoInfer, a 5.4 intrinsic.

How it works

flags is a map of key to its mandatory default. There is no way to declare a flag without one — that is the OpenFeature invariant, and it is why resolution can never fail.

Each default's runtime type selects the resolution method:

| Default | Method called | | ------------------------------------- | ------------------- | | boolean | getBooleanDetails | | string | getStringDetails | | number | getNumberDetails | | anything else (object, array, null) | getObjectDetails |

All declared flags resolve concurrently, so a stack of ten flags costs one round trip's latency rather than ten.

The declared default's type must match the flag's type

This is the one sharp edge. The default you write picks the resolution method, so if it disagrees with the flag's type in your provider, the provider returns a TYPE_MISMATCH error, the flag falls back to your default, and nothing tells you at runtime:

// `theme` is a STRING flag in the provider
withOpenFeature({ client, flags: { theme: false } }, handler)
//                                        ^^^^^ boolean
// ctx.flags.theme === false, with a 200 response and no error anywhere

ctx.flags holds values, not details, so a handler cannot tell a real false from a fallback false. That is a deliberate trade — see below — but it means a wrong default type fails silently.

If you need to check, ask your OpenFeature client directly and look at reason:

const details = await client.getBooleanDetails('theme', false)
details.reason // 'ERROR' with errorCode 'TYPE_MISMATCH' when the type is wrong

It never gates

The middleware never short-circuits. An OpenFeature client already returns the default with reason: 'ERROR' rather than throwing; if a client rejects anyway, the flag falls back to its declared default and the handler still runs. A provider outage is not a bad request, so there is nothing to reject.

If you want to gate on a flag, that is withFeatureFlag's job.

ctx.flags holds values, not details

EvaluationDetails also carries reason, variant, errorCode and flagMetadata. Those matter when debugging a flag that silently fell back — but they belong on your OpenFeature client, via OpenFeature hooks, which is a standard feature of the client you already own. This package adds no surface for it, and contributes exactly one ctx key.

Targeting

context builds the OpenFeature evaluation context per request. It receives the Request and the accumulated upstream context, so a targeting key can come from an upstream auth middleware:

import { withClaims } from '@supabase/server'

withClaims(
  withOpenFeature(
    {
      client,
      flags: { betaCheckout: false },
      context: (_req, ctx) => ({ targetingKey: ctx.jwtClaims?.sub ?? 'anon' }),
    },
    async (_req, ctx) => Response.json({ beta: ctx.flags.betaCheckout }),
  ),
)

With pipeline, a context callback that reads upstream needs one annotation

import { pipeline } from '@supabase/middleware'
import { withClaims } from '@supabase/server'
import type { JWTClaims } from '@supabase/server'

pipeline(
  [
    withClaims(),
    withOpenFeature({
      client,
      flags: { betaCheckout: false },
      //                                  ↓ this annotation is required
      context: (_req, ctx: { jwtClaims: JWTClaims | null }) => ({
        targetingKey: ctx.jwtClaims?.sub ?? 'anon',
      }),
    }),
  ],
  async (_req, ctx) => Response.json({ beta: ctx.flags.betaCheckout }),
)

Why: withOpenFeature({…}) is a complete expression, so TypeScript checks it before pipeline sees the array. Array-position information cannot flow backwards into an already-checked argument. It is inherent to evaluation order, not a defect.

What it does not affect: composition, ordering, prerequisites, and handler typing all work normally under pipeline — the handler sees every upstream key and ctx.flags at full fidelity, with nothing annotated. And a context callback that ignores the upstream context needs no annotation either. At runtime the accumulated context is passed through in both forms, so only the static type differs.

Bring your own client

FlagClient is four methods. Any OpenFeature server client satisfies it as-is, and anything else can implement it directly:

import type { FlagClient } from '@supabase-labs/middleware-openfeature'

const client: FlagClient = {
  async getBooleanDetails(flagKey, defaultValue) {
    return { flagKey, value: defaultValue, reason: 'STATIC' }
  },
  // …getStringDetails, getNumberDetails, getObjectDetails
}

Running on Deno and Supabase Edge Functions

Verified end to end against a real Vercel flag on both plain Deno 2.7.8 and the Supabase Edge Runtime 1.74.3. A string flag resolved to its dashboard value with reason: STATIC and arrived at ctx.flags unchanged, on both runtimes.

Two things worth reading before you try it: the provider reads FLAGS, not EDGE_CONFIG, and it needs sys access to "hostname" at module load. Full results, including the OIDC token's 12-hour limit and why an SDK key is the better credential, are in docs/deno-vercel-findings.md.

Examples

Four handlers — Vercel Flags and PostHog, basic and claims-based targeting — each running unmodified on Supabase Edge Functions, Vercel Functions and Cloudflare Workers. Eight of the twelve combinations are verified against live providers.

See examples/, which also carries the credentials matrix: Vercel Flags needs nothing on Vercel and an SDK key everywhere else.

Development

pnpm install
pnpm typecheck           # source + must-compile type tests
pnpm typecheck:negative  # must-NOT-compile type tests, message-asserted
pnpm typecheck:consumer  # a consumer at the TypeScript 5.4 floor
pnpm test
pnpm build

This package was built from scratch against docs/authoring-guide.md as a dogfooding exercise. Every place the guide fell short is recorded in docs/authoring-guide-friction.md.

Releases

Not automated yet. The package is not published to npm, and there is no release workflow.

When it is time to publish, @supabase/middleware's own .github/workflows/release.yml and release-please-config.json are the template: release-please opens a release PR from conventional commits, and npm publishing uses OIDC trusted publishing rather than a token. That setup needs two repository secrets, GH_APP_ID and GH_APP_PRIVATE_KEY.

License

MIT