Skip to content

Creating Plugins

This guide walks you through creating an Authrim plugin from scratch.

Prerequisites

RequirementVersion
Node.js18+
TypeScript5.0+
Zod3.x

Install the required dependencies:

Terminal window
pnpm add @authrim/ar-lib-plugin zod

Plugin Interface

Every Authrim plugin must implement the AuthrimPlugin interface:

interface AuthrimPlugin<TConfig = unknown> {
/** Unique plugin ID (e.g., 'notifier-resend') */
readonly id: string;
/** Semantic version (e.g., '1.0.0') */
readonly version: string;
/** Capabilities this plugin provides */
readonly capabilities: PluginCapability[];
/** Whether this is an official Authrim plugin */
readonly official?: boolean;
/** Zod schema for configuration validation */
readonly configSchema: z.ZodType<TConfig, z.ZodTypeDef, unknown>;
/** UI metadata for Admin dashboard */
readonly meta: PluginMeta;
/** Register capabilities with the registry (required) */
register(registry: CapabilityRegistry, config: TConfig): void;
/** Initialize the plugin (optional) */
initialize?(ctx: PluginContext, config: TConfig): Promise<void>;
/** Cleanup on unload (optional) */
shutdown?(): Promise<void>;
/** Health check for monitoring (optional) */
healthCheck?(ctx?: PluginContext, config?: TConfig): Promise<HealthStatus>;
}

Minimal Plugin Template

Here’s a complete, copy-paste ready template:

import { z } from 'zod';
import type {
AuthrimPlugin,
PluginContext,
CapabilityRegistry,
HealthStatus,
} from '@authrim/ar-lib-plugin';
// 1. Define configuration schema with descriptions for Admin UI
const configSchema = z.object({
apiKey: z.string().min(1).describe('Your API key from the provider dashboard'),
endpoint: z.string().url().default('https://api.example.com').describe('API endpoint URL'),
timeout: z.number().int().min(1000).max(30000).default(10000).describe('Request timeout (ms)'),
});
type MyPluginConfig = z.infer<typeof configSchema>;
// 2. Export the plugin
export const myPlugin: AuthrimPlugin<MyPluginConfig> = {
id: 'my-custom-plugin',
version: '1.0.0',
capabilities: ['notifier.custom'],
configSchema,
meta: {
name: 'My Custom Plugin',
description: 'A custom notification plugin for Example Service',
category: 'notification',
icon: 'bell',
author: {
name: 'Your Name',
},
license: 'MIT',
stability: 'stable',
minAuthrimVersion: '1.0.0',
},
// 3. Optional: Initialize external connections
async initialize(ctx: PluginContext, config: MyPluginConfig): Promise<void> {
ctx.logger.info('Initializing plugin', { pluginId: this.id });
// Validate external service connectivity
const response = await fetch(`${config.endpoint}/health`, {
signal: AbortSignal.timeout(config.timeout),
});
if (!response.ok) {
throw new Error(`Service unavailable: ${response.status}`);
}
},
// 4. Required: Register capabilities
register(registry: CapabilityRegistry, config: MyPluginConfig): void {
registry.registerNotifier('custom', {
async send(notification) {
const response = await fetch(`${config.endpoint}/send`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${config.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
to: notification.to,
message: notification.body,
}),
signal: AbortSignal.timeout(config.timeout),
});
if (!response.ok) {
return {
success: false,
error: `API error: ${response.status}`,
retryable: response.status >= 500,
};
}
const result = await response.json();
return {
success: true,
messageId: result.id,
};
},
});
},
// 5. Optional: Health check
async healthCheck(ctx, config): Promise<HealthStatus> {
if (!config) {
return { status: 'unhealthy', message: 'No configuration available' };
}
try {
const response = await fetch(`${config.endpoint}/health`, {
signal: AbortSignal.timeout(5000),
});
if (response.ok) {
return { status: 'healthy', timestamp: Date.now() };
}
return {
status: 'degraded',
message: `API returned ${response.status}`,
timestamp: Date.now(),
};
} catch (error) {
return {
status: 'unhealthy',
message: error instanceof Error ? error.message : 'Unknown error',
timestamp: Date.now(),
};
}
},
// 6. Optional: Cleanup
async shutdown(): Promise<void> {
// Close connections, cleanup resources
},
};

Plugin Metadata

The meta field provides information for the Admin UI:

interface PluginMeta {
// Required
name: string; // Display name
description: string; // Short description (1-2 sentences)
category: PluginCategory; // 'notification' | 'identity' | 'authentication' | 'flow'
// Author & License (required for community plugins)
author?: {
name: string;
email?: string;
url?: string;
};
license?: string; // SPDX format (e.g., "MIT", "Apache-2.0")
// Display
icon?: string; // Lucide icon name or URL
tags?: string[]; // Search tags
// Documentation
documentationUrl?: string;
repositoryUrl?: string;
// Compatibility
minAuthrimVersion?: string; // Minimum required Authrim version
// Status
stability?: 'stable' | 'beta' | 'alpha' | 'deprecated';
deprecationNotice?: string; // Migration instructions if deprecated
// Admin notes (internal only)
adminNotes?: string; // Tips, known issues, deployment notes
}

Capability Types

Plugins declare their capabilities using a {type}.{channel} format:

type PluginCapability =
| `notifier.${string}` // notifier.email, notifier.sms, notifier.push
| `idp.${string}` // idp.google, idp.saml, idp.oidc
| `authenticator.${string}` // authenticator.passkey, authenticator.totp
| `flow.${string}`; // flow.otp-send (future extension)

Plugin Context

The PluginContext provides access to Authrim’s infrastructure:

interface PluginContext {
readonly storage: PluginStorageAccess; // User, Client, Session stores
readonly policy: IPolicyInfra; // Authorization checks
readonly config: PluginConfigStore; // Plugin configuration
readonly logger: Logger; // Structured logging
readonly audit: AuditLogger; // Audit events
readonly tenantId: string; // Current tenant
readonly env: Env; // Environment bindings
}

Health Check Implementation

Implement healthCheck to report plugin status:

async healthCheck(ctx, config): Promise<HealthStatus> {
return {
status: 'healthy', // 'healthy' | 'degraded' | 'unhealthy'
message: 'All systems operational',
timestamp: Date.now(),
checks: {
api: { status: 'pass', message: 'API reachable' },
credentials: { status: 'pass', message: 'Credentials valid' },
},
};
}

Testing Your Plugin

Use Vitest for unit testing:

import { describe, it, expect, vi } from 'vitest';
import { CapabilityRegistry } from '@authrim/ar-lib-plugin';
import { myPlugin } from './my-plugin';
describe('MyPlugin', () => {
it('should register notifier capability', () => {
const registry = new CapabilityRegistry();
const config = {
apiKey: 'test-key',
endpoint: 'https://api.example.com',
timeout: 10000,
};
myPlugin.register(registry, config);
expect(registry.getNotifier('custom')).toBeDefined();
expect(registry.listCapabilities()).toContain('notifier.custom');
});
it('should send notification successfully', async () => {
const registry = new CapabilityRegistry();
const config = {
apiKey: 'test-key',
endpoint: 'https://api.example.com',
timeout: 10000,
};
// Mock fetch
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ id: 'msg-123' }),
});
myPlugin.register(registry, config);
const notifier = registry.getNotifier('custom')!;
const result = await notifier.send({
channel: 'custom',
body: 'Test message',
});
expect(result.success).toBe(true);
expect(result.messageId).toBe('msg-123');
});
it('should handle API errors', async () => {
const registry = new CapabilityRegistry();
const config = {
apiKey: 'test-key',
endpoint: 'https://api.example.com',
timeout: 10000,
};
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 500,
});
myPlugin.register(registry, config);
const notifier = registry.getNotifier('custom')!;
const result = await notifier.send({
channel: 'custom',
body: 'Test message',
});
expect(result.success).toBe(false);
expect(result.retryable).toBe(true);
});
});

Best Practices

Error Handling

Always return structured errors instead of throwing:

async send(notification) {
try {
const response = await fetch(url, options);
if (!response.ok) {
return {
success: false,
error: `API error: ${response.status}`,
retryable: response.status >= 500,
};
}
return { success: true, messageId: result.id };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
retryable: true,
};
}
}

Timeout Management

Always use timeouts for external calls:

const response = await fetch(url, {
signal: AbortSignal.timeout(config.timeout),
});

Security

  • Never log sensitive data (API keys, tokens)
  • Validate URLs to prevent SSRF
  • Set appropriate timeouts
  • Sanitize user input in templates

Next Steps