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

@voilabs/mta

v0.2.0

Published

A gateway-compatible module API SDK for building multi-module tenant services.

Readme

@voilabs/mta

Tiny, type-safe SDK for building voi gateway-compatible module APIs.

It is intentionally service-oriented: one HTTP API can expose many independent modules on the same port. Each module has its own key, route prefix, permissions, layout contract and data contract. Your tables are plain Drizzle pgTables — they type every query and drive the runtime auto-migration.

Install

bun add @voilabs/mta drizzle-orm elysia

Scaffold A New API

voi init commerce-api --modules products,orders

The scaffold includes a Drizzle schema, a module catalog, runtime setup and signed internal routes. Domain routes are ordinary Elysia routes you write yourself.

Minimal Usage

voi.server() returns a fully typed Elysia app with everything wired in — signed system routes, tenant/module context and, for a single-module service, the automatic module guard. Handlers receive a typed tenant, tenantDb (a Drizzle client bound to your schema), module, moduleKey and moduleEnv. No : any, no generated CRUD — you query Drizzle directly.

import { integer, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
import {
  Core,
  createDrizzleTenantDbPool,
  defineCrudModule,
  defineModules,
  defineService
} from "@voilabs/mta";
import { desc } from "drizzle-orm";
import { drizzle } from "drizzle-orm/postgres-js";
import { t } from "elysia";

const service = defineService({
  key: "products-api",
  title: "Products API",
  protocolVersion: 2
});

// The Drizzle table is the single source of truth: it types every query and is
// what the tenant DB pool auto-creates/migrates on first use.
export const products = pgTable("products", {
  id: uuid("id").primaryKey().defaultRandom(),
  name: text("name").notNull(),
  price_cents: integer("price_cents").notNull().default(0),
  created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
});

const schema = { products };

const modules = defineModules([
  defineCrudModule({
    key: "products",
    title: "Products",
    path: "/products",
    tables: [products],
    fields: [
      { name: "name", label: "Name", type: "text", required: true, listVisible: true },
      { name: "price_cents", label: "Price", type: "currency", required: true, listVisible: true }
    ]
  })
]);

const tenantDb = createDrizzleTenantDbPool({ schema, drizzle });

const voi = new Core({
  service,
  modules,
  resolveTenantDb: tenantDb.resolveTenantDb
});

const app = voi.server().group("/products", (group) =>
  group
    .get("/", ({ tenantDb }) =>
      tenantDb.select().from(products).orderBy(desc(products.created_at)))
    .post(
      "/",
      async ({ tenantDb, body, status }) => {
        const [row] = await tenantDb.insert(products).values(body).returning();
        return status(201, row);
      },
      { body: t.Object({ name: t.String({ minLength: 1 }), price_cents: t.Integer() }) }
    )
);

app.listen(4001);

Unsigned direct access returns 404. The gateway discovers modules through a signed internal endpoint:

GET /__voi/manifest

Runtime module keys are pushed through:

POST /__voi/module-key

The first key push uses trust-on-first-use so the module service can boot without any .env secret. After a module key exists, key rotation and tenant requests must be signed with the current per-module key.

Manifest Depth

A module definition can describe:

  • navigation and screens
  • table, detail and form fields
  • permissions and capabilities
  • default limits
  • data tenancy and migrations
  • JSON-schema-like input/output contracts
  • route contracts
  • action placement, confirmation and intent

That makes the gateway/admin UI able to reason about a module without hardcoding its screens.

Helpers

Core is the entry point and voi.server() is the ergonomic way to build the app — a typed Elysia instance with the system routes and tenant/module context already mounted, so handlers get tenant, tenantDb and moduleEnv for free:

const app = voi.server().get("/products/health", ({ tenant, moduleEnv }) => ({
  ok: true,
  tenant: tenant.slug,
  hasRedis: Boolean(moduleEnv.PRODUCTS_REDIS_URL)
}));

createDrizzleTenantDbPool({ schema, drizzle }) is the package-owned tenant pool. It hands each request a PostgresJsDatabase<typeof schema> (a Drizzle client) and runs the runtime auto-migration. Module APIs never define their own getTenantDb. Passing your app's own drizzle import keeps table/query/client types aligned in local linked package setups where TypeScript may otherwise see two Drizzle installations.

voi.requireModule("orders") is a typed beforeHandle guard for multi-module services (see below).

If you need full control over the Elysia chain, the building blocks are also exposed: voi.use mounts the signed system endpoints (e.g. /__voi/manifest), voi.derive resolves tenant/module context, and voi.macro adds { requireModule: "..." } to route groups. server() is use + derive composed. No domain route is generated automatically.

defineCrudModule turns a small field list into a complete manifest — navigation, permissions, capabilities, list/get/create/update/delete route contracts, create/update/output schemas, and table/form/detail views. Pass tables (your Drizzle tables) so the manifest advertises the module's migrations.

Single-Module Services

When a service registers exactly one module, the SDK enforces that module on every route automatically. You never write a requireModule guard — any request whose module context does not match the only module gets 404, while signed /__voi/* system routes stay reachable.

// Single module → just add routes to server(); the guard is implicit.
const app = voi.server().group("/products", (group) =>
  group.get("/", ({ tenantDb }) => tenantDb.select().from(products))
);

Services with two or more modules scope each group to a module with the typed requireModule guard, since the SDK cannot infer which route belongs to which module:

const app = voi
  .server()
  .guard({ beforeHandle: voi.requireModule("products") }, (app) =>
    app.group("/products", (group) =>
      group.get("/", ({ tenantDb }) => tenantDb.select().from(products))))
  .guard({ beforeHandle: voi.requireModule("orders") }, (app) =>
    app.group("/orders", (group) =>
      group.get("/", ({ tenantDb }) => tenantDb.select().from(orders))));

Auto Migrate

The Drizzle schema you pass to createDrizzleTenantDbPool({ schema, drizzle }) is the single source of truth. On the first request per tenant database the pool creates the database if missing and runs idempotent CREATE TABLE / ADD COLUMN / CREATE INDEX statements derived from the table metadata, then hands the handler a typed Drizzle client:

const tenantDb = createDrizzleTenantDbPool({
  schema: { products, orders },
  drizzle,
  autoMigrate: true,   // default; set false to manage migrations yourself
  createDatabase: true // default; set false if the database always exists
});

Module Env

Modules can declare runtime values they need from the host system:

defineCrudModule({
  key: "products",
  title: "Products",
  path: "/products",
  env: {
    variables: [
      {
        key: "PRODUCTS_REDIS_URL",
        label: "Products Redis URL",
        type: "url",
        sensitive: true,
        required: true
      }
    ]
  },
  fields: []
});

The main app/admin can collect those values, store them encrypted, and forward them to the module through signed proxy requests. Route handlers receive them as moduleEnv in the request context.

Multi-Module Example

The package includes a runnable example that serves products and orders from one API process:

bun run examples/multi-module-api.ts

Both modules publish their own manifest layout, route contracts, permissions and schemas through the same signed GET /__voi/manifest endpoint.

Startup Validation

Core validates the manifest at startup and fails fast for common mistakes:

  • duplicate module keys
  • duplicate module paths
  • invalid service/module keys
  • route permissions that do not exist
  • UI views pointing to missing resources
  • duplicate field, resource, route or action keys