BaasixBaasix

Plugins Guide

Baasix has a powerful plugin system that allows you to extend the platform with custom functionality. Plugins can add new database schemas, API routes, lifecycle hooks, services, scheduled tasks, and more.

Table of Contents

  1. Plugin Overview
  2. Creating a Plugin
  3. Plugin Structure
  4. Plugin Context & Services
  5. Adding Schemas
  6. Adding Routes
  7. Adding Hooks
  8. Adding Services
  9. Adding Scheduled Tasks
  10. Adding Middleware
  11. Lifecycle Hooks
  12. Cross-Plugin Communication
  13. Publishing Your Plugin
  14. Example: Complete Plugin

Plugin Overview

Plugins are self-contained modules that can:

  • Add Schemas: Create new database collections/tables
  • Add Routes: Register custom API endpoints
  • Add Hooks: Run code before/after CRUD operations
  • Add Services: Provide reusable business logic
  • Add Schedules: Run tasks on a cron schedule
  • Add Middleware: Inject Express middleware

Plugin Types

TypeDescriptionExamples
featureBusiness featuresE-commerce, CMS, Blog
authAuthentication providersOAuth, OTP, Passkey
paymentPayment processingStripe, PayPal, Razorpay
storageFile storageS3, GCS, Cloudinary
aiAI/ML integrationsRAG, Embeddings, LLM
notificationMessagingEmail, SMS, Push
integrationThird-party servicesWebhooks, APIs

Creating a Plugin

Basic Plugin Structure

import { definePlugin } from '@tspvivek/baasix';

export function myPlugin(config: MyPluginConfig) {
  return definePlugin({
    meta: {
      name: 'my-plugin',
      version: '1.0.0',
      type: 'feature',
      description: 'My awesome plugin',
      author: 'Your Name',
    },

    // Database schemas
    schemas: [...],

    // API routes
    routes: [...],

    // Lifecycle hooks
    hooks: [...],

    // Services
    services: [...],

    // Scheduled tasks
    schedules: [...],

    // Middleware
    middleware: [...],

    // Lifecycle callbacks
    onInit: async (context) => { ... },
    onReady: async (context) => { ... },
    onShutdown: async (context) => { ... },
  });
}

Registering the Plugin

import { startServer } from '@tspvivek/baasix';
import { myPlugin } from './plugins/my-plugin';

startServer({
  port: 8055,
  plugins: [
    myPlugin({
      apiKey: process.env.MY_API_KEY,
      // ... other config
    }),
  ],
});

Plugin Structure

my-plugin/
├── package.json
├── tsconfig.json
├── src/
│   ├── index.ts          # Main entry point
│   ├── types.ts          # Type definitions
│   ├── schemas.ts        # Database schemas
│   ├── routes.ts         # API routes
│   ├── hooks.ts          # Lifecycle hooks
│   ├── services/         # Service implementations
│   │   └── myService.ts
│   └── utils/            # Utilities
└── README.md

Package.json Example

{
  "name": "@baasix/plugin-my-feature",
  "version": "1.0.0",
  "type": "module",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  },
  "scripts": {
    "build": "tsup src/index.ts --format esm --dts",
    "dev": "tsup src/index.ts --format esm --dts --watch"
  },
  "peerDependencies": {
    "@tspvivek/baasix": "^1.0.0"
  }
}

Plugin Context & Services

When your plugin's lifecycle hooks, service factories, and route handlers are called, they receive a context object containing all available services.

Available Services in Context

Singleton Services (Pre-initialized)

These are ready to use directly:

ServiceTypeDescription
dbDrizzle ORMDatabase connection for raw queries
permissionServiceSingletonRole-based access control, field-level security
mailServiceSingletonEmail sending via SMTP with Liquid templates
storageServiceSingletonFile storage abstraction (LOCAL/S3 drivers)
settingsServiceSingletonGlobal and tenant-specific app settings
socketServiceSingletonWebSocket/Socket.IO management, rooms, handlers
realtimeServiceSingletonPostgreSQL WAL-based realtime change data capture
tasksServiceSingletonScheduled task execution with Redis locking
workflowServiceSingletonWorkflow engine with multiple node types
migrationServiceSingletonDatabase migration execution and tracking
hooksManagerSingletonLifecycle hooks manager for collections

Utility Functions

FunctionDescription
getCacheService()Get cache instance for caching operations
invalidateCache(collection?)Invalidate cache for a collection or entire cache
getPluginService(pluginName, serviceName)Get another plugin's service

Class-Based Services (Per-Request)

These should be instantiated with accountability for proper permission checks:

ServiceUsageDescription
ItemsServicenew ItemsService(collection, { accountability })CRUD operations on collections
FilesServicenew FilesService({ accountability })File upload/download with metadata
AssetsServicenew AssetsService({ accountability })Image processing with transformations
NotificationServicenew NotificationService({ accountability })In-app notifications
ReportServicenew ReportService(collection, { accountability })Report generation with aggregation
StatsServicenew StatsService({ accountability })Statistics from multiple collections

Using Services in Routes

const routes = [
  {
    path: '/my-plugin/data',
    method: 'GET',
    requireAuth: true,
    handler: async (req, res, context) => {
      const { ItemsService, mailService, storageService, getCacheService } = context;

      // Use ItemsService with user's permissions
      const itemsService = new ItemsService('my_collection', {
        accountability: req.accountability,
      });

      const data = await itemsService.readByQuery({
        filter: { status: { eq: 'active' } },
        limit: 10,
      });

      // Use singleton services directly
      await mailService.send({
        to: 'user@example.com',
        subject: 'Data Retrieved',
        template: 'notification',
      });

      // Use cache service
      const cache = getCacheService();
      if (cache) {
        await cache.set('my-key', data, 3600);
      }

      res.json({ data: data.data });
    },
  },
];

Using Services in Lifecycle Hooks

const plugin = definePlugin({
  // ...
  onInit: async (context) => {
    const { settingsService, tasksService } = context;

    // Register a scheduled task
    await tasksService.register({
      name: 'my-plugin-cleanup',
      cron: '0 0 * * *', // Daily at midnight
      handler: async () => {
        console.log('Running cleanup...');
      },
    });
  },

  onReady: async (context) => {
    const { socketService, realtimeService } = context;

    // Register custom socket handler
    socketService?.registerHandler('my-plugin:event', (socket, data) => {
      console.log('Received event:', data);
    });
  },

  onShutdown: async (context) => {
    console.log('Plugin shutting down...');
  },
});

Adding Schemas

Plugins can define database schemas that are automatically created when the plugin is loaded.

const schemas = [
  {
    collectionName: 'my_plugin_items',
    schema: {
      name: 'my_plugin_items',
      timestamps: true, // Adds createdAt, updatedAt
      paranoid: true, // Adds deletedAt for soft deletes
      usertrack: true, // Adds createdBy, updatedBy
      fields: {
        title: {
          type: 'string',
          length: 255,
          notNull: true,
        },
        description: {
          type: 'text',
        },
        status: {
          type: 'string',
          length: 50,
          default: 'draft',
        },
        price: {
          type: 'decimal',
          precision: 10,
          scale: 2,
        },
        isActive: {
          type: 'boolean',
          default: true,
        },
        metadata: {
          type: 'json',
        },
        // Relationships
        user_Id: {
          type: 'uuid',
          references: {
            table: 'baasix_User',
            column: 'id',
            onDelete: 'CASCADE',
          },
        },
      },
      indexes: [{ fields: ['status'] }, { fields: ['user_Id', 'status'] }, { fields: ['title'], unique: true }],
    },
  },
];

Field Types

TypeDescriptionOptions
stringVARCHARlength (default: 255)
textTEXT-
integerINT-
bigintBIGINT-
decimalDECIMALprecision, scale
booleanBOOLEAN-
dateDATE-
timestampTIMESTAMP-
jsonJSONB-
uuidUUID-
arrayArray of typeitemType

Adding Routes

Define custom API endpoints for your plugin.

const routes = [
  {
    path: '/my-plugin/items',
    method: 'GET',
    requireAuth: true,
    description: 'List all items',
    handler: async (req, res, context) => {
      const { ItemsService } = context;
      const service = new ItemsService('my_plugin_items', {
        accountability: req.accountability,
      });

      const result = await service.readByQuery({
        filter: req.query.filter ? JSON.parse(req.query.filter) : {},
        limit: parseInt(req.query.limit) || 25,
        page: parseInt(req.query.page) || 1,
      });

      res.json(result);
    },
  },
  {
    path: '/my-plugin/items',
    method: 'POST',
    requireAuth: true,
    description: 'Create an item',
    handler: async (req, res, context) => {
      const { ItemsService } = context;
      const service = new ItemsService('my_plugin_items', {
        accountability: req.accountability,
      });

      const item = await service.createOne(req.body);
      res.status(201).json({ data: item });
    },
  },
  {
    path: '/my-plugin/webhook',
    method: 'POST',
    requireAuth: false, // Public endpoint
    rawBody: true, // For webhook signature verification
    description: 'Webhook endpoint',
    handler: async (req, res, context) => {
      const signature = req.headers['x-signature'];
      const rawBody = req.rawBody;

      // Verify and process webhook
      res.json({ received: true });
    },
  },
];

Route Options

OptionTypeDescription
pathstringRoute path (e.g., /my-plugin/action)
methodstringHTTP method (GET, POST, PUT, PATCH, DELETE)
requireAuthbooleanWhether authentication is required
rawBodybooleanParse raw body (for webhooks)
middlewarearrayCustom Express middleware
descriptionstringRoute description for docs

Adding Hooks

Hooks run before or after CRUD operations on collections.

const hooks = [
  {
    collection: 'my_plugin_items',
    event: 'items.create', // Before create
    handler: async (context) => {
      // Validate or modify data before insert
      if (!context.data.title) {
        throw new Error('Title is required');
      }

      // Add computed field
      context.data.slug = context.data.title.toLowerCase().replace(/\s+/g, '-');

      return context;
    },
  },
  {
    collection: 'my_plugin_items',
    event: 'items.create.after', // After create
    handler: async (context) => {
      // Send notification, update cache, etc.
      console.log('Item created:', context.id);
      return context;
    },
  },
  {
    collection: '*', // All collections
    event: 'items.delete',
    handler: async (context) => {
      // Log all deletions
      console.log(`Deleting from ${context.collection}:`, context.id);
      return context;
    },
  },
];

Hook Events

EventTimingUse Case
items.createBefore insertValidation, data transformation
items.create.afterAfter insertNotifications, cache update
items.readBefore readQuery modification
items.read.afterAfter readData transformation
items.updateBefore updateValidation, computed fields
items.update.afterAfter updateNotifications, audit log
items.deleteBefore deletePrevent deletion, cascade
items.delete.afterAfter deleteCleanup, notifications

Hook Context Properties

interface HookContext {
  collection: string; // Collection name
  accountability: object; // User context
  db: any; // Database connection
  data?: any; // Data being created/updated
  id?: string; // ID for update/delete/read
  query?: object; // Query options
}

Adding Services

Services provide reusable business logic that can be used in routes, hooks, and other plugins.

// services/myService.ts
export function createMyService(context) {
  const { ItemsService, mailService, getCacheService } = context;

  return {
    async processItem(id: string) {
      const service = new ItemsService('my_plugin_items', {});
      const item = await service.readOne(id);

      // Process the item
      const result = await this.doSomething(item);

      // Cache the result
      const cache = getCacheService();
      if (cache) {
        await cache.set(`processed:${id}`, result, 3600);
      }

      return result;
    },

    async doSomething(item: any) {
      // Business logic here
      return { ...item, processed: true };
    },

    async sendNotification(userId: string, message: string) {
      await mailService.send({
        to: userId,
        subject: 'Notification',
        text: message,
      });
    },
  };
}

// In your plugin definition
const plugin = definePlugin({
  // ...
  services: [
    {
      name: 'myService',
      factory: (context) => createMyService(context),
    },
  ],
});

Using Plugin Services

// In routes
handler: async (req, res, context) => {
  const { services } = context;
  const result = await services.myService.processItem(req.params.id);
  res.json(result);
};

// In other plugins
handler: async (req, res, context) => {
  const myService = context.getPluginService('my-plugin', 'myService');
  const result = await myService.processItem(req.params.id);
  res.json(result);
};

Adding Scheduled Tasks

Run tasks on a schedule using cron expressions.

const schedules = [
  {
    name: 'daily-cleanup',
    cron: '0 0 * * *', // Every day at midnight
    runOnStart: false, // Don't run immediately on startup
    handler: async (context) => {
      const { ItemsService, invalidateCache } = context;

      // Delete old records
      const service = new ItemsService('my_plugin_logs', {});
      await service.deleteByQuery({
        filter: {
          createdAt: {
            lt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), // 30 days ago
          },
        },
      });

      // Invalidate cache
      await invalidateCache('my_plugin_logs');

      console.log('Cleanup completed');
    },
  },
  {
    name: 'hourly-sync',
    cron: '0 * * * *', // Every hour
    handler: async (context) => {
      // Sync with external service
      console.log('Syncing...');
    },
  },
];

Cron Expression Reference

┌───────────── minute (0 - 59)
│ ┌───────────── hour (0 - 23)
│ │ ┌───────────── day of month (1 - 31)
│ │ │ ┌───────────── month (1 - 12)
│ │ │ │ ┌───────────── day of week (0 - 6)
│ │ │ │ │
* * * * *
ExpressionDescription
* * * * *Every minute
0 * * * *Every hour
0 0 * * *Every day at midnight
0 0 * * 0Every Sunday at midnight
0 0 1 * *First day of every month
*/5 * * * *Every 5 minutes

Adding Middleware

Add Express middleware that runs on all or specific routes.

const middleware = [
  {
    name: 'request-logger',
    priority: 1, // Lower runs first
    handler: (req, res, next) => {
      console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
      next();
    },
  },
  {
    name: 'rate-limiter',
    path: '/my-plugin/*', // Only on plugin routes
    priority: 10,
    handler: (req, res, next) => {
      // Rate limiting logic
      next();
    },
  },
];

Lifecycle Hooks

onInit

Called when the plugin is initialized, before routes are registered. Use for setup that doesn't depend on other plugins.

onInit: async (context) => {
  console.log('Plugin initializing...');

  // Initialize external SDK
  const client = new ExternalSDK(context.config.apiKey);

  // Store in context for later use
  context.services.externalClient = client;
};

onReady

Called when all plugins are loaded and the server is ready. Use for setup that may depend on other plugins.

onReady: async (context) => {
  console.log('Plugin ready!');

  // Register socket handlers
  context.socketService?.registerHandler('my-event', (socket, data) => {
    console.log('Received:', data);
  });

  // Start background processes
  startBackgroundWorker(context);
};

onShutdown

Called when the server is shutting down. Use for cleanup.

onShutdown: async (context) => {
  console.log('Plugin shutting down...');

  // Close connections
  await context.services.externalClient?.close();

  // Flush queues
  await flushPendingJobs();
};

Cross-Plugin Communication

Plugins can access services from other plugins using getPluginService.

// Plugin A registers a service
const pluginA = definePlugin({
  meta: { name: 'plugin-a', ... },
  services: [
    {
      name: 'dataProcessor',
      factory: (context) => ({
        process: async (data) => {
          // Processing logic
          return { processed: true, data };
        },
      }),
    },
  ],
});

// Plugin B uses Plugin A's service
const pluginB = definePlugin({
  meta: {
    name: 'plugin-b',
    dependencies: ['plugin-a'],  // Ensure Plugin A loads first
  },
  routes: [
    {
      path: '/plugin-b/process',
      method: 'POST',
      handler: async (req, res, context) => {
        // Get Plugin A's service
        const dataProcessor = context.getPluginService('plugin-a', 'dataProcessor');

        const result = await dataProcessor.process(req.body);
        res.json(result);
      },
    },
  ],
});

Publishing Your Plugin

1. Prepare for Publishing

# Build the plugin
npm run build

# Test locally
npm pack

2. Package.json Configuration

{
  "name": "@baasix/plugin-my-feature",
  "version": "1.0.0",
  "description": "My awesome Baasix plugin",
  "keywords": ["baasix", "plugin", "my-feature"],
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "files": ["dist", "README.md"],
  "repository": {
    "type": "git",
    "url": "https://github.com/username/baasix-plugin-my-feature"
  },
  "peerDependencies": {
    "@tspvivek/baasix": ">=1.0.0"
  }
}

3. Publish to npm

npm publish --access public

4. Usage in Projects

import { startServer } from '@tspvivek/baasix';
import { myFeaturePlugin } from '@baasix/plugin-my-feature';

startServer({
  plugins: [
    myFeaturePlugin({
      // Configuration
    }),
  ],
});

Example: Complete Plugin

Here's a complete example of a "Comments" plugin:

// src/index.ts
import { definePlugin } from '@tspvivek/baasix';

export interface CommentsPluginConfig {
  moderationEnabled?: boolean;
  maxCommentLength?: number;
}

export function commentsPlugin(config: CommentsPluginConfig = {}) {
  const { moderationEnabled = true, maxCommentLength = 1000 } = config;

  return definePlugin({
    meta: {
      name: 'comments',
      version: '1.0.0',
      type: 'feature',
      description: 'Add comments to any collection',
      author: 'Baasix Team',
    },

    schemas: [
      {
        collectionName: 'comments',
        schema: {
          name: 'comments',
          timestamps: true,
          usertrack: true,
          fields: {
            content: {
              type: 'text',
              notNull: true,
            },
            targetCollection: {
              type: 'string',
              length: 100,
              notNull: true,
            },
            targetId: {
              type: 'uuid',
              notNull: true,
            },
            status: {
              type: 'string',
              length: 20,
              default: moderationEnabled ? 'pending' : 'approved',
            },
            parentComment_Id: {
              type: 'uuid',
              references: {
                table: 'comments',
                column: 'id',
                onDelete: 'CASCADE',
              },
            },
          },
          indexes: [
            { fields: ['targetCollection', 'targetId'] },
            { fields: ['status'] },
            { fields: ['parentComment_Id'] },
          ],
        },
      },
    ],

    routes: [
      {
        path: '/comments/:collection/:targetId',
        method: 'GET',
        requireAuth: false,
        description: 'Get comments for an item',
        handler: async (req, res, context) => {
          const { ItemsService } = context;
          const { collection, targetId } = req.params;

          const service = new ItemsService('comments', {
            accountability: req.accountability,
          });

          const result = await service.readByQuery({
            filter: {
              targetCollection: { eq: collection },
              targetId: { eq: targetId },
              status: { eq: 'approved' },
            },
            fields: ['*', 'createdBy.firstName', 'createdBy.lastName'],
            sort: ['-createdAt'],
          });

          res.json(result);
        },
      },
      {
        path: '/comments/:collection/:targetId',
        method: 'POST',
        requireAuth: true,
        description: 'Add a comment',
        handler: async (req, res, context) => {
          const { ItemsService } = context;
          const { collection, targetId } = req.params;
          const { content, parentComment_Id } = req.body;

          if (!content || content.length > maxCommentLength) {
            return res.status(400).json({
              error: `Content is required and must be under ${maxCommentLength} characters`,
            });
          }

          const service = new ItemsService('comments', {
            accountability: req.accountability,
          });

          const comment = await service.createOne({
            content,
            targetCollection: collection,
            targetId,
            parentComment_Id,
            status: moderationEnabled ? 'pending' : 'approved',
          });

          res.status(201).json({ data: comment });
        },
      },
    ],

    hooks: [
      {
        collection: 'comments',
        event: 'items.create.after',
        handler: async (context) => {
          const { NotificationService } = context;

          // Notify moderators if moderation is enabled
          if (moderationEnabled) {
            const notificationService = new NotificationService({});
            // Get moderator user IDs and notify them
            console.log('New comment pending moderation:', context.id);
          }

          return context;
        },
      },
    ],

    services: [
      {
        name: 'commentsService',
        factory: (context) => ({
          async getCommentCount(collection: string, targetId: string) {
            const { ItemsService } = context;
            const service = new ItemsService('comments', {});

            const result = await service.readByQuery({
              filter: {
                targetCollection: { eq: collection },
                targetId: { eq: targetId },
                status: { eq: 'approved' },
              },
              aggregate: { count: '*' },
            });

            return result.data?.[0]?.count || 0;
          },

          async moderateComment(commentId: string, status: 'approved' | 'rejected') {
            const { ItemsService } = context;
            const service = new ItemsService('comments', {});

            return service.updateOne(commentId, { status });
          },
        }),
      },
    ],

    onReady: async (context) => {
      console.log('[Comments Plugin] Ready! Moderation:', moderationEnabled ? 'enabled' : 'disabled');
    },
  });
}

// Export types
export type { CommentsPluginConfig };

Using the Comments Plugin

import { startServer } from '@tspvivek/baasix';
import { commentsPlugin } from '@baasix/plugin-comments';

startServer({
  port: 8055,
  plugins: [
    commentsPlugin({
      moderationEnabled: true,
      maxCommentLength: 2000,
    }),
  ],
});

Best Practices

  1. Use TypeScript: Provides better developer experience and type safety
  2. Validate Input: Always validate user input in route handlers
  3. Handle Errors: Use try-catch and return appropriate error responses
  4. Document Everything: Add descriptions to routes and clear README
  5. Test Thoroughly: Write tests for your plugin functionality
  6. Version Properly: Follow semver for version numbers
  7. Minimal Dependencies: Keep external dependencies minimal
  8. Use Context Services: Leverage built-in services instead of reimplementing
  9. Respect Permissions: Use ItemsService with accountability for permission checks
  10. Clean Up: Implement onShutdown to clean up resources

On this page