Version 0.4.8 | Create custom plugins for nself
This guide covers how to create custom plugins for nself. Plugins extend nself with third-party integrations, providing database schemas, webhook handlers, CLI commands, and more.
nself plugins are TypeScript/JavaScript packages that follow a standard structure:
nself-plugin-myservice/
├── package.json # NPM package manifest
├── tsconfig.json # TypeScript configuration
├── src/
│ ├── index.ts # Main entry point
│ ├── cli.ts # CLI commands
│ ├── schema.ts # Database schema definition
│ ├── sync.ts # Data synchronization logic
│ ├── webhooks.ts # Webhook handlers
│ ├── api.ts # External API client
│ └── types.ts # TypeScript types
├── migrations/ # Database migrations
│ ├── 001_initial.sql
│ └── ...
└── README.md# Create new plugin project
nself plugin create my-service
# This creates:
# - package.json with dependencies
# - TypeScript configuration
# - Basic src/ structure
# - Example testscd nself-plugin-my-service
npm installsrc/schema.ts
import { PluginSchema } from '@nself/plugin-sdk';
export const schema: PluginSchema = {
tables: [
{
name: 'myservice_items',
columns: [
{ name: 'id', type: 'text', primaryKey: true },
{ name: 'external_id', type: 'text', unique: true },
{ name: 'name', type: 'text', nullable: false },
{ name: 'data', type: 'jsonb', default: '{}' },
{ name: 'synced_at', type: 'timestamptz', default: 'now()' },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
],
indexes: [
{ columns: ['external_id'] },
{ columns: ['synced_at'] },
],
},
],
views: [
{
name: 'myservice_stats',
query: `
SELECT
COUNT(*) as total_items,
MAX(synced_at) as last_sync
FROM myservice_items
`,
},
],
};src/sync.ts
import { Database, Logger } from '@nself/plugin-sdk';
import { MyServiceClient } from './api';
export async function syncAll(
db: Database,
client: MyServiceClient,
logger: Logger
): Promise<void> {
logger.info('Starting full sync...');
const items = await client.listItems();
for (const item of items) {
await db.upsert('myservice_items', {
id: item.id,
external_id: item.externalId,
name: item.name,
data: item.metadata,
synced_at: new Date(),
});
}
logger.info(`Synced ${items.length} items`);
}src/cli.ts
import { Command } from 'commander';
import { createDatabase, createLogger } from '@nself/plugin-sdk';
import { MyServiceClient } from './api';
import { syncAll } from './sync';
const program = new Command();
program
.name('nself-myservice')
.description('MyService plugin for nself')
.version('1.0.0');
program
.command('init')
.description('Initialize database schema')
.action(async () => {
const db = await createDatabase();
await db.migrate('./migrations');
console.log('Database initialized');
});
program
.command('sync')
.description('Sync data from MyService')
.option('--incremental', 'Only sync changes')
.action(async (options) => {
const db = await createDatabase();
const client = new MyServiceClient(process.env.MYSERVICE_API_KEY!);
const logger = createLogger();
await syncAll(db, client, logger);
});
program.parse();# Build TypeScript
npm run build
# Run locally
./bin/nself-myservice init
./bin/nself-myservice sync
# Install globally for testing
npm linkpackage.json
{
"name": "nself-plugin-myservice",
"version": "1.0.0",
"description": "MyService integration for nself",
"author": "Your Name",
"license": "MIT",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"bin": {
"nself-myservice": "./bin/nself-myservice"
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"test": "jest",
"lint": "eslint src/"
},
"dependencies": {
"@nself/plugin-sdk": "^0.4.8",
"commander": "^11.0.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.0.0"
},
"engines": {
"node": ">=20.0.0"
},
"nself": {
"category": "custom",
"minVersion": "0.4.8",
"defaultPort": 3010,
"env": {
"required": ["MYSERVICE_API_KEY", "DATABASE_URL"],
"optional": ["MYSERVICE_WEBHOOK_SECRET", "PORT"]
}
}
}Implement webhook handling with signature verification:
src/webhooks.ts
import express from 'express';
import crypto from 'crypto';
import { Database, Logger } from '@nself/plugin-sdk';
export function createWebhookHandler(db: Database, logger: Logger) {
const router = express.Router();
// Verify webhook signature
function verifySignature(payload: string, signature: string): boolean {
const secret = process.env.MYSERVICE_WEBHOOK_SECRET;
if (!secret) {
logger.warn('No webhook secret configured');
return true; // Skip verification if no secret
}
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
router.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
const signature = req.headers['x-myservice-signature'] as string;
const payload = req.body.toString();
if (!verifySignature(payload, signature)) {
logger.error('Invalid webhook signature');
return res.status(401).json({ error: 'Invalid signature' });
}
try {
const event = JSON.parse(payload);
logger.info(`Received webhook: ${event.type}`);
// Store event
await db.insert('myservice_webhook_events', {
event_id: event.id,
event_type: event.type,
payload: event,
received_at: new Date(),
});
// Process based on event type
switch (event.type) {
case 'item.created':
await handleItemCreated(db, event.data);
break;
case 'item.updated':
await handleItemUpdated(db, event.data);
break;
case 'item.deleted':
await handleItemDeleted(db, event.data);
break;
default:
logger.warn(`Unknown event type: ${event.type}`);
}
res.json({ received: true });
} catch (error) {
logger.error('Webhook processing failed', error);
res.status(500).json({ error: 'Processing failed' });
}
});
return router;
}
async function handleItemCreated(db: Database, data: any) {
await db.insert('myservice_items', {
id: data.id,
external_id: data.external_id,
name: data.name,
data: data.metadata,
synced_at: new Date(),
});
}
async function handleItemUpdated(db: Database, data: any) {
await db.update('myservice_items', { id: data.id }, {
name: data.name,
data: data.metadata,
synced_at: new Date(),
updated_at: new Date(),
});
}
async function handleItemDeleted(db: Database, data: any) {
await db.delete('myservice_items', { id: data.id });
}Create an HTTP server for webhooks and REST API:
src/server.ts
import express from 'express';
import { createDatabase, createLogger } from '@nself/plugin-sdk';
import { createWebhookHandler } from './webhooks';
import { createApiRouter } from './api-routes';
export async function startServer(port: number = 3010) {
const db = await createDatabase();
const logger = createLogger();
const app = express();
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Webhook endpoint
app.use(createWebhookHandler(db, logger));
// REST API
app.use('/api', createApiRouter(db, logger));
app.listen(port, () => {
logger.info(`Server listening on port ${port}`);
});
}Use SQL migrations for schema changes:
migrations/001_initial.sql
-- Migration: 001_initial
-- Description: Initial schema for myservice plugin
CREATE TABLE IF NOT EXISTS myservice_items (
id TEXT PRIMARY KEY,
external_id TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
data JSONB DEFAULT '{}',
synced_at TIMESTAMPTZ DEFAULT NOW(),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_myservice_items_external_id ON myservice_items(external_id);
CREATE INDEX idx_myservice_items_synced_at ON myservice_items(synced_at);
CREATE TABLE IF NOT EXISTS myservice_webhook_events (
id SERIAL PRIMARY KEY,
event_id TEXT UNIQUE NOT NULL,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
status TEXT DEFAULT 'pending',
processed_at TIMESTAMPTZ,
error TEXT,
received_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_myservice_webhook_events_status ON myservice_webhook_events(status);
CREATE VIEW myservice_stats AS
SELECT
COUNT(*) AS total_items,
COUNT(*) FILTER (WHERE synced_at > NOW() - INTERVAL '24 hours') AS synced_today,
MAX(synced_at) AS last_sync
FROM myservice_items;migrations/002_add_category.sql
-- Migration: 002_add_category
-- Description: Add category field to items
ALTER TABLE myservice_items ADD COLUMN category TEXT;
CREATE INDEX idx_myservice_items_category ON myservice_items(category);src/__tests__/sync.test.ts
import { syncAll } from '../sync';
import { createMockDatabase, createMockLogger } from '@nself/plugin-sdk/testing';
describe('syncAll', () => {
it('should sync all items from API', async () => {
const db = createMockDatabase();
const logger = createMockLogger();
const mockClient = {
listItems: jest.fn().mockResolvedValue([
{ id: '1', externalId: 'ext-1', name: 'Item 1' },
{ id: '2', externalId: 'ext-2', name: 'Item 2' },
]),
};
await syncAll(db, mockClient as any, logger);
expect(db.upsert).toHaveBeenCalledTimes(2);
expect(logger.info).toHaveBeenCalledWith('Synced 2 items');
});
});# Run with test database
DATABASE_URL=postgresql://localhost:5432/nself_test \
npm test
# Run specific test file
npm test -- src/__tests__/sync.test.ts# Build and link locally
npm run build
npm link
# Test init command
nself-myservice init
# Test sync
nself-myservice sync
# Test server
nself-myservice server --port 3010
# Test webhook (in another terminal)
curl -X POST http://localhost:3010/webhook \
-H "Content-Type: application/json" \
-d '{"type":"item.created","id":"test-1","data":{"id":"1","name":"Test"}}'# Ensure all tests pass
npm test
# Build for production
npm run build
# Verify package contents
npm pack --dry-run# Login to NPM
npm login
# Publish package
npm publish
# Or publish with specific tag
npm publish --tag betaSubmit your plugin to the nself plugin registry:
registry/plugins.jsonregistry/plugins.json entry:
{
"name": "myservice",
"package": "nself-plugin-myservice",
"version": "1.0.0",
"description": "MyService integration for nself",
"category": "custom",
"author": "Your Name",
"repository": "https://github.com/yourusername/nself-plugin-myservice",
"keywords": ["sync", "api", "webhooks"],
"minNselfVersion": "0.4.8"
}import { createDatabase, Database } from '@nself/plugin-sdk';
const db = await createDatabase();
// Insert
await db.insert('table_name', { column: 'value' });
// Upsert (insert or update)
await db.upsert('table_name', { id: '1', name: 'Updated' });
// Update
await db.update('table_name', { id: '1' }, { name: 'New Name' });
// Delete
await db.delete('table_name', { id: '1' });
// Query
const rows = await db.query('SELECT * FROM table_name WHERE id = $1', ['1']);
// Run migrations
await db.migrate('./migrations');import { createLogger, Logger } from '@nself/plugin-sdk';
const logger = createLogger();
logger.debug('Debug message');
logger.info('Info message');
logger.warn('Warning message');
logger.error('Error message', error);import { createHttpClient } from '@nself/plugin-sdk';
const client = createHttpClient({
baseUrl: 'https://api.myservice.com',
headers: {
'Authorization': `Bearer ${process.env.MYSERVICE_API_KEY}`,
},
});
const response = await client.get('/items');
const data = await client.post('/items', { name: 'New Item' });--help for all commandsLast Updated: January 2026 | Version 0.4.8