Extension Architecture
This guide explains the internal architecture of NetPad's extension system, including how extensions are loaded, registered, and integrated with the core application.
System Overviewβ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β NetPad Application β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ β
β β Extension β β Extension β β Extension β β
β β Loader βββββΆβ Registry βββββΆβ Services β β
β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ β
β β β β β
β βΌ βΌ βΌ β
β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ β
β β Environment β β Feature Flags β β Route Handler β β
β β Detection β β β β β β
β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Core Componentsβ
Extension Loader (src/lib/extensions/loader.ts)β
The loader is responsible for discovering and importing extension packages.
// Loading process
async function loadExtensions(): Promise<void> {
// 1. Load cloud extension if in cloud mode
const cloudExtension = await loadCloudExtension();
if (cloudExtension) registerExtension(cloudExtension);
// 2. Load plugin extensions from NETPAD_EXTENSIONS
const pluginExtensions = await loadPluginExtensions();
for (const extension of pluginExtensions) {
registerExtension(extension);
}
// 3. Initialize all registered extensions
await initializeExtensions();
}
Key functions:
| Function | Description |
|---|---|
loadExtensions() | Main entry point, loads all extensions |
loadExtensionPackage(name) | Dynamically imports an extension package |
extensionsLoaded() | Returns true if extensions have been loaded |
waitForExtensions() | Awaits extension loading completion |
resetLoader() | Resets loader state (for testing) |
Extension Registry (src/lib/extensions/registry.ts)β
The registry maintains the state of all loaded extensions and provides access to their capabilities.
// Registry state
interface ExtensionRegistryState {
extensions: Map<string, NetPadExtension>;
features: Set<ExtensionFeature>;
initialized: boolean;
}
// Key operations
registerExtension(extension: NetPadExtension): void
initializeExtensions(): Promise<void>
getExtension(id: string): NetPadExtension | undefined
isFeatureAvailable(feature: ExtensionFeature): boolean
getExtensionRoutes(): ExtensionRoute[]
getExtensionMiddleware(): ExtensionMiddleware[]
getService<T>(extensionId: string, serviceName: string): T | undefined
Extension Types (src/lib/extensions/types.ts)β
Core type definitions for the extension system:
/**
* Main extension interface - all extensions must implement this
*/
export interface NetPadExtension {
metadata: ExtensionMetadata;
features: ExtensionFeature[];
routes?: ExtensionRoute[];
middleware?: ExtensionMiddleware[];
services?: Record<string, unknown>;
components?: Record<string, React.ComponentType>;
initialize?: () => Promise<void>;
cleanup?: () => Promise<void>;
}
/**
* Extension metadata
*/
export interface ExtensionMetadata {
id: string; // Unique identifier
name: string; // Display name
version: string; // Semantic version
description?: string; // Human-readable description
author?: string; // Package author
homepage?: string; // Documentation URL
}
/**
* API route definition
*/
export interface ExtensionRoute {
path: string; // URL path
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
handler: (req: NextRequest, ctx?: RouteContext) => Promise<NextResponse>;
middleware?: string[]; // Middleware IDs to apply
}
/**
* Middleware definition
*/
export interface ExtensionMiddleware {
id?: string; // Optional identifier
path: string; // Path pattern to match
handler: MiddlewareHandler;
priority?: number; // Execution order (lower = earlier)
}
/**
* Feature flags
*/
export type ExtensionFeature =
| 'billing'
| 'stripe_integration'
| 'subscription_management'
| 'usage_tracking'
| 'atlas_provisioning'
| 'premium_support'
| 'advanced_analytics'
| 'sso_authentication'
| 'audit_logging'
| 'custom_branding'
| `custom:${string}`; // Custom features use this prefix
Extension Lifecycleβ
1. Discovery Phaseβ
Extensions are discovered through:
- Environment Variables -
NETPAD_EXTENSIONScontains comma-separated package names - Cloud Mode Detection -
NETPAD_CLOUD=truetriggers cloud extension loading - Static Registration - Known extensions are pre-registered in the loader
// Known extension loaders (enables webpack static analysis)
const knownExtensionLoaders: Record<string, () => Promise<unknown>> = {
'@netpad/cloud-features': () => import('@netpad/cloud-features').catch(() => null),
'@netpad/collaborate': () => import('@netpad/collaborate').catch(() => null),
};
2. Loading Phaseβ
Each extension package is dynamically imported and validated:
async function loadExtensionPackage(packageName: string): Promise<NetPadExtension | null> {
// Try known loaders first (for webpack compatibility)
if (knownExtensionLoaders[packageName]) {
const module = await knownExtensionLoaders[packageName]();
// Extract extension from module exports
}
// Look for default export or named exports
// - module.default
// - module.extension
// - module.{packageName}Extension
}
3. Registration Phaseβ
Extensions are registered in the global registry:
function registerExtension(extension: NetPadExtension): void {
// Add to extensions map
state.extensions.set(extension.metadata.id, extension);
// Register features
for (const feature of extension.features) {
state.features.add(feature);
}
console.log(`[Extensions] Registered: ${extension.metadata.name} v${extension.metadata.version}`);
}
4. Initialization Phaseβ
Each extension's initialize() hook is called:
async function initializeExtensions(): Promise<void> {
for (const [id, extension] of state.extensions) {
if (extension.initialize) {
await extension.initialize();
console.log(`[Extensions] Initialized: ${id}`);
}
}
state.initialized = true;
}
5. Runtime Phaseβ
During runtime, the application accesses extension capabilities:
- Routes are matched via the catch-all handler at
/api/ext/[...path] - Features are checked via
isFeatureAvailable() - Services are accessed via
getService() - Middleware is applied based on path matching
6. Cleanup Phaseβ
When the application shuts down or during hot reload:
async function cleanupExtensions(): Promise<void> {
for (const [id, extension] of state.extensions) {
if (extension.cleanup) {
await extension.cleanup();
}
}
}
Route Handlingβ
Extension routes are handled by a catch-all Next.js route:
src/app/api/ext/[...path]/route.ts
This handler:
- Loads extensions if not already loaded
- Extracts the path from the URL
- Matches against registered routes
- Applies any associated middleware
- Invokes the route handler
// Route matching
async function handleExtensionRoute(
request: NextRequest,
method: HttpMethod,
pathParts: string[]
): Promise<NextResponse> {
// Ensure extensions are loaded
if (!extensionsLoaded()) {
await loadExtensions();
}
// Build full path
const fullPath = `/api/ext/${pathParts.join('/')}`;
// Find matching route
const routes = getExtensionRoutes();
const matchingRoute = routes.find(
route => route.method === method && route.path === fullPath
);
if (!matchingRoute) {
return NextResponse.json(
{ error: 'Extension route not found', path: fullPath, method },
{ status: 404 }
);
}
// Execute handler
return await matchingRoute.handler(request, { params: { path: pathParts } });
}
Feature Flag Systemβ
Features allow conditional functionality based on which extensions are loaded:
// Check feature availability
function isFeatureAvailable(feature: ExtensionFeature): boolean {
return state.features.has(feature);
}
// Get feature availability details
function getFeatureAvailability(feature: ExtensionFeature): {
available: boolean;
provider?: string;
message?: string;
} {
if (!state.features.has(feature)) {
return {
available: false,
message: `Feature "${feature}" requires an extension that is not installed`,
};
}
// Find which extension provides this feature
for (const [id, extension] of state.extensions) {
if (extension.features.includes(feature)) {
return {
available: true,
provider: extension.metadata.name,
};
}
}
return { available: true };
}
Using Features in Codeβ
// In API routes
import { isFeatureAvailable } from '@/lib/extensions/registry';
export async function POST(request: NextRequest) {
if (!isFeatureAvailable('billing')) {
return NextResponse.json(
{ error: 'Billing features are not available in this deployment' },
{ status: 400 }
);
}
// Process billing request...
}
// In React components
function BillingSection() {
const billingAvailable = isFeatureAvailable('billing');
if (!billingAvailable) {
return <SelfHostedBillingMessage />;
}
return <BillingDashboard />;
}
Service Architectureβ
Extensions can expose services for shared functionality:
// Define a service
interface MyService {
doSomething(): Promise<void>;
getSomething(): Promise<Data>;
}
// Register in extension
const myExtension: NetPadExtension = {
// ...
services: {
myService: myServiceImplementation,
},
};
// Access from other code
import { getService } from '@/lib/extensions/registry';
const myService = getService<MyService>('my-extension', 'myService');
if (myService) {
await myService.doSomething();
}
Middleware Systemβ
Extensions can provide middleware that processes requests:
// Define middleware
const authMiddleware: ExtensionMiddleware = {
id: 'my-extension:auth',
path: '/api/ext/my-extension/*',
priority: 10, // Lower = runs earlier
handler: async (request, next) => {
// Verify authentication
const token = request.headers.get('authorization');
if (!token) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Continue to next middleware or route handler
return next(request);
},
};
Middleware is executed in priority order:
function getExtensionMiddleware(): ExtensionMiddleware[] {
const allMiddleware: ExtensionMiddleware[] = [];
for (const extension of state.extensions.values()) {
if (extension.middleware) {
allMiddleware.push(...extension.middleware);
}
}
// Sort by priority (lower first)
return allMiddleware.sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100));
}
Error Handlingβ
Extensions are designed to fail gracefully:
// Extension loading errors are caught
async function loadExtensionPackage(packageName: string) {
try {
// Attempt to load
} catch (error) {
// Log but don't crash
console.error(`[Extensions] Error loading ${packageName}:`, error);
return null;
}
}
// Initialization errors are isolated
async function initializeExtensions() {
for (const [id, extension] of state.extensions) {
try {
await extension.initialize?.();
} catch (error) {
console.error(`[Extensions] Failed to initialize ${id}:`, error);
// Continue with other extensions
}
}
}
Testing Extensionsβ
The extension system provides testing utilities:
import { resetRegistry } from '@/lib/extensions/registry';
import { resetLoader } from '@/lib/extensions/loader';
describe('My Extension', () => {
beforeEach(() => {
resetRegistry();
resetLoader();
});
it('should register correctly', () => {
registerExtension(myExtension);
expect(getExtension('my-extension')).toBeDefined();
});
it('should provide features', () => {
registerExtension(myExtension);
expect(isFeatureAvailable('custom:my_feature')).toBe(true);
});
});
Next Stepsβ
- Building Extensions - Create your own extension
- API Reference - Complete API documentation
- Example: Collaborate Extension - Real-world walkthrough