Creating Plugins


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.

Plugin Architecture

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

Quick Start

1. Create Plugin Scaffold

# Create new plugin project
nself plugin create my-service

# This creates:
# - package.json with dependencies
# - TypeScript configuration
# - Basic src/ structure
# - Example tests

2. Install Dependencies

cd nself-plugin-my-service
npm install

3. Define Your Schema

src/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
      `,
    },
  ],
};

4. Implement Sync Logic

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`);
}

5. Add CLI Commands

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();

6. Build and Test

# Build TypeScript
npm run build

# Run locally
./bin/nself-myservice init
./bin/nself-myservice sync

# Install globally for testing
npm link

Package.json Configuration

package.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"]
    }
  }
}

Webhook Handler

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 });
}

HTTP Server

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}`);
  });
}

Database Migrations

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);

Testing Your Plugin

Unit Tests

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');
  });
});

Integration Tests

# Run with test database
DATABASE_URL=postgresql://localhost:5432/nself_test \
npm test

# Run specific test file
npm test -- src/__tests__/sync.test.ts

Manual Testing

# 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"}}'

Publishing Your Plugin

1. Prepare for Publication

# Ensure all tests pass
npm test

# Build for production
npm run build

# Verify package contents
npm pack --dry-run

2. Publish to NPM

# Login to NPM
npm login

# Publish package
npm publish

# Or publish with specific tag
npm publish --tag beta

3. Register with nself

Submit your plugin to the nself plugin registry:

  1. Fork the nself-plugins repository
  2. Add your plugin to registry/plugins.json
  3. Submit a pull request

registry/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"
}

Plugin SDK Reference

Database Helpers

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');

Logger

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);

HTTP Client

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' });

Best Practices

Security

  • Never log secrets - Mask API keys and tokens in logs
  • Always verify webhooks - Use HMAC signature verification
  • Parameterize queries - Never concatenate user input into SQL
  • Use environment variables - Never hardcode credentials

Performance

  • Batch database operations - Use bulk inserts/updates
  • Implement incremental sync - Only sync changes when possible
  • Respect rate limits - Implement backoff and retry logic
  • Use connection pooling - Reuse database connections

Error Handling

  • Validate input early - Check environment variables on startup
  • Provide clear error messages - Include context and suggestions
  • Implement retries - Handle transient failures gracefully
  • Log errors with context - Include request IDs and timestamps

Documentation

  • README.md - Include quick start, configuration, and examples
  • CLI help - Implement --help for all commands
  • Type definitions - Export TypeScript types for consumers
  • Changelog - Document all changes in CHANGELOG.md

Related

Last Updated: January 2026 | Version 0.4.8