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

@wtfalch/flags

v0.1.0

Published

The estate's feature flags: a flag declared in code, its rules stored in a table, one resolver per org then per estate then the default.

Readme

@wtfalch/flags

The estate's feature flags: a flag declared in code, its rules stored in a table, and one resolver that answers per organisation, then per estate, then the declared default. Extracted from app-template/src/lib/flags/ (ADR 0017 there), generalised into a library every generated app can depend on instead of copying.

Status

v1. Not published yet: that needs the user's npm 2FA. Both dependencies — @wtfalch/authz and @wtfalch/audit — are on npm already.

Install

pnpm add @wtfalch/flags
pnpm exec flags-migrations   # copies migrations/*.sql into drizzle/ as the next number

The copy is recorded in drizzle/.flags-migrations.json; running it again copies nothing. Apply the copied file with the host's own migrate script.

Use

  1. Apply the migration — either the file flags-migrations copied into drizzle/, or, for a quick local setup with no host migrate script, migrate() against any drizzle DbOrTx:

    import { migrate } from '@wtfalch/flags';
    
    await migrate(db); // idempotent
  2. Declare the app's own flags. Nothing is declared here — the way a template has no feature of its own to gate — a host calls defineFlags once:

    import { defineFlags } from '@wtfalch/flags';
    
    export const FLAGS = defineFlags({
      'billing:new-invoices': {
        description: 'The new invoice flow.',
        kind: 'release',       // 'release' | 'kill' | 'ops'
        owner: 'platform',
        on: false,
        expires: '2027-01-31', // required for 'release', forbidden otherwise
      },
      'search:fallback': {
        description: 'Fall back to the old search index.',
        kind: 'kill',           // must default on: it exists to be turned OFF
        owner: 'platform',
        on: true,
      },
    });
    
    export type Flag = keyof typeof FLAGS & string;

    defineFlags validates at import time — a typo, a missing expiry on a release flag, or a kill flag defaulting off all throw immediately, every problem at once. It hands the object back with its literal type, so flag(db, FLAGS, 'billing:new-invoices') is a compile error on a misspelled key once the host narrows to its own Flag union.

    Add overdueReleaseFlags(FLAGS) to the host's own test suite to get ADR 0017's mechanical expiry check — a release flag that outlives its date fails the test rather than quietly becoming permanent:

    import { overdueReleaseFlags } from '@wtfalch/flags';
    
    it('has no release flag past the day it was meant to go', () => {
      expect(overdueReleaseFlags(FLAGS)).toEqual([]);
    });
  3. Read a flag:

    import { flag } from '@wtfalch/flags';
    
    const on = await flag(db, FLAGS, 'billing:new-invoices', { tenantId });

    tenantId is the organisation the request is about, when there is one. Precedence: that organisation's rule, then the estate-wide rule, then the declared default. An undeclared flag is off, whatever its rules say — fail closed.

    No built-in per-request cache. app-template's original wrapped this in React's cache() so one request loads every rule once; this package makes no assumption about Next.js/RSC. A host on RSC wraps its own loadRules-equivalent call (or just flag's first call per request) in cache() if it wants that; a host with no framework request scope calls it as many times as it likes — there are tens of rows, not thousands.

    A flag read never throws. If the database is unreachable, every flag answers with its declared default and the failure goes to console.error — a flag is not an authorisation decision.

  4. Set or clear a rule, gated by flags:update (see step 6):

    import { setFlagRule, clearFlagRule } from '@wtfalch/flags';
    
    const change = await setFlagRule(db, {
      key: 'billing:new-invoices',
      tenantId,            // null for the estate-wide rule
      enabled: true,
      note: 'pilot for org-42',
      updatedBy: personId,
    });
    // { key, tenantId, before, after } | null — null when nothing changed
    // (setting a rule to what it already says is not an event)
    
    await clearFlagRule(db, { key: 'billing:new-invoices', tenantId });

    Pass an AuditOptions (a bound @wtfalch/audit AuditWriter plus the acting Actor) as the third argument to either function to write the flag.changed audit event — carrying the rule before and after — in the same transaction as the change (ADR 0017: "Changing a rule is an audit event"):

    await setFlagRule(db, input, { writer: auditWriter, actor });
  5. Everything for a dashboard, in one call:

    import { flagStates } from '@wtfalch/flags';
    
    const states = await flagStates(db, FLAGS);
    // [{ key, declaration, effective, expiry, global, perTenant }]

    declaration is undefined for a rule whose flag has been deleted from the code — flagStates is where a host's page finds rows to clean up. expiry is { state: 'none' | 'fine' | 'soon' | 'overdue', days? } for a release flag; soon is the last fortnight.

  6. Authorization, checked before every write above (this package's store functions do not check it themselves — the same separation package-template's widget toy and @wtfalch/people keep):

    import { catalogue, checkFlagsRead, checkFlagsUpdate } from '@wtfalch/flags';
    import { resourceAccess } from '@wtfalch/authz';
    
    const access = resourceAccess({ catalogue, principal, /* ...organisations, grants */ });
    const result = checkFlagsUpdate(access, {
      id: 'billing:new-invoices', type: 'flag', applicationId, platformId, organisationId, teamId: null,
    });
    if (!result.allowed) throw new Error(result.reason);

    Two permissions: flags:read and flags:update — not platform.flags:read/platform.flags:update (ADR 0017's own app-level namespace). A host composes this module's vocabulary into its own the same way it does @wtfalch/people's.

Tests

pnpm test                                  # PGlite, in memory
TEST_DATABASE_URL=postgres://... pnpm test # a real Postgres; a scratch schema per run

Not in v1

Experiments, percentage rollouts and analytics (the root README's own scope). No dashboard/UI component — app-template's src/app/flags/ (the page) sits outside src/lib/flags/, the extraction source, and outside this package's scope.