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

vex-core

v0.11.0

Published

Local-first reactive backend engine. Plugin architecture, SQLite storage, real-time subscriptions.

Readme

vex-core

Local-first reactive backend engine. Plugin architecture, SQLite storage, real-time subscriptions via SSE.

npm install vex-core

Runtime requirement: Bun 1.3+

vex-core intentionally targets Bun. Its SQLite adapter, reactive hashing, and WebSocket server use Bun runtime APIs.

Peer: react ^19 (only if you use vex-core/client).

Quick start

import { Vex, sqliteAdapter } from "vex-core";

const vex = await Vex.create({
  storage: sqliteAdapter(".vex/data.db"),
  plugins: [
    (api) => {
      api.setName("counter");
      api.registerTable("counters", {
        columns: { key: { type: "string" }, value: { type: "number" } },
      });
      api.registerMutation("set", {
        args: { key: "string", value: "number" },
        async handler(ctx, args) {
          return ctx.db.table("counters").insert(args);
        },
      });
      api.registerQuery("list", {
        args: {},
        async handler(ctx) {
          return ctx.db.table("counters").all();
        },
      });
    },
  ],
});

await vex.mutate("counter.set", { key: "hits", value: 1 });
await vex.query("counter.list");

const unsub = await vex.subscribe("counter.list", {}, (rows) => {
  console.log("changed:", rows);
});

Reactive safety

Explicit query() calls may return large results. subscribe() calls may not: reactive queries are re-run after writes, hashed, and delivered to callbacks, so Vex enforces row and byte budgets before accepting or continuing a subscription.

const vex = await Vex.create({
  storage,
  plugins,
  reactive: { maxRows: 500, maxBytes: 512 * 1024 },
});

api.registerQuery("list", {
  args: {},
  reactive: { maxRows: 100, maxBytes: 64 * 1024 },
  handler: (ctx) => ctx.db.table("items").limit(100).all(),
});

api.registerQuery("export", {
  args: {},
  reactive: false,
  handler: (ctx) => ctx.db.table("items").all(), // query() only, not subscribe()
});

Reactive metadata is recorded on spans: resultRows, resultBytes, reactive, budget, and budgetExceeded. Query-builder reads record dependency descriptors for table, filters, selected columns, ordering, and limits. Raw SQL is tracked conservatively by parsed FROM/JOIN tables; external API work and unbounded exports should stay outside reactive queries or be marked reactive: false.

Query builder

Reads, filters, aggregations — pushed down to SQL.

ctx.db.table("orders").where("region", "=", "US").all();
ctx.db.table("orders").where("amount", ">", 100).select("id", "amount").first();

ctx.db.table("orders").count();
ctx.db.table("orders").sum("amount");
ctx.db.table("orders").avg("amount");

ctx.db.table("orders")
  .groupBy(["region", "product"], {
    revenue: ["sum", "amount"],
    count: "count",
  })
  .having("count", ">=", 10)
  .order("revenue", "desc")
  .limit(20);

File-based conventions

Flat files, no config. Use scanDirectory to turn a folder into plugins.

// schema.ts
import { table } from "vex-core/framework";
export const todos = table({ text: "string", done: "boolean" });

// todos.ts
import { query, mutation } from "vex-core/framework";
export const list = query({}, async (ctx) => ctx.db.table("todos").all());
export const add = mutation({ text: "string" }, async (ctx, args) =>
  ctx.db.table("todos").insert({ text: args.text, done: false }),
);
import { scanDirectory } from "vex-core/framework";
import { Vex, sqliteAdapter } from "vex-core";

const { plugins } = await scanDirectory("./app");
const vex = await Vex.create({
  storage: sqliteAdapter(".vex/data.db"),
  plugins,
});

Also supported in files: webhook(path, handler), job(schedule, handler), middleware(fn).

HTTP

vex-core/http is a small, composable HTTP toolkit built around the Fetch API. A Router matches methods and paths (via URLPattern), middleware wraps the chain in classic onion order, handlers return Response (or undefined to fall through). Inspired by Vert.x Web and Koa; no dependencies beyond Bun's built-ins.

import {
  createRouter,
  vexHandler,
  cors,
  bearerAuth,
  accessLog,
  requestId,
  errorBoundary,
  serveDir,
  serveFile,
  sessions,
  HttpError,
} from "vex-core/http";

const app = createRouter()
  .use(errorBoundary())
  .use(requestId())
  .use(accessLog())
  .use(cors({ origin: "*" }))
  .use(bearerAuth({ token: process.env.VEX_TOKEN! }))
  .mount("/vex", vexHandler(vex))
  .get("/assets/*", serveDir({ dir: "./dist/ui/assets", stripPrefix: "/assets" }))
  .get("/*", serveFile("./dist/ui/index.html"));

Bun.serve({ port: 3000, fetch: (req) => app.handle(req) });

Router

createRouter({ prefix?: "/api" })
  .use(mw)                                   // global middleware
  .get("/users/:id", handler)                // also post/put/patch/delete/options/head/all
  .get("/users/:id", mw1, mw2, finalHandler) // handler chain — `undefined` = fall through
  .mount("/vex", subRouter)                  // strip prefix, dispatch to inner router
  .onError(handler)                          // catchall
  .onError((err) => err instanceof HttpError && err.status === 404, render404);

Path syntax is whatever URLPattern supports: :param, *, regex groups. Captures land in ctx.params.

Context

Every middleware and handler receives a RequestCtx:

interface RequestCtx {
  req: Request;
  url: URL;
  params: Record<string, string>;
  state: Record<string, unknown>;   // scratchpad: request id, parsed body, logger bindings
  signal: AbortSignal;
  user?: VexUser | null;             // set by auth middleware
  session?: Session;                 // set by sessions middleware
  span?: Span;                       // set by tracer middleware
}

HttpError

throw HttpError.badRequest("Missing id");
throw HttpError.unauthorized();
throw HttpError.notFound();
throw HttpError.tooManyRequests(30);           // Retry-After: 30
throw new HttpError(418, "I'm a teapot", { body: { code: "TEAPOT" } });

The router catches any thrown error, renders HttpError via .toResponse(), wraps everything else as a 500.

Vex dispatcher

vexHandler(vex, opts?)   // a Router; mount anywhere

Exposes, relative to wherever it's mounted:

POST /query              { name, args? }
POST /mutate             { name, args? }
GET  /subscribe          ?name=...&args=... (SSE)
ALL  /webhook/*          user-defined webhooks

Middleware catalog

| Middleware | What it does | |---------------|--------------| | errorBoundary({ devStackTraces, logger }) | Catches thrown errors, renders HttpError, wraps everything else as 500. | | requestId({ header, generator }) | Read or mint X-Request-Id; stash on ctx.state.requestId; echo on response. | | accessLog({ logger, skipPaths }) | One line per request: GET /path -> 200 (12ms). | | cors({ origin, credentials, allowedHeaders, maxAge }) | Fetch-standard CORS. Downgrades * to request origin when credentials: true. | | bearerAuth({ token, publicPaths, loginPage }) | Single-token HTTP gate: Authorization: Bearer OR session cookie. Built-in /login + /logout; per-IP rate limit on failures. | | bodyParser({ limit, json, urlencoded, text }) | Parse request body into ctx.state.body. | | rateLimit({ requests, window, key, cost, limiter }) | Token-bucket limiting: 429 + Retry-After, full X-RateLimit-* on every response. Works as use() middleware or as a route-chain gate. | | sessions({ storage, cookieName, maxAge, rolling }) | Server-side session store on any StorageAdapter (SQLite/custom). ctx.session.get/set/delete/destroy. |

Static serving — namespace owners, not fallbacks

serveDir and serveFile are plain route handlers. Give each URL namespace exactly one owner and every request has one deterministic path through the router — no Accept sniffing, no extension heuristics, no 404-then-retry:

import { serveDir, serveFile } from "vex-core/http";

app
  // Asset namespace: bytes or 404 — a missing script can never
  // receive the HTML shell.
  .get("/assets/*", serveDir({
    dir: "./dist/ui/assets",
    stripPrefix: "/assets",
    cacheControl: "public, max-age=31536000, immutable",
  }))
  .get("/favicon.ico", serveFile("./dist/ui/favicon.ico"))
  // Document namespace: the terminal GET route owns every
  // client-routed URL — dotted or not — and serves the app shell.
  .get("/*", serveFile("./dist/ui/index.html"));

Both resolve paths inside their directory (traversal-guarded), infer content type by extension, support HEAD, and default to no-cache.

Sessions

Backed by any vex-core StorageAdapter. One row per session, cookie holds the id, data is a JSON blob with sliding expiry.

import { sessions } from "vex-core/http";
import { sqliteAdapter } from "vex-core";

const store = sqliteAdapter("./sessions.db");

app.use(sessions({ storage: store, maxAge: 86400 * 7 }))
   .post("/login", async (ctx) => {
     const { email } = (await ctx.req.json()) as { email: string };
     ctx.session?.set("email", email);
     return new Response("ok");
   })
   .get("/me", (ctx) => Response.json({ email: ctx.session?.get("email") ?? null }))
   .post("/logout", async (ctx) => {
     await ctx.session?.destroy();
     return new Response("ok");
   });

Rate limiting

RateLimiter is the core primitive — a token bucket (capacity requests, refilling at requests / window per second), keyed by caller identity, with cost-weighted consumption for expensive operations. One instance = one scope + one budget; share the instance to share the budget.

import { RateLimiter, RateLimitExceededError } from "vex-core";

const runs = new RateLimiter({
  limit: { requests: 30, window: 60 },
  scope: "agent-runs",
  onDecision: (d) => { if (!d.allowed) log.warn("rate limited", d); },
});

runs.tryConsume(userId);        // → { allowed, retryAfter, remaining, … }
runs.consume(userId, 5);        // throws RateLimitExceededError on reject
runs.peek(userId);              // report without spending budget
runs.stats();                   // { allowed, rejected, keys }

State is in-memory and bounded (prune() + a maxKeys cap with LRU-ish eviction). Per-key limits bound per-key abuse, not aggregate volume — cap aggregate volume with a second limiter on a constant key.

Over HTTP, the rateLimit middleware wraps the same primitive:

import { rateLimit } from "vex-core/http";

app.use(rateLimit({ requests: 100, window: 60 }));            // per-IP, all routes

const shared = new RateLimiter({ limit: { requests: 10, window: 60 }, scope: "expensive" });
app.get("/report", rateLimit({ limiter: shared, cost: () => 5 }), reportHandler)
   .get("/export", rateLimit({ limiter: shared, cost: () => 10 }), exportHandler);

Every response carries X-RateLimit-Limit / -Remaining / -Reset; rejections return the 429 body { error: "too_many_requests", retryAfter, resource } and record an errored rateLimit child span when tracing is composed upstream.

React client

import { VexProvider, useQuery, useMutation } from "vex-core/client";

function App() {
  return (
    <VexProvider basePath="/vex">
      <Todos />
    </VexProvider>
  );
}

function Todos() {
  const { data: todos, isLoading } = useQuery("todos.list");
  const { mutate: addTodo } = useMutation("todos.add");
  // ...
}

SSE-backed — data updates automatically on mutations that touch the underlying tables.

Observability

Every engine operation is a span. Pass a tracer to Vex.create to capture queries, mutations, handler timings, tables touched, invalidated subscriptions, and errors. See src/core/tracer.ts for the Tracer and Span interfaces.

A tracer can make one head-sampling decision for each root. Returning false prevents the entire trace from recording and skips result-size instrumentation for one-shot queries:

const tracer: Tracer = {
  shouldRecord: ({ traceId }) => sample(traceId),
  onSpan: (span) => persist(span),
};

Core metadata describes query and write structure without recording filter values, row values, handler arguments, or results. Core emits spans to the tracer but does not persist them automatically; it reserves the internal _spans table for apps that choose SQLite persistence. Retention remains app-specific. For a long-running server that writes _spans, register bounded cleanup:

api.registerJob("spanRetention", {
  schedule: "every 1h",
  async handler(ctx) {
    const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000; // 7 days
    await ctx.db.sql(
      `DELETE FROM _spans WHERE _id IN (
         SELECT _id FROM _spans WHERE startTime < ? LIMIT 1000
       )`,
      cutoff,
    );
  },
});

Age retention alone is not a hard volume bound. Production sinks should also cap queued spans, individual span size, and total retained rows or bytes.

Exports

| Entry | Contents | |-------|----------| | vex-core | Vex, sqliteAdapter, id, core types, tracer, RateLimiter | | vex-core/framework | table, query, mutation, webhook, job, middleware, scanDirectory | | vex-core/http | Router, createRouter, HttpError, compose, vexHandler, static handlers (serveDir, serveFile), plus middleware (cors, bearerAuth, bodyParser, accessLog, requestId, rateLimit, errorBoundary, sessions) | | vex-core/client | VexProvider, useQuery, useMutation | | vex-core/adapters/sqlite | sqliteAdapter |

License

MIT