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

@lopebase/adapter

v0.2.0

Published

Show your app's users and data in LopeBase: a small, read-only, signed admin adapter for any JavaScript backend.

Downloads

818

Readme


LopeBase calls this endpoint with signed requests, and you decide exactly which fields it can see.

You do not need this to use LopeBase. Analytics, search, and revenue sources connect without touching your code. Add the adapter when you want your own data (signups, users, plans) next to those numbers.

What it does, and what it never does

  • Read-only by default. It lists and shows the resources you declare. It cannot change anything unless you add an update or an action yourself.
  • Only fields you declare leave your app. Every row is cut down to the fields you list. Anything else a query returns is dropped.
  • Secrets cannot slip out. A field named like a password, hash, token, API key, session, or card detail stops the adapter from starting, unless you mark that one field sensitive: "allow".
  • You choose how personal data leaves. Emails, names, phone numbers, and text people wrote go out as stored (full), masked so rows can be told apart but nobody can be contacted (masked: d••@acme.com, D. E.), or not at all (ids_only). The adapter will not start with personal fields until you pick one. The same cut applies to action results and audit snapshots. The overview is sent as you return it, so keep people out of it.
  • Signed requests only. LopeBase signs each request with a secret only your app and LopeBase know (HMAC-SHA256 over the time, method, path, and body). The secret never crosses the wire. A captured request is useless after five minutes, and a changed request fails the check.
  • Bounded. At most 100 rows per page, and 600 authenticated requests a minute per running instance by default.
  • Quiet on failure. Errors are logged in your app; LopeBase only ever sees "adapter error", never a stack trace or a connection string.
  • No dependencies, no network calls of its own. Web Crypto only, so it runs on Cloudflare Workers, Node 18+, Vercel, Netlify, Deno, and Convex.

Set it up

  1. Install it:

    npm install @lopebase/adapter
  2. Create a signing secret and store it where your app keeps secrets. The command prints only the secret, so pipe it straight in:

    npx lopebase adapter secret | npx wrangler secret put LOPEBASE_SIGNING_SECRET   # Cloudflare
    npx lopebase adapter secret | vercel env add LOPEBASE_SIGNING_SECRET production   # Vercel

    Keep the same value for step 5. Do not commit it.

  3. Describe what LopeBase may read:

    // lib/lopebase.ts
    import { createAdminAdapter, memoryAuditStore, signedAuth } from "@lopebase/adapter";
    import { db } from "./db";
    
    export const lopebase = createAdminAdapter({
      connectorId: "my-app",
      // How emails, names, and phones reach LopeBase: "full", "masked", or "ids_only"
      personalData: "masked",
      authenticate: signedAuth({
        secrets: [process.env.LOPEBASE_SIGNING_SECRET, process.env.LOPEBASE_SIGNING_SECRET_PREVIOUS],
        capabilities: ["users.read", "system.read"],
      }),
      audit: memoryAuditStore(), // read-only: nothing is written
      async overview() {
        return {
          attentionItems: [],
          // Standard names feed LopeBase's dashboard (see "The numbers on your dashboard")
          stats: {
            users: await db.countUsers(),
            signups_7d: await db.countSignupsSince(7),
            active_users_28d: await db.countActiveSince(28),
          },
        };
      },
      resources: [
        {
          name: "users",
          label: "Users",
          readCapability: "users.read",
          fields: [
            { name: "id", label: "ID", type: "string" },
            { name: "email", label: "Email", type: "string", inList: true },
            { name: "plan", label: "Plan", type: "string", inList: true },
            { name: "created_at", label: "Joined", type: "date", inList: true },
          ],
          // Free text people wrote is not recognized by name; mark it:
          // { name: "message", label: "Message", type: "string", personal: "text" }
          // limit is at most 100; cursor is whatever you returned as nextCursor
          list: async ({ limit, cursor, search }) => db.listUsers({ limit, cursor, search }),
          get: async (id) => db.getUser(id),
        },
      ],
    });
  4. Mount it at /api/lopebase:

    // Next.js: app/api/lopebase/[[...path]]/route.ts
    import { lopebase } from "@/lib/lopebase";
    export const GET = (req: Request) => lopebase.handle(req);
    // Hono or plain Workers
    app.all("/api/lopebase/*", (c) => lopebase.handle(c.req.raw));
    // Convex: convex/http.ts
    http.route({ pathPrefix: "/api/lopebase/", method: "GET", handler: httpAction((_, req) => lopebase.handle(req)) });
  5. Check it, locally or deployed, before connecting:

    LOPEBASE_SIGNING_SECRET=... npx lopebase adapter check http://localhost:3000/api/lopebase

    It runs the same checks LopeBase relies on: signatures required, stale and altered requests refused, no undeclared or sensitive fields, pages capped. Against a live adapter it only reads.

  6. In LopeBase, open the product and connect Your app with the base URL (https://your-app.com/api/lopebase) and the same secret. Or let your coding agent do it with npx lopebase.

The numbers on your dashboard

LopeBase's dashboard and weekly summary read these names from overview().stats. Report the ones you have; each is counted in your own database, so it is exact however many users you have, and no rows need to leave your app for it.

| Stat | Meaning | | --- | --- | | users | Total users (or accounts) | | signups_7d | New users in the last 7 days | | active_users_28d | Users active in the last 28 days | | mrr_usd | Monthly recurring revenue, in dollars | | paying_customers | Customers on a paid plan |

Any other stats you add show up as extra detail.

Rotating the secret

Set the new value as LOPEBASE_SIGNING_SECRET and the old one as LOPEBASE_SIGNING_SECRET_PREVIOUS, deploy, update the secret in LopeBase, then remove the previous one. Both are accepted in between, so nothing breaks.

Testing in your own suite

import { conformanceChecks } from "@lopebase/adapter/conformance";
import { lopebase } from "../lib/lopebase";

for (const c of conformanceChecks({ adapter: lopebase, secret: "lbs_test_secret_at_least_16" })) {
  it(c.name, c.run);
}

The contract

| Method + path | Auth | Returns | | --- | --- | --- | | GET /api/lopebase/health | none | { ok }, no data | | GET /api/lopebase/overview | signed | { attentionItems, stats } | | GET /api/lopebase/resources | signed | the resources and fields you declared | | GET /api/lopebase/resources/:name?limit=&cursor=&search= | signed | { rows, nextCursor? } | | GET /api/lopebase/resources/:name/:id | signed | the row, or 404 | | PATCH, POST /actions, GET /audit | signed | only if you add updates or actions |

Requests carry LopeBase-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256> over t.METHOD.path?query.sha256(body). signRequest and verifyRequestSignature are exported if you want to check it yourself.

Adapters built before signing used /api/admin and a bearer token. They keep working: set basePath: "/api/admin", and pass the old token as signedAuth({ ..., allowBearer }) while you move over.

Help

Questions or a security concern: [email protected]. Privacy: app.lopebase.com/legal/privacy.

MIT licensed. LopeBase is made by Jackalope Digital.