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

@xenterprises/fastify-xadmin

v1.1.0

Published

Fastify plugin for admin dashboard APIs - tenants, roles, users, audit logs, impersonation, and Stripe management.

Readme

xAdmin

Fastify v5 plugin that mounts a complete admin-dashboard API — tenant management, roles/permissions (RBAC), user administration, audit logging, impersonation, and Stripe billing management — for SaaS applications that already use Prisma and an auth plugin. Built as the backend for the @xenterprises/nuxt-x-app-admin Nuxt layer, but usable by any admin frontend.

Install

npm install @xenterprises/fastify-xadmin fastify@5 @prisma/client

fastify (^5) and @prisma/client (^5 || ^6 || ^7) are peer dependencies.

Minimal example

import Fastify from "fastify";
import xConfig from "@xenterprises/fastify-xconfig"; // provides fastify.prisma
import xAuthBetter from "@xenterprises/fastify-xauth-better"; // provides fastify.authenticate
import xAdmin from "@xenterprises/fastify-xadmin";

const app = Fastify({ logger: true });

await app.register(xConfig, { /* ... */ });
await app.register(xAuthBetter, { /* ... */ });
await app.register(xAdmin); // all defaults: routes at /api/admin

await app.listen({ port: 3000 });

xAdmin does not read process.env — the consumer owns env access and passes values to the plugins that need them.

Options

| Option | Type | Default | Required | Description | |--------|------|---------|----------|-------------| | prefix | string | "/api/admin" | No | Route prefix for all admin endpoints. Must start with /. | | permissions | array | [] | No | Custom permissions to add. Each entry needs a string id like "resource:action" (plus resource, action, optional category). | | skipAuth | boolean | false | No | Skip authentication/permission checks. Development only. | | stripe.enabled | boolean | auto-detected from the fastify.stripe decorator | No | Enable Stripe routes. Defaults to true when a fastify.stripe decorator is registered, false otherwise. Explicit true without the decorator logs a warning and the routes return 503. | | cache.health | number | 10 | No | Cache TTL (seconds) for GET /dashboard/health. | | cache.metrics | number | 60 | No | Cache TTL (seconds) for GET /dashboard/metrics and GET /dashboard/errors. | | cache.activeUsers | number | 30 | No | Cache TTL (seconds) for GET /dashboard/active-users. | | cache.permissions | number | 300 | No | Permission cache TTL (seconds). Reserved — permission checks are in-memory config lookups and are not cached. |

Registration fails fast with an actionable error when an option has the wrong shape, when fastify.prisma is missing, or when fastify.authenticate is missing and skipAuth is not true.

Dashboard read caching

Dashboard aggregation reads (/dashboard/health, /dashboard/metrics, /dashboard/active-users, /dashboard/errors) are cached in memory with the TTLs above. The cache is per-process (one instance per Fastify application, created at registration) with no external dependencies; entries expire lazily on read. Successful user and tenant writes (create/update/delete/ suspend/activate) invalidate the affected keys (metrics, activeUsers), so reads never serve stale counts after a mutation. The cache is not shared between processes — with multiple app replicas each has its own copy.

Decorators

All API is exposed under a single namespace, fastify.xAdmin:

| Member | Signature | Description | |--------|-----------|-------------| | config | object | Resolved plugin configuration (prefix, permissions, skipAuth, stripe, cache). | | prefix | string | Resolved route prefix. | | cache | object | Resolved cache TTLs. | | skipAuth | boolean | Whether auth checks are skipped. | | stripeEnabled | boolean | true only when Stripe routes are enabled AND fastify.stripe exists. | | permissions.all | array | All permissions (defaults + custom). | | permissions.map | Map | Permission id → permission object. | | permissions.has(userPermissions, required) | → boolean | Permission check; admin:* grants all, <resource>:manage implies other actions on that resource. | | permissions.get(id) | → object \| undefined | Look up a permission by id. | | permissions.byCategory() | → object | Permissions grouped by category. | | permissions.requirePermission(id) | → preHandler | Same as requirePermission below. | | requirePermission(id) | → preHandler | Route preHandler factory; reads request.user.permissions and throws 403 when the permission is missing. No-op when skipAuth is true. | | audit.log(request, entry) | → Promise<void> | Write an audit entry: { action, resource, resourceId?, description?, metadata? }. Failures are logged (message only) and swallowed. |

Named exports are also available for direct use: DEFAULT_PERMISSIONS, hasPermission, AUDIT_ACTIONS, pagination helpers (parsePaginationQuery, buildPaginationMeta, buildOrderBy), status-coded error helpers (httpError, badRequest, unauthorized, forbidden, notFound, conflict, serviceUnavailable), the cache helper (createCache, CACHE_KEYS), and the impersonation/health/metrics service functions.

Permission checking in your own routes

app.get("/custom", {
  preHandler: [app.xAdmin.requirePermission("custom:read")],
}, async (request) => {
  // only users with 'custom:read' reach here
});

Audit logging from your own routes

await app.xAdmin.audit.log(request, {
  action: "create",
  resource: "orders",
  resourceId: order.id,
  description: "Created order #1234",
  metadata: { amount: 100 },
});

Routes

All routes are mounted under prefix (default /api/admin) and require the listed permission unless skipAuth is true. authenticate runs first on every route. Every route validates its body, params, and querystring with JSON Schema (querystring values are type-coerced, so ?limit=10 arrives as a number and ?hardDelete=false as a boolean); invalid input is rejected with a 400 before the handler runs.

Dashboard (4)

| Method | Path | Permission | Description | |--------|------|------------|-------------| | GET | /dashboard/health | dashboard:read | System health (database, Stripe) | | GET | /dashboard/metrics | dashboard:read | Platform metrics | | GET | /dashboard/active-users | dashboard:read | Active user count | | GET | /dashboard/errors | dashboard:read | Recent errors (requires ErrorLog model) |

Tenants (9)

| Method | Path | Permission | Description | |--------|------|------------|-------------| | GET | /tenants | tenants:read | List tenants (paginated) | | POST | /tenants | tenants:create | Create tenant | | GET | /tenants/:id | tenants:read | Get tenant | | PUT | /tenants/:id | tenants:update | Update tenant | | DELETE | /tenants/:id | tenants:delete | Delete tenant | | POST | /tenants/:id/suspend | tenants:manage | Suspend tenant | | POST | /tenants/:id/activate | tenants:manage | Activate tenant | | PUT | /tenants/:id/plan | tenants:manage | Change plan | | PUT | /tenants/:id/settings | tenants:manage | Update settings |

Roles (9)

| Method | Path | Permission | Description | |--------|------|------------|-------------| | GET | /roles | roles:read | List roles | | POST | /roles | roles:create | Create role | | GET | /roles/permissions | roles:read | List all permissions | | GET | /roles/:id | roles:read | Get role | | PUT | /roles/:id | roles:update | Update role | | DELETE | /roles/:id | roles:delete | Delete role (409 while users assigned) | | PUT | /roles/:id/permissions | roles:manage | Update role permissions (400 on unknown ids) | | POST | /roles/:id/assign | roles:manage | Assign role to user | | POST | /roles/:id/revoke | roles:manage | Revoke role from user |

Users (9)

| Method | Path | Permission | Description | |--------|------|------------|-------------| | GET | /users | users:read | List users (paginated) | | POST | /users | users:create | Create user (409 on duplicate email) | | GET | /users/:id | users:read | Get user | | PUT | /users/:id | users:update | Update user | | DELETE | /users/:id | users:delete | Soft delete by default; ?hardDelete=true hard-deletes (any other value, including false, soft-deletes) | | PUT | /users/:id/role | users:manage | Change role | | POST | /users/:id/suspend | users:manage | Suspend user | | POST | /users/:id/unsuspend | users:manage | Unsuspend user | | POST | /users/:id/reset-password | users:manage | Trigger password reset |

Stripe (10)

Requires the fastify.stripe decorator (from @xenterprises/fastify-xstripe); returns 503 otherwise. The Stripe client is created once by xStripe and reused — xAdmin never creates Stripe clients.

| Method | Path | Permission | Description | |--------|------|------------|-------------| | GET | /stripe/customers | billing:read | List customers | | GET | /stripe/customers/:id | billing:read | Get customer | | POST | /stripe/customers/:id/sync | billing:manage | Sync from Stripe | | GET | /stripe/subscriptions | billing:read | List subscriptions | | POST | /stripe/subscriptions/:id/cancel | billing:manage | Cancel subscription | | GET | /stripe/invoices | invoices:read | List invoices | | POST | /stripe/invoices/:id/refund | invoices:refund | Refund invoice | | POST | /stripe/invoices/:id/void | billing:manage | Void invoice | | GET | /stripe/payment-methods | billing:read | List payment methods | | GET | /stripe/webhook-logs | billing:read | List webhook events (requires StripeWebhookLog model) |

Audit log (2)

| Method | Path | Permission | Description | |--------|------|------------|-------------| | GET | /audit-log | audit:read | List entries (paginated, filterable) | | POST | /audit-log/export | audit:export | Request an export |

Impersonation (3)

| Method | Path | Permission | Description | |--------|------|------------|-------------| | POST | /impersonation/start | impersonation:use | Start impersonating a user (403 for admin targets, 401 without an authenticated admin) | | POST | /impersonation/stop | impersonation:use | Stop impersonating (token via x-impersonation-token header or body) | | GET | /impersonation/status | impersonation:use | Current session status |

Error behavior

  • Registration throws Errors naming the plugin, the option/decorator, and an example, e.g. xadmin: option `skipAuth` must be a boolean, e.g. `app.register(xAdmin, { skipAuth: true })` .
  • Routes throw status-coded errors handled by Fastify's error machinery: 400 (bad input — including JSON Schema validation failures on body, params, or querystring), 401 (unauthenticated), 403 (missing permission / forbidden target), 404 (missing resource), 409 (conflict), 503 (Stripe not configured). Responses are Fastify's standard { statusCode, error, message }.
  • Errors from Prisma or Stripe propagate as 500s. Audit-log write failures are caught and logged (error message only, never raw SDK error objects) so they never break the operation being audited.
  • No secrets are logged: no API keys, tokens, passwords, or Authorization headers ever reach the logs.

Database schema

See docs/prisma-schema.prisma for the required models: Tenant, Role, UserRole, AuditLog, ImpersonationSession, StripeWebhookLog. ErrorLog is optional (dashboard /errors returns [] without it), as are StripeWebhookLog and the UserRole join table (routes degrade gracefully).

Default permissions

| Category | Permissions | |----------|-------------| | User Management | users:create, users:read, users:update, users:delete, users:manage | | Tenant Management | tenants:create, tenants:read, tenants:update, tenants:delete, tenants:manage | | Role Management | roles:create, roles:read, roles:update, roles:delete, roles:manage | | Billing | billing:read, billing:manage, invoices:read, invoices:refund | | Audit | audit:read, audit:export | | Security | impersonation:use | | Dashboard | dashboard:read | | Super Admin | admin:* (grants all permissions) |

Requirements

  • Node.js >= 20
  • Fastify ^5
  • Prisma (@prisma/client ^5 || ^6 || ^7) via a fastify.prisma decorator (e.g. xConfig)
  • An auth plugin providing fastify.authenticate (e.g. xAuthBetter), unless skipAuth: true
  • Optional: @xenterprises/fastify-xstripe for the Stripe routes

License

Proprietary — All Rights Reserved X Enterprises. See LICENSE.