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

@qwickapps/server

v1.9.0

Published

Plugin-based application server framework for building websites, APIs, admin dashboards, and full-stack products

Readme

@qwickapps/server

A flexible, pluggable control panel framework for QwickApps services. Provides a web-based dashboard with authentication, health monitoring, and extensible plugin architecture.

Features

  • Gateway Pattern: Production-ready gateway that proxies API requests while keeping control panel always responsive
  • Configurable Mount Paths: Control panel mounts at /cpanel by default, reserving root for frontend apps
  • Route Guards: Unified authentication system supporting Basic Auth, Supabase OAuth, and Auth0
  • Plugin System: Extensible architecture for adding custom functionality
  • Health Monitoring: Built-in health check management with customizable checks
  • Frontend App Support: Handle root path with redirect, static files, or landing page
  • Theming: Customizable branding and styling

What's New in v1.9.0

  • Custom Auth Path: Auth plugin now supports SUPERTOKENS_API_BASE_PATH for custom auth endpoint routing
  • Dashboard Fix: Fixed "Unable to load auth status" on the control panel dashboard
  • Seed Reliability: Fixed seed execution failures in Docker production and stuck UI state after errors

See CHANGELOG.md for full release history.

What's New in v1.7.0

  • Tenants Plugin - Complete multi-tenancy system with data isolation and organization management
    • Support for multiple tenant types: user (single-user), organization, group, department
    • PostgreSQL store with auto-created tables and foreign key CASCADE delete
    • Global roles: owner, admin, member, viewer
    • REST API with 10 endpoints for CRUD, search, and membership management
    • UI Components: Full tenant management page with create/edit/search/filter capabilities
    • Member management: Add/remove members, change roles, invite via email
    • Statistics dashboard showing total tenants by type
  • Plugin Composition - Auto-tenant creation on user signup
    • Users plugin detects tenants plugin and auto-creates personal tenant for new users
    • Graceful degradation: Works independently or together
    • Loose coupling: No forced dependencies between plugins
  • Comprehensive Testing - 60 unit tests, 11 integration tests, 20 E2E tests for tenants plugin

See CHANGELOG.md for full release history.

Installation

pnpm add @qwickapps/server

Quick Start

import { createControlPanel, createHealthPlugin } from '@qwickapps/server';

const controlPanel = createControlPanel({
  config: {
    productName: 'My Service',
    port: 3101,
    version: '1.0.0',
    mountPath: '/cpanel',
    guard: {
      type: 'basic',
      username: 'admin',
      password: process.env.ADMIN_PASSWORD!,
      realm: 'My Service Control Panel',
      excludePaths: ['/api/health'],
    },
  },
  plugins: [
    createHealthPlugin({
      checks: [
        {
          name: 'database',
          type: 'http',
          url: 'http://localhost:5432/health',
          interval: 10000,
        },
      ],
    }),
  ],
});

await controlPanel.start();
console.log(`Control panel running at http://localhost:3101/cpanel`);

Gateway Pattern

For production deployments, use createGateway to run a gateway that:

  1. Serves the control panel UI (always responsive, even if the API crashes)
  2. Proxies API requests to an internal service
  3. Handles graceful error responses when the internal service is down
  4. Supports maintenance mode with customizable status pages
  5. Shows service unavailable pages when mounted apps are unreachable
import { createGateway, createHealthPlugin } from '@qwickapps/server';
import { createApp } from './app.js';

const gateway = createGateway(
  {
    productName: 'My Service',
    gatewayPort: 3101,  // Public port (control panel)
    servicePort: 3100,  // Internal port (API)
    controlPanelPath: '/cpanel',
    controlPanelGuard: {
      type: 'basic',
      username: 'admin',
      password: process.env.ADMIN_PASSWORD!,
      realm: 'My Service Control Panel',
      excludePaths: ['/health', '/api/v1'],
    },
    proxyPaths: ['/api/v1'],
    plugins: [
      createHealthPlugin({
        checks: [
          { name: 'api', type: 'http', url: 'http://localhost:3100/health' },
        ],
      }),
    ],
  },
  // Service factory - creates the internal API service
  async (port) => {
    const app = createApp();
    const server = app.listen(port);
    return {
      app,
      server,
      shutdown: async () => { server.close(); },
    };
  }
);

await gateway.start();

Gateway Architecture

Internet → Gateway (3101, public) → API Service (3100, internal)
                ↓
         Control Panel UI (/cpanel)
         /cpanel/api/health
         /cpanel/api/diagnostics

The gateway is always responsive even if the internal API service crashes, allowing you to view diagnostics and error information.

Mounted Apps with Maintenance Mode

Mount frontend apps or proxy services with full maintenance and fallback support:

const gateway = createGateway({
  // ... base config
  mountedApps: [
    {
      path: '/app',
      name: 'Main App',
      type: 'proxy',
      target: 'http://localhost:4000',
      maintenance: {
        enabled: false,  // Toggle to enable maintenance mode
        title: 'Scheduled Maintenance',
        message: 'We are upgrading our systems.',
        expectedBackAt: '2 hours',  // or ISO date, or "soon"
        contactUrl: 'https://status.example.com',
        bypassPaths: ['/app/health', '/app/api/status'],
      },
      fallback: {
        title: 'Service Unavailable',
        message: 'The application is temporarily unavailable.',
        showRetry: true,
        autoRefresh: 30,  // seconds
      },
    },
    {
      path: '/docs',
      name: 'Documentation',
      type: 'static',
      staticPath: './docs-dist',
    },
  ],
});

Maintenance and Fallback Configuration

MaintenanceConfig - Shown when maintenance mode is enabled:

| Property | Type | Description | |----------|------|-------------| | enabled | boolean | Enable/disable maintenance mode | | title | string | Page title (default: "Under Maintenance") | | message | string | Custom message to display | | expectedBackAt | string | ETA: ISO date, relative time ("2 hours"), or "soon" | | contactUrl | string | Link to status page or contact | | bypassPaths | string[] | Paths that bypass maintenance (e.g., health checks) |

FallbackConfig - Shown when the proxied service is unreachable:

| Property | Type | Description | |----------|------|-------------| | title | string | Page title (default: "Service Unavailable") | | message | string | Custom message to display | | showRetry | boolean | Show retry button (default: true) | | autoRefresh | number | Auto-refresh countdown in seconds (default: 30) |

Both pages feature modern, responsive designs with automatic dark mode support.

Configuration

ControlPanelConfig

| Property | Type | Required | Description | |----------|------|----------|-------------| | productName | string | Yes | Name displayed in the dashboard | | port | number | Yes | Port to run the control panel on | | version | string | No | Product version to display | | mountPath | string | No | Path to mount control panel (default: /cpanel) | | guard | RouteGuardConfig | No | Authentication guard configuration | | branding | object | No | Logo, primary color, favicon | | cors | object | No | CORS origins configuration | | links | array | No | Quick links for the dashboard | | skipBodyParserPaths | string[] | No | Paths to skip body parsing (for proxy) | | logoUrl | string | No | Custom logo URL for the landing page |

Route Guards

The unified guard system provides authentication for the control panel. All guards support excludePaths to allow unauthenticated access to specific routes.

No Authentication

guard: { type: 'none' }

Basic Auth

guard: {
  type: 'basic',
  username: 'admin',
  password: process.env.ADMIN_PASSWORD!,
  realm: 'Control Panel',
  excludePaths: ['/api/health'],
}

Supabase OAuth

guard: {
  type: 'supabase',
  supabaseUrl: process.env.SUPABASE_URL!,
  supabaseAnonKey: process.env.SUPABASE_ANON_KEY!,
  excludePaths: ['/api/health'],
}

Auth0 OpenID Connect

Requires express-openid-connect as a peer dependency.

guard: {
  type: 'auth0',
  domain: process.env.AUTH0_DOMAIN!,
  clientId: process.env.AUTH0_CLIENT_ID!,
  clientSecret: process.env.AUTH0_CLIENT_SECRET!,
  baseUrl: 'https://myapp.example.com',
  secret: process.env.AUTH0_SECRET!,
  routes: {
    login: '/login',
    logout: '/logout',
    callback: '/callback',
  },
  excludePaths: ['/api/health'],
}

Mount Paths

By default, the control panel mounts at /cpanel, reserving the root path (/) for frontend applications:

// Control panel at /cpanel
config: {
  mountPath: '/cpanel',  // Dashboard at /cpanel, API at /cpanel/api/*
}

// Or mount at root (legacy behavior)
config: {
  mountPath: '/',  // Dashboard at /, API at /api/*
}

Frontend App Plugin

Handle the root path when control panel is mounted elsewhere:

import { createFrontendAppPlugin } from '@qwickapps/server';

// Redirect to external URL
createFrontendAppPlugin({
  redirectUrl: 'https://myapp.example.com',
})

// Serve static files
createFrontendAppPlugin({
  staticPath: './dist',
})

// Show landing page with links
createFrontendAppPlugin({
  landingPage: {
    title: 'My Service',
    heading: 'Welcome',
    description: 'Select a destination',
    links: [
      { label: 'Control Panel', url: '/cpanel' },
      { label: 'API Docs', url: '/docs' },
    ],
  },
})

API Authentication (M2M)

Enable machine-to-machine authentication for your API routes using bearer tokens. This is ideal for service-to-service communication.

Prerequisites

API authentication requires the rate-limit plugin:

import { createRateLimitPlugin, postgresRateLimitStore, getPostgres } from '@qwickapps/server';

plugins: [
  // PostgreSQL plugin (required by rate-limit)
  {
    plugin: createPostgresPlugin({
      url: process.env.DATABASE_URL,
    }),
  },

  // Rate-limit plugin (required by API keys auth)
  {
    plugin: createRateLimitPlugin({
      store: postgresRateLimitStore({
        pool: () => getPostgres().getPool(),
      }),
      defaults: {
        maxRequests: 1000,
        windowMs: 60000, // 1 minute
      },
    }),
  },
]

Enabling Authentication on Routes

Routes can opt-in to M2M authentication by setting auth: { required: true }:

import { createControlPanel } from '@qwickapps/server';

const controlPanel = createControlPanel({
  config: { /* ... */ },
  plugins: [
    {
      plugin: {
        name: 'my-api',
        async start({ registry }) {
          // Authenticated route - requires API key
          registry.addRoute({
            method: 'post',
            path: '/api/jobs/schedule',
            pluginId: 'my-api',
            auth: { required: true }, // Enable authentication
            handler: async (req, res) => {
              // req.apiKey contains verified key info
              res.json({ success: true });
            },
          });

          // Public route - no authentication required
          registry.addRoute({
            method: 'get',
            path: '/api/status',
            pluginId: 'my-api',
            // No auth property = unauthenticated
            handler: async (req, res) => {
              res.json({ status: 'ok' });
            },
          });
        },
      },
    },
  ],
});

Generating API Keys

Create M2M API keys for your clients:

import { createApiKey } from '@qwickapps/server';

// Generate a new M2M API key
const apiKey = await createApiKey({
  user_id: '00000000-0000-0000-0000-000000000000', // Service user ID
  name: 'production-worker',
  key_type: 'm2m', // Machine-to-machine
  scopes: [], // No scopes required in Phase 1A
  expires_at: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), // 90 days
});

console.log(`API Key: ${apiKey.plaintext_key}`);
// Output: qk_live_BdrbOkdpYidqkx0vkFnulmSUR4eYFLbAGndrPOAOxM8

Important: The plaintext_key is only returned once during creation. Store it securely.

Using API Keys

Clients pass the API key in the Authorization header:

# cURL example
curl -X POST https://api.example.com/api/jobs/schedule \
  -H "Authorization: Bearer qk_live_BdrbOkdpYidqkx0vkFnulmSUR4eYFLbAGndrPOAOxM8" \
  -H "Content-Type: application/json" \
  -d '{"type": "llm-completion", "model": "gpt-3.5-turbo"}'
// Node.js example
const response = await fetch('https://api.example.com/api/jobs/schedule', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    type: 'llm-completion',
    model: 'gpt-3.5-turbo',
  }),
});

API Key Management

import { listApiKeys, getApiKey, updateApiKey, deleteApiKey } from '@qwickapps/server';

// List all keys for a user
const keys = await listApiKeys(userId);

// Get specific key
const key = await getApiKey(userId, keyId);

// Update key (name, scopes, expiration, active status)
await updateApiKey(userId, keyId, {
  name: 'updated-name',
  is_active: false, // Disable key
});

// Delete (revoke) key
await deleteApiKey(userId, keyId);

Security Best Practices

  1. Store keys securely: Use environment variables or secrets management
  2. Rotate keys regularly: Generate new keys and delete old ones every 90 days
  3. Use HTTPS: Always transmit keys over encrypted connections
  4. Monitor usage: Track last_used_at to identify unused keys
  5. Set expiration: Configure expires_at for automatic key expiry
  6. Revoke compromised keys: Delete keys immediately if leaked

Error Handling

Authentication failures return HTTP 401:

{
  "success": false,
  "error": "Invalid, expired, or inactive API key"
}

Rate limit exceeded returns HTTP 429:

{
  "success": false,
  "error": "Rate limit exceeded"
}

Plugins

Built-in Plugins

Health Plugin

Monitors service health with configurable checks:

import { createHealthPlugin } from '@qwickapps/server';

createHealthPlugin({
  checks: [
    {
      name: 'api',
      type: 'http',
      url: 'http://localhost:3000/health',
      interval: 10000,
      timeout: 5000,
    },
    {
      name: 'custom',
      type: 'custom',
      check: async () => ({
        healthy: true,
        details: { message: 'All good' },
      }),
      interval: 30000,
    },
  ],
});

Diagnostics Plugin

import { createDiagnosticsPlugin } from '@qwickapps/server';

createDiagnosticsPlugin({});

Config Plugin

import { createConfigPlugin } from '@qwickapps/server';

createConfigPlugin({
  show: ['NODE_ENV', 'PORT', 'DATABASE_URL'],
  mask: ['SECRET', 'PASSWORD', 'KEY', 'TOKEN'],
});

Logs Plugin

import { createLogsPlugin } from '@qwickapps/server';

createLogsPlugin({
  sources: [
    { name: 'app', type: 'file', path: './logs/app.log' },
    { name: 'error', type: 'file', path: './logs/error.log' },
  ],
});

PostgreSQL Plugin

Provides connection pooling, transactions, and health checks for PostgreSQL databases.

import { createPostgresPlugin, getPostgres } from '@qwickapps/server';

// Register the plugin
createPostgresPlugin({
  connectionString: process.env.DATABASE_URL,
  // Or individual options:
  // host: 'localhost',
  // port: 5432,
  // database: 'mydb',
  // user: 'postgres',
  // password: 'secret',
  maxConnections: 20,
  healthCheckInterval: 30000,
});

// Use in your code
const pg = getPostgres();
const result = await pg.query('SELECT * FROM users WHERE id = $1', [userId]);

// With transactions
await pg.withTransaction(async (client) => {
  await client.query('INSERT INTO orders ...');
  await client.query('UPDATE inventory ...');
});

Exports:

  • createPostgresPlugin(config) - Create and register the plugin
  • getPostgres(name?) - Get a PostgreSQL instance (throws if not registered)
  • hasPostgres(name?) - Check if an instance is registered
  • PostgresInstance - TypeScript type for the instance

Cache Plugin

Redis-based caching with key prefixing, TTL support, and pattern operations.

import { createCachePlugin, getCache } from '@qwickapps/server';

// Register the plugin
createCachePlugin({
  url: process.env.REDIS_URL,
  // Or individual options:
  // host: 'localhost',
  // port: 6379,
  // password: 'secret',
  keyPrefix: 'myapp:',
  defaultTTL: 3600, // 1 hour default
  healthCheckInterval: 30000,
});

// Use in your code
const cache = getCache();

// Basic operations
await cache.set('user:123', userData, 600); // TTL in seconds
const user = await cache.get('user:123');
await cache.delete('user:123');

// Pattern operations
const keys = await cache.keys('user:*');
await cache.deletePattern('session:*');

// Stats and maintenance
const stats = await cache.getStats();
await cache.flush(); // Clear all keys with prefix

Exports:

  • createCachePlugin(config) - Create and register the plugin
  • getCache(name?) - Get a cache instance (throws if not registered)
  • hasCache(name?) - Check if an instance is registered
  • CacheInstance - TypeScript type for the instance

Auth Plugin

Pluggable authentication with support for multiple providers via the adapter pattern.

Zero-Config Environment Setup

The simplest way to configure auth is via environment variables:

import { createAuthPluginFromEnv } from '@qwickapps/server';

// Auto-configures based on AUTH_ADAPTER env var
const authPlugin = createAuthPluginFromEnv();

// With overrides
const authPlugin = createAuthPluginFromEnv({
  excludePaths: ['/health', '/metrics'],
  authRequired: true,
});

Environment Variables:

# General (required to enable auth)
AUTH_ADAPTER=supertokens|auth0|supabase|basic
AUTH_REQUIRED=true                    # Default: true
AUTH_EXCLUDE_PATHS=/health,/metrics   # Comma-separated
AUTH_DEBUG=false

# Supertokens
SUPERTOKENS_CONNECTION_URI=http://localhost:3567
SUPERTOKENS_APP_NAME=MyApp
SUPERTOKENS_API_DOMAIN=http://localhost:3000
SUPERTOKENS_WEBSITE_DOMAIN=http://localhost:3000
SUPERTOKENS_API_KEY=                  # Optional, for managed service
SUPERTOKENS_GOOGLE_CLIENT_ID=         # Optional social providers
SUPERTOKENS_GOOGLE_CLIENT_SECRET=
SUPERTOKENS_GITHUB_CLIENT_ID=
SUPERTOKENS_GITHUB_CLIENT_SECRET=

# Auth0
AUTH0_DOMAIN=myapp.auth0.com
AUTH0_CLIENT_ID=
AUTH0_CLIENT_SECRET=
AUTH0_BASE_URL=http://localhost:3000
AUTH0_SECRET=                         # Session encryption secret
AUTH0_AUDIENCE=                       # Optional, for API access tokens
AUTH0_SCOPES=openid,profile,email     # Default scopes

# Supabase
SUPABASE_URL=https://xxx.supabase.co
SUPABASE_ANON_KEY=

# Basic Auth
BASIC_AUTH_USERNAME=admin
BASIC_AUTH_PASSWORD=
BASIC_AUTH_REALM=Protected            # Default: Protected

Plugin States:

| State | Condition | Behavior | |-------|-----------|----------| | disabled | AUTH_ADAPTER not set | No auth middleware | | enabled | Valid configuration | Auth active | | error | Invalid configuration | Disabled with error details |

Check Auth Status:

import { getAuthStatus } from '@qwickapps/server';

const status = getAuthStatus();
// { state: 'enabled', adapter: 'supertokens', config: {...} }
Programmatic Configuration

For more control, configure adapters directly in code:

import { createAuthPlugin, auth0Adapter, basicAdapter } from '@qwickapps/server';

// Auth0 with RBAC and domain restrictions
createAuthPlugin({
  adapter: auth0Adapter({
    domain: process.env.AUTH0_DOMAIN!,
    clientId: process.env.AUTH0_CLIENT_ID!,
    clientSecret: process.env.AUTH0_CLIENT_SECRET!,
    baseUrl: 'https://myapp.example.com',
    secret: process.env.SESSION_SECRET!,
    audience: process.env.AUTH0_AUDIENCE,    // For API access tokens
    allowedRoles: ['admin', 'support'],       // RBAC filtering
    allowedDomains: ['@company.com'],         // Domain whitelist
    exposeAccessToken: true,                  // For downstream API calls
  }),
  excludePaths: ['/health', '/api/public'],
});

// Basic auth fallback
createAuthPlugin({
  adapter: basicAdapter({
    username: 'admin',
    password: process.env.ADMIN_PASSWORD!,
  }),
});

Available Adapters:

  • supertokensAdapter - Supertokens email/password + social logins (requires supertokens-node)
  • auth0Adapter - Auth0 OIDC (requires express-openid-connect)
  • supabaseAdapter - Supabase JWT validation
  • basicAdapter - HTTP Basic authentication

Helper Functions:

import { isAuthenticated, getAuthenticatedUser, getAccessToken } from '@qwickapps/server';

// In your route handlers
if (isAuthenticated(req)) {
  const user = getAuthenticatedUser(req);
  // { id, email, name, picture, emailVerified, roles }

  const accessToken = getAccessToken(req);
  // Use for downstream API calls
}

Middleware Helpers:

import { requireAuth, requireRoles, requireAnyRole } from '@qwickapps/server';

// Require authentication
app.get('/admin', requireAuth(), (req, res) => { ... });

// Require specific roles (all required)
app.get('/admin/users', requireRoles('admin', 'user-manager'), (req, res) => { ... });

// Require any of the roles
app.get('/dashboard', requireAnyRole('admin', 'editor', 'viewer'), (req, res) => { ... });

Users Plugin

Storage-agnostic user management with ban support.

import { createUsersPlugin, postgresUserStore, getPostgres } from '@qwickapps/server';
import { Pool } from 'pg';

// Create with PostgreSQL storage
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

createUsersPlugin({
  store: postgresUserStore({
    pool,
    usersTable: 'users',
    bansTable: 'user_bans',
    autoCreateTables: true,
  }),
  bans: {
    enabled: true,
    supportTemporary: true, // Enable expiring bans
    onBan: async (user, ban) => {
      // Notify external systems, revoke sessions, etc.
      console.log(`User ${user.email} banned: ${ban.reason}`);
    },
    onUnban: async (user) => {
      console.log(`User ${user.email} unbanned`);
    },
  },
  api: {
    prefix: '/api/users',
    crud: true,    // GET/POST/PUT/DELETE /api/users
    search: true,  // GET /api/users?q=...
    bans: true,    // Ban management endpoints
  },
});

REST API Endpoints:

| Endpoint | Method | Description | |----------|--------|-------------| | /api/users | GET | List/search users | | /api/users | POST | Create user | | /api/users/:id | GET | Get user by ID | | /api/users/:id | PUT | Update user | | /api/users/:id | DELETE | Delete user | | /api/users/bans | GET | List active bans | | /api/users/:id/ban | GET | Get user's ban status | | /api/users/:id/ban | POST | Ban user | | /api/users/:id/ban | DELETE | Unban user | | /api/users/:id/bans | GET | Get user's ban history |

Helper Functions:

import { getUserById, getUserByEmail, isUserBanned, findOrCreateUser } from '@qwickapps/server';

// Get user
const user = await getUserById('user-123');
const userByEmail = await getUserByEmail('[email protected]');

// Check ban status
const banned = await isUserBanned('user-123');

// Find or create from auth provider
const user = await findOrCreateUser({
  email: '[email protected]',
  name: 'Test User',
  external_id: 'auth0|12345',
  provider: 'auth0',
});

Email Ban Support (for auth-only scenarios):

For cases where you don't store users locally but need to ban by email:

import { isEmailBanned, getEmailBan, banEmail, unbanEmail } from '@qwickapps/server';

// Check if email is banned
const banned = await isEmailBanned('[email protected]');

// Get ban details
const ban = await getEmailBan('[email protected]');

// Ban an email
await banEmail({
  email: '[email protected]',
  reason: 'Spam activity',
  banned_by: '[email protected]',
  duration: 86400, // 24 hours (optional, null = permanent)
});

// Unban an email
await unbanEmail({
  email: '[email protected]',
  unbanned_by: '[email protected]',
  note: 'Cleared after review',
});

Tenants Plugin

Multi-tenant data isolation and organization management with role-based access control.

import { createTenantsPlugin, postgresTenantStore } from '@qwickapps/server';
import { Pool } from 'pg';

// Create with PostgreSQL storage
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

createTenantsPlugin({
  store: postgresTenantStore({
    pool,
    tenantsTable: 'tenants',
    membershipsTable: 'tenant_memberships',
    autoCreateTables: true,
  }),
  apiPrefix: '/api/tenants',
  apiEnabled: true,
});

Tenant Types:

  • user - Single-user personal tenant (auto-created on user signup)
  • organization - Multi-user organization
  • group - Team or project group
  • department - Company department

Global Roles:

  • owner - Full control, can delete tenant
  • admin - Manage members and settings
  • member - Standard access
  • viewer - Read-only access

Features:

  • Auto-creates owner membership when tenant is created
  • Foreign key constraints with CASCADE delete for data integrity
  • UPSERT behavior for membership updates (ON CONFLICT DO UPDATE)
  • Search with filtering by type, owner, and query string
  • Pagination and sorting support

Authentication & Authorization:

All tenant API routes require authentication. Authorization rules enforce tenant-level access control:

  • List tenants: Users see only their own tenants
  • Get tenant: User must be a member
  • Create tenant: Auto-added as owner
  • Update tenant: Admin or owner role required
  • Delete tenant: Owner role only
  • Manage members: Admin or owner role required

REST API Endpoints:

| Endpoint | Method | Description | Authorization | |----------|--------|-------------|---------------| | /api/tenants | GET | List/search user's tenants | Authenticated (returns user's tenants) | | /api/tenants | POST | Create new tenant | Authenticated (becomes owner) | | /api/tenants/:id | GET | Get tenant by ID | Member of tenant | | /api/tenants/:id | PUT | Update tenant | Admin or owner | | /api/tenants/:id | DELETE | Delete tenant (cascades) | Owner only | | /api/tenants/user/:userId | GET | Get tenants for user | Own tenants only | | /api/tenants/:tenantId/members | GET | List tenant members | Member of tenant | | /api/tenants/:tenantId/members | POST | Add member to tenant | Admin or owner | | /api/tenants/:tenantId/members/:userId | PUT | Update member role | Admin or owner | | /api/tenants/:tenantId/members/:userId | DELETE | Remove member | Admin or owner |

Helper Functions:

import { getTenantStore } from '@qwickapps/server';

const store = getTenantStore();

// Get tenant
const tenant = await store.getById('tenant-123');

// Get all tenants for a user with membership info
const tenants = await store.getTenantsForUser('user-123');

// Create organization tenant
const org = await store.create({
  name: 'Acme Corp',
  type: 'organization',
  owner_id: 'user-123',
  metadata: { industry: 'technology' },
});

// Add team member
await store.addMember({
  tenant_id: org.id,
  user_id: 'user-456',
  role: 'admin',
});

// Search tenants
const results = await store.search({
  query: 'acme',
  type: 'organization',
  page: 1,
  limit: 20,
  sortBy: 'name',
  sortOrder: 'asc',
});

Plugin Composition:

The Tenants plugin integrates seamlessly with the Users plugin through the plugin registry:

// Users plugin auto-detects tenants plugin
createUsersPlugin({ store: userStore });
createTenantsPlugin({ store: tenantStore });

// When a user is created, a personal tenant is auto-created
// Works gracefully whether tenants plugin is present or not

Preferences Plugin

Per-user preferences storage with PostgreSQL Row-Level Security (RLS) for data isolation.

import { createPreferencesPlugin, postgresPreferencesStore } from '@qwickapps/server';
import { Pool } from 'pg';

const pool = new Pool({ connectionString: process.env.DATABASE_URL });

createPreferencesPlugin({
  store: postgresPreferencesStore({
    pool,
    tableName: 'user_preferences',
    autoCreateTables: true,
    enableRLS: true,  // Enable Row-Level Security
  }),
  defaults: {
    theme: 'system',
    notifications: {
      email: true,
      push: true,
    },
  },
  api: {
    prefix: '/preferences',
    enabled: true,
  },
});

Note: The Preferences plugin requires the Users plugin to be loaded first, as it creates a foreign key reference to the users table.

REST API Endpoints:

| Endpoint | Method | Description | |----------|--------|-------------| | /api/preferences | GET | Get current user's preferences (merged with defaults) | | /api/preferences | PUT | Update preferences (deep merge with existing) | | /api/preferences | DELETE | Reset preferences to defaults |

Helper Functions:

import {
  getPreferences,
  updatePreferences,
  deletePreferences,
  getDefaultPreferences,
  deepMerge,
} from '@qwickapps/server';

// Get user preferences (merged with defaults)
const prefs = await getPreferences('user-123');

// Update preferences (deep merge - preserves nested values)
const updated = await updatePreferences('user-123', {
  theme: 'dark',
  notifications: { email: false },
});
// Result: { theme: 'dark', notifications: { email: false, push: true } }

// Reset to defaults
await deletePreferences('user-123');

// Get configured defaults
const defaults = getDefaultPreferences();

// Deep merge utility (exported for custom use)
const merged = deepMerge(baseObject, overrides);

Security Features:

  • PostgreSQL Row-Level Security ensures users can only access their own preferences
  • Transaction-safe RLS context for connection pooling compatibility
  • Input validation: 100KB size limit, 10-level nesting depth
  • Foreign key to users table with ON DELETE CASCADE

Rate Limit Plugin

API rate limiting with multiple strategies, PostgreSQL persistence, and Redis caching.

import {
  createRateLimitPlugin,
  postgresRateLimitStore,
  rateLimitMiddleware,
} from '@qwickapps/server';
import { Pool } from 'pg';

const pool = new Pool({ connectionString: process.env.DATABASE_URL });

// Add plugin to gateway
createRateLimitPlugin({
  store: postgresRateLimitStore({
    pool,
    tableName: 'rate_limits',
    autoCreateTables: true,
    enableRLS: true,
  }),
  defaults: {
    windowMs: 60000,      // 1 minute window
    maxRequests: 100,     // 100 requests per window
    strategy: 'sliding-window',  // or 'fixed-window', 'token-bucket'
  },
  cache: {
    type: 'auto',  // 'redis' | 'memory' | 'auto' (uses Redis if available)
  },
  cleanup: {
    enabled: true,
    intervalMs: 300000,  // Clean up expired limits every 5 minutes
  },
});

// Use middleware on routes
app.use('/api', rateLimitMiddleware());

// Per-route configuration
app.post('/api/chat', rateLimitMiddleware({
  windowMs: 60000,
  max: 50,  // Lower limit for expensive operations
  keyGenerator: (req) => `chat:${req.user?.id}`,
}));

// Dynamic limits based on user tier
app.use(rateLimitMiddleware({
  max: (req) => req.user?.tier === 'premium' ? 1000 : 50,
}));

Rate Limiting Strategies:

| Strategy | Description | Best For | |----------|-------------|----------| | sliding-window | Smooth rate limiting with weighted overlap | Most use cases (default) | | fixed-window | Simple discrete time windows | High performance, simple needs | | token-bucket | Allows bursts while maintaining average rate | APIs needing burst capacity |

REST API Endpoints:

| Endpoint | Method | Description | |----------|--------|-------------| | /api/rate-limit/status | GET | Get rate limit status for current user | | /api/rate-limit/status/:key | GET | Get status for a specific key | | /api/rate-limit/clear/:key | DELETE | Clear a rate limit (requires auth) |

Programmatic API:

import {
  isLimited,
  checkLimit,
  incrementLimit,
  getRemainingRequests,
  getLimitStatus,
  clearLimit,
} from '@qwickapps/server';

// Check if rate limited
const limited = await isLimited('user:123:api');

// Get full status without incrementing
const status = await checkLimit('user:123:api');
// { limited: false, current: 45, limit: 100, remaining: 55, resetAt: 1702656000, retryAfter: 30 }

// Increment and get status
const result = await incrementLimit('user:123:api');

// Clear limit (e.g., after CAPTCHA)
await clearLimit('user:123:api');

Response Headers:

RateLimit-Limit: 100
RateLimit-Remaining: 55
RateLimit-Reset: 1702656000
Retry-After: 30  (only when limited)

Security Features:

  • PostgreSQL Row-Level Security isolates rate limits per user
  • Redis caching with in-memory fallback for high availability
  • Fail-open by default (allows requests on errors)

Environment Variable Configuration:

Use createRateLimitPluginFromEnv() for zero-config setup:

import { createRateLimitPluginFromEnv, createPostgresPlugin } from '@qwickapps/server';

// Requires postgres-plugin to be registered first
const gateway = createGateway({
  plugins: [
    createPostgresPlugin({ /* ... */ }),
    createRateLimitPluginFromEnv(),  // Reads config from env vars
  ],
});

| Environment Variable | Default | Description | |---------------------|---------|-------------| | RATE_LIMIT_ENABLED | true | Enable rate limiting | | RATE_LIMIT_STRATEGY | sliding-window | Strategy: sliding-window, fixed-window, token-bucket | | RATE_LIMIT_WINDOW_MS | 60000 | Window size in milliseconds | | RATE_LIMIT_MAX_REQUESTS | 100 | Maximum requests per window | | RATE_LIMIT_CACHE_TYPE | auto | Cache type: redis, memory, auto | | RATE_LIMIT_CLEANUP_ENABLED | true | Enable cleanup job | | RATE_LIMIT_CLEANUP_INTERVAL_MS | 300000 | Cleanup interval (5 min) | | RATE_LIMIT_API_ENABLED | true | Enable status API endpoints | | RATE_LIMIT_API_PREFIX | /rate-limit | API route prefix | | RATE_LIMIT_TABLE_NAME | rate_limits | PostgreSQL table name | | RATE_LIMIT_ENABLE_RLS | true | Enable Row-Level Security | | RATE_LIMIT_AUTO_CREATE_TABLES | true | Auto-create tables | | RATE_LIMIT_DEBUG | false | Enable debug logging |

Config status endpoint: GET /api/rate-limit/config/status

Runtime Configuration UI:

The Rate Limit plugin includes a Control Panel page for live configuration:

  • Navigate to /rate-limits in the Control Panel
  • Edit default window size, max requests, and strategy
  • Toggle cleanup job on/off and adjust interval
  • Changes take effect immediately (no restart required)

Runtime Config API:

# Get current config
curl http://localhost:3000/api/rate-limit/config

# Update config at runtime
curl -X PUT http://localhost:3000/api/rate-limit/config \
  -H "Content-Type: application/json" \
  -d '{"maxRequests": 200, "windowMs": 120000, "strategy": "token-bucket"}'

Creating Custom Plugins

import type { ControlPanelPlugin, PluginContext } from '@qwickapps/server';

const myPlugin: ControlPanelPlugin = {
  name: 'my-plugin',
  order: 10,
  routes: [
    {
      method: 'get',
      path: '/my-plugin/status',
      handler: async (req, res) => {
        res.json({ status: 'ok' });
      },
    },
  ],
  initialize: async (context: PluginContext) => {
    context.logger.debug('My plugin initialized');
  },
  shutdown: async () => {
    // Cleanup
  },
};

API Reference

createControlPanel(options)

Creates a new control panel instance.

Returns: ControlPanelInstance

interface ControlPanelInstance {
  app: Express.Application;
  start: () => Promise<void>;
  stop: () => Promise<void>;
  registerPlugin: (plugin: ControlPanelPlugin) => void;
  getHealthStatus: () => Promise<HealthStatus>;
}

createRouteGuard(config)

Creates authentication middleware from guard configuration.

import { createRouteGuard } from '@qwickapps/server';

const guard = createRouteGuard({
  type: 'basic',
  username: 'admin',
  password: 'secret',
});

app.use(guard);

isAuthenticated(req)

Check if current request is authenticated.

import { isAuthenticated } from '@qwickapps/server';

if (isAuthenticated(req)) {
  // User is authenticated
}

getAuthenticatedUser(req)

Get authenticated user information.

import { getAuthenticatedUser } from '@qwickapps/server';

const user = getAuthenticatedUser(req);
// { id: string; email?: string; name?: string; }

Built-in Routes

Routes are mounted at the configured mountPath (default /cpanel):

| Route | Method | Description | |-------|--------|-------------| | {mountPath}/ | GET | Dashboard UI | | {mountPath}/api/health | GET | Aggregated health status | | {mountPath}/api/diagnostics | GET | System diagnostics | | {mountPath}/api/config | GET | Configuration (if plugin enabled) | | {mountPath}/api/logs | GET | Log viewer (if plugin enabled) |

License

This software is licensed under the PolyForm Shield License 1.0.0.

What This Means

Permitted Uses:

  • Internal business applications
  • Learning and educational projects
  • Non-competitive commercial applications
  • Academic research and teaching
  • Building applications that use this control panel framework

Prohibited Uses:

  • Creating competing control panel frameworks
  • Building competing developer tools or dashboards
  • Reselling or redistributing as a competing product
  • Reverse engineering to create competitive products

For full license terms, see PolyForm Shield 1.0.0.

For commercial licensing options, contact [email protected].


Copyright (c) 2025 QwickApps. All rights reserved.

Build trigger Mon Dec 29 09:36:28 EST 2025