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

@startup-api/cloudflare

v0.5.0

Published

[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)

Readme

Startup API Cloudflare App

License

This application uses the Cloudflare Developer Platform, including Workers and DurableObjects, to implement foundational web application functionality. It acts as a transparent proxy for your application, allowing you to inject custom UI elements and intercept specific paths.

Features

  • Transparent Proxying: Forwards requests to your origin application
  • HTML Injection: Uses HTMLRewriter to inject scripts and custom elements (like <power-strip>) into your HTML pages
  • Path Interception: Intercepts requests to a configurable path to serve internal assets

Installation

Start a new project with the npm create startup-api scaffolder. It generates a tiny Cloudflare Worker that pulls this framework in as the @startup-api/cloudflare npm dependency — so you stay up to date with npm update instead of maintaining a fork of this repository.

npm create startup-api my-app -- --origin https://your-app-origin.com
cd my-app
npm run dev      # local dev at http://localhost:8787
npm run deploy   # deploy to Cloudflare

Run npm create startup-api with no arguments to be prompted for the project name and origin URL interactively. Useful flags: --no-install (skip npm install) and --yes / -y (non-interactive — requires a name and --origin).

What you get:

  • A minimal src/index.ts that re-exports the worker plus a wrangler.jsonc you control. The framework ships as the @startup-api/cloudflare dependency, so your project stays small.
  • A .dev.vars file with a random SESSION_SECRET for local development. For production, set your own with npx wrangler secret put SESSION_SECRET.
  • Framework updates are just npm update @startup-api/cloudflare — no fork to rebase.

Then set the required ORIGIN_URL and any OAuth credentials (see Configuration below) and run npm run deploy. See create-startup-api for full details.

Automated deployments

npm run deploy deploys from your machine. To deploy automatically instead, push your scaffolded project to a GitHub repository and use either:

  • Cloudflare Workers GitHub app — connect the repo to Cloudflare's Workers Builds Git integration and Cloudflare builds and deploys on every push, no CI config to maintain.
  • A GitHub Actions workflow — run cloudflare/wrangler-action on push to deploy with Wrangler. Add a CLOUDFLARE_API_TOKEN (and CLOUDFLARE_ACCOUNT_ID) repository secret so the action can authenticate.

Either way, set your production secrets (SESSION_SECRET, OAuth credentials) on the Worker in the Cloudflare dashboard or with npx wrangler secret put rather than committing them.

Working on the framework itself? See CONTRIBUTING.md for cloning and running this repository locally.

Configuration Details

How to set environment variables

  • Using Cloudflare Dashboard (Recommended):

    1. Go to Workers & Pages
    2. Select your worker
    3. Navigate to Settings > Variables
    4. Click Add variable under Environment Variables
    5. Add ORIGIN_URL and any optional variables
    6. Click Save and deploy
  • Using wrangler.jsonc: Add the variables to the "vars" object in your configuration file. See Cloudflare documentation for more details.

| Variable | Required | Default | Description | | :----------------------- | :------- | :-------- | :---------------------------------------------------------------------------- | | ORIGIN_URL | Yes | N/A | The base URL of your origin application (e.g., https://your-app-origin.com) | | USERS_PATH | No | /users/ | The path used to serve internal assets like power-strip.js | | AUTH_ORIGIN | No | N/A | Optional base URL for OAuth redirects (overrides request origin) | | GOOGLE_CLIENT_ID | No | N/A | Google OAuth2 Client ID | | GOOGLE_CLIENT_SECRET | No | N/A | Google OAuth2 Client Secret | | TWITCH_CLIENT_ID | No | N/A | Twitch OAuth2 Client ID | | TWITCH_CLIENT_SECRET | No | N/A | Twitch OAuth2 Client Secret | | PATREON_CLIENT_ID | No | N/A | Patreon OAuth2 Client ID | | PATREON_CLIENT_SECRET | No | N/A | Patreon OAuth2 Client Secret | | PATREON_WEBHOOK_SECRET | No | N/A | Secret for verifying Patreon webhook signatures | | ATPROTO_ENABLED | No | N/A | Set truthy (true/1/yes/on) to enable AT Protocol (Atmosphere) login |

AT Protocol (Atmosphere) login needs no client secret — it is a public OAuth client. Enable it either by setting ATPROTO_ENABLED truthy (a per-deployment switch) or through the createStartupAPI factory (see AT Protocol / Atmosphere below). A factory atproto: { enabled: false } overrides the env flag, so a deployment can force it off.

Environment variables hold only credentials/secrets (OAuth client IDs and all secrets) plus the per‑deployment values ORIGIN_URL, AUTH_ORIGIN, USERS_PATH, ADMIN_IDS, and ENVIRONMENT. All other configuration — OAuth scopes, Patreon campaign id, the access policy, entitlement freshness — is passed to the createStartupAPI factory (see Access policy & provider entitlements).

Setting up OAuth

Google

  1. Go to the Google Cloud Console
  2. Create a new project or select an existing one
  3. Navigate to APIs & Services > Credentials
  4. Click Create Credentials > OAuth client ID
  5. Select Web application as the application type
  6. Add your authorized redirect URI: https://<your-worker-url>/users/auth/google/callback
  7. Copy the Client ID and Client Secret and add them to your Worker's environment variables

Twitch

  1. Go to the Twitch Developer Console
  2. Register a new application
  3. Add your authorized redirect URI: https://<your-worker-url>/users/auth/twitch/callback
  4. Select Website as the category
  5. Copy the Client ID and generate a Client Secret to add them to your Worker's environment variables

Patreon

  1. Go to the Patreon Developer Portal
  2. Click Create Client and fill in your app details
  3. Add your authorized redirect URI: https://<your-worker-url>/users/auth/patreon/callback
  4. Copy the Client ID and Client Secret and add them to your Worker's environment variables

AT Protocol / Atmosphere (atproto)

atproto login is decentralized: there is no central provider to register with and no client secret. Instead the worker acts as a public OAuth client identified by a client-metadata document it serves itself, and it discovers the right authorization server per user from their handle or DID — so it works with bsky.social and any self-hosted PDS alike, with no provider host hardcoded.

Because it has no secrets, atproto is enabled in one of two ways:

  • Env flag (no code): set ATPROTO_ENABLED truthy (true/1/yes/on) — handy for toggling it per deployment (e.g. on in prod, off in preview) without touching code. This uses the default settings.
  • Factory config (for customization): include its config key in createStartupAPI — an empty object is enough, and the optional fields below let you set the client name, resolvers, or scopes.

A factory atproto: { enabled: false } is an explicit opt-out that overrides the env flag, so a deployment can force the provider off.

import { createStartupAPI } from '@startup-api/cloudflare';

const api = createStartupAPI({
  providers: {
    atproto: {}, // including the key enables it — no client id/secret needed
    // All fields below are optional:
    // atproto: {
    //   clientName: 'My App',           // shown on the consent screen (default "StartupAPI")
    //   plcUrl: 'https://plc.directory', // override the PLC directory for did:plc
    //   dohUrl: 'https://cloudflare-dns.com/dns-query', // override the DoH resolver
    //   scopes: 'transition:generic',    // extra scopes on top of the base `atproto`
    //   enabled: false,                  // explicit opt-out (e.g. for dynamically-built config)
    // },
  },
});

export default api.default;
export const { UserDO, AccountDO, SystemDO, CredentialDO } = api;
  1. Enable it — set ATPROTO_ENABLED truthy, or include atproto: {} in the factory providers config (no client id/secret needed either way). A factory enabled: false forces it off.

  2. Deploy over HTTPS with a stable hostname. The worker automatically serves its client metadata at https://<your-worker-url>/users/auth/atproto/client-metadata.json (this URL is the OAuth client_id) and registers the redirect URI https://<your-worker-url>/users/auth/atproto/callback.

  3. That's it. When a visitor clicks Login with your Atmosphere account, they're asked for their handle (e.g. alice.bsky.social) or DID; the worker then resolves it through the full atproto discovery chain and redirects them to their own server to sign in:

    handle ─▶ DID            (HTTPS .well-known/atproto-did, then DNS _atproto.<handle> via DoH)
    DID    ─▶ DID document   (did:plc via the PLC directory, did:web via the domain)
    DID doc─▶ PDS endpoint    (the #atproto_pds service)
    PDS    ─▶ auth server     (.well-known/oauth-protected-resource → oauth-authorization-server)

The flow uses PKCE, DPoP-bound (sender-constrained) tokens, and Pushed Authorization Requests (PAR) as required by the atproto OAuth profile. The PLC directory and DNS-over-HTTPS resolver are generic infrastructure and can be overridden via the plcUrl / dohUrl factory options.

Requesting additional scopes

Each provider requests the minimal scopes needed to sign a user in and read their basic profile. To request more (for example, to read a user's Patreon memberships), set the provider's scopes in the factory config (a string or array). The extra scopes are merged with the required base scopes:

import { createStartupAPI } from '@startup-api/cloudflare';

const api = createStartupAPI({
  providers: {
    // Adds `identity.memberships` on top of the base `identity identity[email]`
    patreon: { scopes: 'identity.memberships' },
  },
});

Example wrangler.jsonc snippet:

{
  "vars": {
    "ORIGIN_URL": "https://your-app-origin.com"
  }
}

How It Works

  1. Request Interception: The worker receives all incoming requests
  2. Path Mapping: If the request path starts with USERS_PATH, the worker serves assets directly from the public/users/ directory
  3. Proxying: All other requests are proxied to the configured ORIGIN_URL
  4. Injection: For text/html responses, the worker injects a <script> tag and a <power-strip> custom element before serving the content to the user

Customizing the power strip

By default the worker injects its own <power-strip> pinned to the top-right corner of the page. If that overlaps your own menu or you simply want it somewhere else, place a <power-strip> element in your own HTML. When the worker sees one, it injects only power-strip.js (which defines the custom element) and leaves your element exactly where you put it — so you control placement and styling:

<nav>
  <!-- ...your links... -->
  <power-strip></power-strip>
</nav>
  • providers is optional. If you omit it, the worker fills in the active providers for you (e.g. providers="google,twitch,patreon"). Set it yourself to override which login buttons appear.
  • Prefer an explicit closing tag. <power-strip></power-strip> and <power-strip/> are both detected, but per the HTML spec <power-strip/> is not truly self-closing — the browser treats it as an open tag and nests the following content inside it. Use a closing tag (or place the element last in its container) to avoid surprises.
  • Script-only opt-out. Use <power-strip hidden> to load power-strip.js (and its JS API) without rendering a visible strip.

Access policy & provider entitlements

StartupAPI can gate access to paths and forward the visitor's login/entitlement status to your origin so it can render gated UI. This is provider-agnostic infrastructure; only Patreon currently implements perk-level (benefit/tier) checks — Google and Twitch participate at the login levels only.

Path-based access policy

Configure an ordered list of rules (first match wins) mapping a path pattern to a requirement, plus a default for unmatched paths. Requirement modes:

  • bypass — raw pass-through: no credential check, no identity resolution, no headers, no power-strip injection.
  • public — anyone; the session is resolved and identity/entitlement headers are forwarded when present.
  • authenticated — any logged-in user.
  • entitlement — a provider condition: Patreon active_patron, a specific benefit (perk) ID, or a tier ID.

Patterns are exact (/special), prefix (/app/*), or / (homepage only). Each rule's on_unauthorized is login (redirect to sign in), forbidden (403), upgrade (redirect to upgrade_url, e.g. a Patreon join page), or gate (serve an explainer page in place, with no redirect — see below). When no policy is configured at all, every path is treated as public (backward compatible).

Serving a gate page in place (on_unauthorized: 'gate')

Instead of redirecting, a denied request can serve an explainer page at the requested URL (no redirect, status 200 by default). The page shown depends on login state, so anonymous and logged-in-but-unentitled visitors can see different copy:

  • anonymous (required) — shown to visitors who are not logged in (e.g. a "become a patron + log in" page).
  • unentitled (optional) — shown to logged-in visitors who fail the requirement (e.g. a "pledge/upgrade" page). Falls back to anonymous when omitted.
  • status (optional) — HTTP status for the served page; defaults to 200 to preserve typical explainer-page UX (set 403 if you prefer).

Each variant is a PageSource whose body comes from either the ASSETS binding or a path proxied from ORIGIN_URL — exactly one of:

  • { asset: '/early-access' } — a local file from ASSETS (resolved like other assets, /early-accessearly-access.html).
  • { origin: '/early-access' } — a path proxied from ORIGIN_URL. The path must be reachable directly on the origin (the raw site, not behind this worker).

The gate config is set per rule via gate, or on the policy default via default_gate. The served gate page is produced inside the deny path, so it is not re-subjected to the access policy and no power-strip is injected — the page is expected to carry its own login CTA. Because nothing redirects, there is no loop risk.

const accessPolicy = {
  default: { mode: 'entitlement', provider: 'patreon', condition: { type: 'benefit', benefit_id: '<BENEFIT_ID>' } },
  default_on_unauthorized: 'gate',
  default_gate: {
    anonymous: { origin: '/early-access' }, // or { asset: '/early-access' }
    unentitled: { origin: '/pledge-needed' }, // or { asset: '/pledge-needed' }
    // status: 403, // optional; defaults to 200
  },
};

Admin users (those listed in ADMIN_IDS) bypass every authenticated/entitlement requirement and can reach any gated path. Their identity is still resolved and the usual identity/entitlement headers are forwarded to the origin — only the gate itself is skipped. (bypass paths remain a raw pass-through for everyone, with no identity resolution.)

The policy is passed to the factory as accessPolicy (see below). Example:

const accessPolicy = {
  rules: [
    { pattern: '/', requirement: { mode: 'public' } },
    {
      pattern: '/special',
      requirement: { mode: 'entitlement', provider: 'patreon', condition: { type: 'benefit', benefit_id: '<BENEFIT_ID>' } },
      on_unauthorized: 'upgrade',
      upgrade_url: 'https://www.patreon.com/yourpage',
    },
  ],
  default: { mode: 'entitlement', provider: 'patreon', condition: { type: 'active_patron' } },
};

Headers forwarded to the origin

For non-bypass paths the worker forwards X-StartupAPI-Authenticated, X-StartupAPI-Login-Provider, a compact X-StartupAPI-Entitlements JSON, and (for Patreon) X-StartupAPI-Patreon-Active / -Tiers / -Benefits alongside the existing X-StartupAPI-User-Id / -Account-Id.

Keeping entitlements fresh

Entitlements are fetched at login and then kept fresh per-provider via three mechanisms. Durations below are a named-unit object ({ days: 1 }, { minutes: 15 } — units are summed) or a plain number of milliseconds.

  • TTL (entitlementTtl) — lazily re-check on the request path when the cache is older than the duration, using the OAuth refresh token. On by default for entitlement providers: default 1 day for Patreon (memberships change slowly; this backstops missed webhooks) and 15 min for other providers. Pass a Duration to tune it (entitlementTtl: { hours: 6 }), or entitlementTtl: false to disable (e.g. rely on the webhook only).
  • Cron (entitlementCron: { schedule }) — a scheduled re-sync of all of a provider's credentials. Off by default. The scheduled() handler is only present when at least one provider enables cron; you must also add a matching triggers.crons to your wrangler config.
  • Webhook (Patreon only, entitlementWebhook: true) — off by default. Provide PATREON_WEBHOOK_SECRET (a secret, in env) and point a Patreon webhook at <your-worker-url>/users/webhooks/patreon (signature verified with HMAC-MD5).

Set providers.patreon.campaignId to disambiguate when a user belongs to multiple campaigns.

Login session lifetime

Login sessions are separate from entitlement freshness: how long a user stays logged in (authentication) is independent of how often their paid status is re-checked (authorization). A user can stay logged in for weeks while their entitlements are refreshed every few minutes via TTL/webhook — and a churned subscriber still loses access within the entitlement window regardless of session validity.

  • Default lifetime is 30 days with a rolling window: on activity, once less than half the window remains, the session expiry is pushed forward and the session_id cookie's Max-Age is refreshed. Active users effectively never have to re-login; idle users expire after the window.
  • The session_id cookie is a persistent cookie (Max-Age), so it survives browser restarts.
  • Configure the window via the factory (session.ttl is a Duration):
const api = createStartupAPI({
  session: { ttl: { days: 30 } }, // default; lower/raise as needed (or a number of ms)
});

Breaking in 0.5.0: entitlement freshness moved from the freshness: { ttl, cron, webhook } block to flat per-provider keys entitlementTtl / entitlementCron / entitlementWebhook; entitlementTtl is now on by default (1 day for Patreon) and takes a Duration or false. The default session lifetime went from a fixed 24h (no renewal) to a rolling 30 days, and session_id is now a persistent cookie.

Configuring via the factory

Environment variables hold only credentials/secrets and the per-deployment values (ORIGIN_URL, AUTH_ORIGIN, USERS_PATH, ADMIN_IDS, ENVIRONMENT). Everything else — provider scopes, Patreon campaign id, the access policy, and entitlement freshness — is passed to createStartupAPI(config). The plain re-export still works with defaults:

// Defaults — unchanged:
export { default, UserDO, AccountDO, SystemDO, CredentialDO } from '@startup-api/cloudflare';
// Custom configuration:
import { createStartupAPI } from '@startup-api/cloudflare';

const api = createStartupAPI({
  providers: {
    patreon: {
      scopes: 'identity.memberships',
      campaignId: '<CAMPAIGN_ID>',
      // entitlementTtl is on by default (1 day for Patreon); tune with a Duration or set false to disable.
      entitlementTtl: { hours: 12 },
      entitlementCron: { schedule: '0 */6 * * *' },
      entitlementWebhook: true,
    },
  },
  session: { ttl: { days: 30 } }, // optional; login session lifetime (default 30d, rolling)
  accessPolicy: {
    rules: [
      /* ... */
    ],
    default: { mode: 'public' },
  },
});

export default api.default; // includes scheduled() because cron is enabled
export const { UserDO, AccountDO, SystemDO, CredentialDO } = api;

(Remember to add triggers.crons to your wrangler config when enabling cron.)

Contributing

Contributions are welcome! See CONTRIBUTING.md for how to clone, run, test, and submit changes to the framework.

License

Licensed under the Apache License, Version 2.0. See LICENSE for details.