@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
Maintainers
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
updateor 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
Install it:
npm install @lopebase/adapterCreate 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 # VercelKeep the same value for step 5. Do not commit it.
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), }, ], });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)) });Check it, locally or deployed, before connecting:
LOPEBASE_SIGNING_SECRET=... npx lopebase adapter check http://localhost:3000/api/lopebaseIt 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.
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 withnpx 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.
