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

@revenexx/app-sdk

v0.7.0

Published

Typed data client + HTTP router for revenexx Apps. Swappable adapters (mock | remote | runtime) so the same App code runs against in-memory fixtures locally and the real data plane in production.

Readme

@revenexx/app-sdk

The SDK for building revenexx Apps — typed data access and HTTP routing for the revenexx Revenue Cloud app platform.

A revenexx App declares its data model in a schema.json and its identity and permissions in a manifest.json. The platform provisions the database, API exposure and tenant isolation from those files; your function code only needs two things:

  • a data client to read and write the App's entities (@revenexx/app-sdk)
  • a router to answer the HTTP routes your App exposes (@revenexx/app-sdk/router)
npm install @revenexx/app-sdk

Data client

The client gives every entity the same methods — list, page, get, create, update, delete — and the same query shape, independent of where the data lives. Swap the adapter to move between environments:

| adapter | use it for | infrastructure | |-----------|-------------------------------------------------------|----------------| | mock | local development + unit tests against fixtures | none | | remote | integration checks against a real development tenant | none local | | runtime | production, inside the deployed function | auto-configured |

const { createClient } = require('@revenexx/app-sdk');

const db = createClient({
  adapter: 'mock',                 // 'remote' | 'runtime'
  entities: {
    markets: { table: 'acme__shop__markets', pk: 'id' },
  },
  seed: { markets: [{ id: 'm1', code: 'de', currency: 'EUR' }] },
});

await db.markets.create({ code: 'at', currency: 'EUR' });
const eur = await db.markets.list({ where: { currency: 'EUR' }, order: 'code.asc' });
const page = await db.markets.page({ limit: 20, offset: 0 });   // { items, total }

Most Apps don't hand-write the entities map: the revenexx tooling generates a typed db.generated.js from your App's schema.json + manifest.json, so createDb({ adapter, ... }) is one import away and every entity is typed.

Queries

One query shape works across all adapters:

await db.markets.list({
  where: {
    currency: 'EUR',                                  // equality
    code: ['de', 'at'],                               // IN list
    created_at: { op: 'gte', value: '2026-01-01' },   // explicit operator
  },
  select: ['id', 'code'],
  order: 'created_at.desc',
  limit: 50,
  offset: 0,
});

Operators: eq, neq, gt, gte, lt, lte, like, ilike, in, is.

Permissions

The methods available per entity follow your App manifest's permissions declarations. Calling an operation the manifest doesn't grant throws a descriptive error (and the platform would reject it anyway) — so local tests catch permission gaps before deploy.

Custom adapters

The adapter is a small interface (list/get/create/update/remove, optional page). Register a new backend before constructing the client:

const { registerAdapter, createClient } = require('@revenexx/app-sdk');

registerAdapter('sqlite', (config) => ({ kind: 'sqlite', /* … */ }));
const db = createClient({ adapter: 'sqlite', entities, file: 'dev.db' });

Router

@revenexx/app-sdk/router turns the single function entrypoint into declarative routes. Path templates use the same {param} syntax as your manifest's capability routes.

const { createApp, notFound } = require('@revenexx/app-sdk/router');
const { createDb } = require('./db.generated');

const app = createApp({ name: 'markets' });

app.get('/markets/{id}/context', async (c) => {
  const db = createDb({ adapter: 'runtime', context: c.ctx });
  const market = await db.markets.get(c.params.id);
  if (!market) throw notFound();
  const locales = await db.locales.list({ where: { market_id: market.id } });
  return c.json({ market, locales });
});

module.exports = app.handler();

What you get:

  • Literal-first matching/markets/defaults wins over /markets/{id}, regardless of registration order.
  • A clean per-request contextc.params, c.query, c.body (parsed), c.tenant, c.header(name), c.log(msg), c.json(data, status).
  • Error mapping — throw HttpError / notFound() / badRequest() / forbidden() / conflict() for explicit statuses; database constraint violations get the status they deserve (see below); data-client permission errors become 403; anything else is logged and answered as 500.
  • A health routeGET / answers with the App identity, status and the registered routes (disable with createApp({ health: false })).
  • 404/405 handling — unknown paths list the available routes; known paths with a wrong method answer 405.

CRUD in one line

mountCrud wires an entity to the standard five REST routes with filtering and pagination built in:

mountCrud(app, db.markets, { path: '/markets', columns: ENTITIES.markets.columns });
// GET    /markets            list — ?currency=EUR&limit=50&offset=0&order=code.asc
// POST   /markets            create
// GET    /markets/{id}       read
// PUT    /markets/{id}       update
// DELETE /markets/{id}       delete

Nested resources scope every operation to their parent:

mountCrud(app, db.locales, {
  path: '/markets/{marketId}/locales',
  columns: ENTITIES.locales.columns,
  parent: { param: 'marketId', column: 'market_id' },
});
// lists filter by market_id, creates inject it, and a locale belonging to a
// different market answers 404 — even if the request body claims otherwise.

Options: only: ['list', 'get'] restricts the mounted routes; defaultLimit / maxLimit tune pagination bounds (defaults 50 / 200).

Constraint violations

A database constraint doing its job is not a server fault. The data adapters map PostgREST's error to the status the condition deserves and answer with a stable, generic message — the raw body (which names tables and constraints) is logged through the function's error() and recorded on the trace, never returned:

| condition | SQLSTATE | answer | message | |---|---|---|---| | duplicate value | 23505 / 23P01 | 409 | a record with this value already exists | | referenced parent missing (write) | 23503 | 400 | a referenced record does not exist | | dependent row blocks a delete | 23503 | 409 | this record is still referenced by other records | | required value missing | 23502 | 400 | a required value is missing | | check constraint | 23514 | 400 | a value is not allowed here | | bad uuid/number/timestamp | 22xxx (incl. 22P02) | 400 | a value in the request has the wrong format | | offset past the last row | PGRST103 | 200 | empty page with the requested limit/offset and the true total |

Responses carry a stable code alongside the message ({"error":"…","code":"unique_violation"}) so clients can branch without parsing prose. Anything unmapped keeps PostgREST's status when it is a client error and becomes 500 otherwise — with a generic message either way.

Catching these outside the router: import { isHttpError } from '@revenexx/app-sdk'. Use isHttpError(err), not instanceof — the package's two entrypoints are separate bundles, so each carries its own copy of the class.

App settings

An App declares the knobs a merchant may turn in settings.json; the Cockpit renders them and Console stores the tenant's choices. c.settings() is how the App reads them back at runtime:

const app = createApp({
  name: 'orders',
  settings: { defaults: { auto_reserve: true, reservation_minutes: 30 } },
});

app.post('/orders', async (c) => {
  const s = await c.settings();
  if (s.auto_reserve) await reserve(c.body, s.reservation_minutes);
  // …
});

defaults mirrors the defaults in your settings.json. They type the resolved object and are what the App runs on if the settings service cannot be reached — they never override a value the merchant actually set.

Outside a handler, build the reader yourself. Create it once, at module scope: the cache lives on it.

const { createSettings } = require('@revenexx/app-sdk');
const settings = createSettings({ app: 'orders', defaults: { auto_reserve: true } });

const s = await settings.get(c);              // c = RouteContext, FnContext or { headers }
const full = await settings.load(c);          // { settings, masked, market, resolved }

Resolution

Values are resolved by the gateway (GET /v1/settings/apps/{app}?market=), not here. Per key, highest wins:

| | source | |---|---| | 1 | per-market override — only for settings declared "scope": "market", and only when the request carries a market | | 2 | the stored tenant value | | 3 | the default in settings.json | | 4 | the defaults you passed here — the offline fallback, and only where 1–3 produced nothing |

The market comes off the request's X-Revenexx-Market, so a market-scoped setting resolves for the market the call is for without being asked. Override it per read with c.settings({ market: 'fr' }).

Settings declared sensitive are never returned; their keys are listed in masked on load(), so an App can tell "not configured" from "not for you".

Failing open, and staying cheap

A settings read sits on the hot path of every request, so it must cost nothing and break nothing:

  • Fail open. An unreachable gateway, a timeout (2s), a missing tenant, an App that declares no settings — you get the last values that did resolve, or your declared defaults. settings() never throws. Pass onError (wire it to the function's error()) to keep the fallback visible.
  • Cache. Per (tenant, app, market), 60s by default, with concurrent reads sharing one in-flight request — a warm read is a Map lookup. The gateway keeps its own 30s cache and drops it when values change, so a Cockpit edit takes effect within about a minute. Tune with ttlMs or REVENEXX_SETTINGS_TTL_MS; a failed read is retried after 5s, not 60.

Testing your App

Handlers are plain functions over a context object, so tests don't need any infrastructure: build the app with a mock-adapter client, call app.handler() with a fake { req, res }, and assert on the captured JSON.

const handler = app.handler();
await handler({
  req: { method: 'GET', path: '/markets', headers: {}, query: {} },
  res: { json: (data, status = 200) => ({ data, status }) },
  log: console.log,
});

License

MIT