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
- Plugin Overview
- Creating a Plugin
- Plugin Structure
- Plugin Context & Services
- Adding Schemas
- Adding Routes
- Adding Hooks
- Adding Services
- Adding Scheduled Tasks
- Adding Middleware
- Lifecycle Hooks
- Cross-Plugin Communication
- Publishing Your Plugin
- 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
| Type | Description | Examples |
|---|---|---|
feature | Business features | E-commerce, CMS, Blog |
auth | Authentication providers | OAuth, OTP, Passkey |
payment | Payment processing | Stripe, PayPal, Razorpay |
storage | File storage | S3, GCS, Cloudinary |
ai | AI/ML integrations | RAG, Embeddings, LLM |
notification | Messaging | Email, SMS, Push |
integration | Third-party services | Webhooks, 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
Recommended Directory 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.mdPackage.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:
| Service | Type | Description |
|---|---|---|
db | Drizzle ORM | Database connection for raw queries |
permissionService | Singleton | Role-based access control, field-level security |
mailService | Singleton | Email sending via SMTP with Liquid templates |
storageService | Singleton | File storage abstraction (LOCAL/S3 drivers) |
settingsService | Singleton | Global and tenant-specific app settings |
socketService | Singleton | WebSocket/Socket.IO management, rooms, handlers |
realtimeService | Singleton | PostgreSQL WAL-based realtime change data capture |
tasksService | Singleton | Scheduled task execution with Redis locking |
workflowService | Singleton | Workflow engine with multiple node types |
migrationService | Singleton | Database migration execution and tracking |
hooksManager | Singleton | Lifecycle hooks manager for collections |
Utility Functions
| Function | Description |
|---|---|
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:
| Service | Usage | Description |
|---|---|---|
ItemsService | new ItemsService(collection, { accountability }) | CRUD operations on collections |
FilesService | new FilesService({ accountability }) | File upload/download with metadata |
AssetsService | new AssetsService({ accountability }) | Image processing with transformations |
NotificationService | new NotificationService({ accountability }) | In-app notifications |
ReportService | new ReportService(collection, { accountability }) | Report generation with aggregation |
StatsService | new 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
| Type | Description | Options |
|---|---|---|
string | VARCHAR | length (default: 255) |
text | TEXT | - |
integer | INT | - |
bigint | BIGINT | - |
decimal | DECIMAL | precision, scale |
boolean | BOOLEAN | - |
date | DATE | - |
timestamp | TIMESTAMP | - |
json | JSONB | - |
uuid | UUID | - |
array | Array of type | itemType |
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
| Option | Type | Description |
|---|---|---|
path | string | Route path (e.g., /my-plugin/action) |
method | string | HTTP method (GET, POST, PUT, PATCH, DELETE) |
requireAuth | boolean | Whether authentication is required |
rawBody | boolean | Parse raw body (for webhooks) |
middleware | array | Custom Express middleware |
description | string | Route 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
| Event | Timing | Use Case |
|---|---|---|
items.create | Before insert | Validation, data transformation |
items.create.after | After insert | Notifications, cache update |
items.read | Before read | Query modification |
items.read.after | After read | Data transformation |
items.update | Before update | Validation, computed fields |
items.update.after | After update | Notifications, audit log |
items.delete | Before delete | Prevent deletion, cascade |
items.delete.after | After delete | Cleanup, 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)
│ │ │ │ │
* * * * *| Expression | Description |
|---|---|
* * * * * | Every minute |
0 * * * * | Every hour |
0 0 * * * | Every day at midnight |
0 0 * * 0 | Every 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 pack2. 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 public4. 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
- Use TypeScript: Provides better developer experience and type safety
- Validate Input: Always validate user input in route handlers
- Handle Errors: Use try-catch and return appropriate error responses
- Document Everything: Add descriptions to routes and clear README
- Test Thoroughly: Write tests for your plugin functionality
- Version Properly: Follow semver for version numbers
- Minimal Dependencies: Keep external dependencies minimal
- Use Context Services: Leverage built-in services instead of reimplementing
- Respect Permissions: Use ItemsService with accountability for permission checks
- Clean Up: Implement onShutdown to clean up resources
Related Documentation
- Services Reference - Detailed service documentation
- Hooks System - Hooks in detail
- Extensions Guide - Hooks and endpoints
- Schema Reference - Schema field types