Skip to content

Providers ​

Overview ​

A provider is an implementation the framework asks for instead of shipping one. The built-in auth module is one. A payment gateway, an SMS driver or a custom user store can be others.

The rule is simple:

The framework never imports your application code. Your application registers an implementation, the framework consumes it through a contract.

There is no container, no decorators and no auto-wiring. A provider is a plain object registered under a key.

Where providers live ​

LocationScopeImported
src/providers/**The whole applicationAutomatically, at boot
src/modules/<m>/providers.tsOne module, plugged into the appAutomatically, at boot
src/modules/<m>/providers/*.tsOne module, several implementationsAutomatically, at boot

All three are discovered by the framework at startup. Drop a file in and it runs — there is nothing to edit in app.ts, and nothing to import from @/framework/... internals.

The scaffold ships an empty src/providers/ folder with a short README.md next to it, and every new module gets a providers.ts next to its facade.ts.

Generating one ​

bash
npm run maker provider:make payment          # -> src/providers/payment.ts
npm run maker module:make-provider billing stripe  # -> src/modules/billing/providers/stripe.ts

provider:make writes an app-wide provider, module:make-provider writes one inside a module. Both files register themselves when they are imported. See Provider Commands.

Registering an implementation ​

Every provider is registered under a key:

ts
// src/providers/payment.ts
import { registerProvider } from "@/framework/facade.js";

registerProvider("payment", {
  async charge(order) {
    return { id: "ch_123", status: "paid" };
  }
});

And read back the same way:

ts
import { getProvider } from "@/framework/facade.js";

const payment = getProvider<any>("payment");

registerProvider() returns the implementation, so it can be inlined:

ts
export const payment = registerProvider("payment", {
  async charge(order) {
    return { id: "ch_123", status: "paid" };
  }
});

The built-in auth contract ​

auth is the key nexwire ships with. Its contract is AuthProvider:

ts
// src/providers/auth.ts
import { registerAuthProvider } from "@/framework/facade.js";
import type { AuthProvider } from "@/framework/facade.js";

const authProvider: AuthProvider = {
  async getUserById(id) {
    return { id, email: "user@example.com", tenantId: 42, plan: "pro" };
  },
  async findRefreshToken(jti) {
    return { id: 1, jti, revoked: false, expiresAt: new Date() };
  },
  async revokeRefreshToken(tokenId) {},
  async getAuthContext(user, payload) {
    return { id: user.id, email: user.email, tenantId: user.tenantId };
  },
  async getUserContact(userId) {
    return { email: "user@example.com", name: "User" };
  }
};

registerAuthProvider(authProvider);
MethodRequiredPurpose
getUserById(id)YesLoads a user. id is `number
findRefreshToken(jti)YesLoads a stored refresh token by its JWT id.
revokeRefreshToken(id)NoRevokes a refresh token. deleteRefreshToken(id) also works.
getAuthContext(user, payload)NoShapes the object handlers receive as auth.
getUserContact(userId)NoFinds the email/name of a user — used by notify(). Accepts `number

Every auth id in the contract is number | string so a provider backed by UUIDs is a first-class citizen.

When no auth provider is registered, the framework falls back to the built-in one from src/modules/auth. Delete that module and register your own to take over completely — see Authentication.

The notification contract ​

notification is the storage contract notify() and /api/notification/* read through. Notifications are opt-in: run module:make-notification notification to generate an implementation at src/modules/notification/providers/notification.ts, or register your own under this key to replace storage — a different table, a remote service, a test double.

ts
// src/providers/notification.ts
import { registerNotificationProvider } from "@/framework/facade.js";
import type { NotificationProvider } from "@/framework/facade.js";

const notificationProvider: NotificationProvider = {
  async create(target, input) {
    // persist and return a NotificationRecord
  },
  async list(target, options) {},
  async unreadCount(target) {},
  async markRead(target, id) {},
  async markAllRead(target) {},
  async remove(target, id) {},
  async deleteForTarget(target) {} // optional bulk cleanup
};

registerNotificationProvider(notificationProvider);
MethodRequiredPurpose
create(target, input)YesStores one notification and returns it.
list(target, options)YesReturns { data, total, unread, page, perPage }.
unreadCount(target)YesReturns the unread count.
markRead(target, id)YesMarks one notification read; returns whether it existed.
markAllRead(target)YesMarks every unread notification read; returns how many changed.
remove(target, id)YesDeletes one notification; returns whether it existed.
deleteForTarget(target)NoRemoves everything owned by a target (useful when the owner is deleted).

A target is { type?: string; id: number | string }; type defaults to "user" and the id is always passed through as-is, so numeric auth ids and UUIDs both work.

Mail and realtime stay in the runtime: a custom provider only replaces storage, and notify({ broadcast, mail }) keeps working. Without the generated module and without a registered provider, notify() warns once and returns null, and the HTTP handlers return 503.

Module providers ​

A module can plug itself into the application the same way:

ts
// src/modules/billing/providers.ts
import { registerProvider } from "@/framework/facade.js";

registerProvider("billing", {
  async invoice(order) {
    return { id: "inv_1" };
  }
});

When a module offers more than one implementation, they live in its providers/ folder — those files are imported at boot as well:

ts
// src/modules/billing/providers/stripe.ts
import { registerProvider } from "@/framework/facade.js";

registerProvider("stripe", {
  async charge(order) {
    return { id: "ch_123" };
  }
});

The module ships the implementation, the application decides to use it. Nothing in src/modules/billing/ has to know whether the app consumed it.

Module-to-module communication ​

Modules do not import each other. There are three ways to talk:

NeedMechanism
Call something another module ownsThat module's facade.ts
Notify another module something happeneddispatchEvent / dispatchCommand
Swap an implementationA provider

A module's facade.ts is its public API — other modules import from it, never from its controllers or services:

ts
// src/modules/billing/facade.ts
export { createInvoice } from "./services/invoice.service.js";
ts
// src/modules/orders/controllers/order.controller.ts
import { createInvoice } from "@/modules/billing/facade.js";

When one module only needs to react to another, use an event rather than an import — the two modules then stay independent:

ts
// src/modules/orders/controllers/order.controller.ts
import { dispatchEvent } from "@/framework/facade.js";

await dispatchEvent("order.created", { orderId });
ts
// src/modules/billing/providers.ts
import { registerProvider } from "@/framework/facade.js";
import { dispatchEvent } from "@/framework/facade.js";

registerProvider("billing", {
  async onOrderCreated(payload) {
    // react to the event
  }
});

API ​

FunctionPurpose
registerProvider(key, impl)Registers an implementation under a key and returns it.
getProvider(key)Returns the implementation, or null.
hasProvider(key)Reports whether an implementation is registered.
clearProvider(key)Removes it — mainly useful in tests.
listProviders()Lists every registered key.
registerAuthProvider(impl)Typed shorthand for the auth key.
getAuthProvider()The registered auth provider, or null.
hasAuthProvider()Reports whether an auth provider is registered.
clearAuthProvider()Removes it — mainly useful in tests.
registerNotificationProvider(impl)Typed shorthand for the notification key.
getNotificationProvider()The registered notification provider, or null.
hasNotificationProvider()Reports whether a notification provider is registered.
clearNotificationProvider()Removes it — mainly useful in tests.
resolveAuthProvider()The registered auth provider, or the built-in one, or null.
resolveNotificationProvider()The registered notification provider, or null.
loadProviders()Imports the provider files. Called by the framework at boot.

All of them are imported from the facade:

ts
import {
  registerProvider,
  getProvider,
  hasProvider,
  clearProvider,
  listProviders,
  registerAuthProvider,
  getAuthProvider,
  hasAuthProvider,
  clearAuthProvider,
  registerNotificationProvider,
  getNotificationProvider,
  hasNotificationProvider,
  clearNotificationProvider
} from "@/framework/facade.js";

Notes ​

  • Provider files are imported once per process. A file that throws is logged and skipped — it does not take the application down.
  • Registration happens before routes are mounted, so a provider is available to route guards, middleware and seeders.
  • The framework never imports @/framework/... internals into your code, and your code never needs to import framework internals either: the facade is the public API.

Released under the MIT License.