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.7.2

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 requirements

  • Bun 1.3+ or Node 24+ — vex-core/http uses the Web platform's URLPattern global for route matching. Older runtimes need a polyfill (urlpattern-polyfill works).

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);
});

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/Node built-ins.

import {
  createRouter,
  vexHandler,
  cors,
  bearerAuth,
  accessLog,
  requestId,
  errorBoundary,
  staticFiles,
  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))
  .use(staticFiles({ dir: "./dist/ui" }));

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 }) | 429 + Retry-After + X-RateLimit-*. | | staticFiles({ dir, index, spaFallback, immutablePrefix }) | Serve built assets. Fallback-on-404 semantics: explicit routes win, static serves what's left. SPA-friendly. | | sessions({ storage, cookieName, maxAge, rolling }) | Server-side session store on any StorageAdapter (SQLite/custom). ctx.session.get/set/delete/destroy. |

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");
   });

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.

Spans are written to the internal _spans table. Core does not prune it — retention policy is app-specific. For a long-running server, register a cron that deletes old rows:

api.registerJob("spanRetention", {
  schedule: "every 1h",
  async handler(ctx) {
    const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000; // 7 days
    await ctx.db.table("_spans").where("startTime", "<", cutoff).delete();
  },
});

Without this, _spans grows unbounded.

Exports

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

License

MIT