Writing Plugins to extend Supamode
Writing Plugins to extend Supamode with your own data types and UI components
This guide explains how to create full-stack plugins for Supamode that add custom Data Types with server-side data transformation and client-side UI rendering.
This feature is still in development, and the API is subject to change.
Plugin Architecture Overview
Supamode uses a bidirectional plugin architecture that separates client and server concerns while maintaining seamless data flow:
┌─────────────────┐ Data Flow ┌──────────────────┐│ ServicePlugin │ ────────────→ │ DataTypePlugin ││ (Server-Side) │ │ (Client-Side) ││ │ │ ││ • Data Transform│ │ • UI Rendering ││ • Batch Process │ │ • Form Inputs ││ • Database APIs │ │ • Configuration │└─────────────────┘ └──────────────────┘
Type Safety in Plugins
Supamode's plugin system supports full TypeScript type safety. You can specify input and output types for your plugins, ensuring type-safe data transformation and rendering.
Creating a Plugin with Turbo Generator
Step 1: Generate Plugin Package
Use Turbo's generator to scaffold a new plugin package:
# From project rootturbo gen plugin
This creates a new package structure:
packages/plugins/user-status/├── package.json├── tsconfig.json├── src/│ ├── index.ts
Step 2: Add Package to your apps
Add the plugin to your apps, so we can import it in our code.
pnpm add "@kit/plugins-user-status@workspace:*" --filter @kit/apppnpm add "@kit/plugins-user-status@workspace:*" --filter @kit/api
Plugin Implementation
ServicePlugin (Server-Side)
The server plugin transforms raw data into formatted data for the UI with full type safety:
// packages/plugins/user-badge/src/server/user-badge-server.tsimport type { DataTypeServicePlugin, ColumnDataTransformer,} from "@kit/plugins/server";import { PluginTypes } from "@kit/plugins/shared";// Define your input type - what comes from the databasetype UserBadgeInput = { value: string; // User ID from the database column};// Define your output type - what your plugin returnstype UserBadgeOutput = { id: string; name: string; email: string; avatarUrl: string | null; status: "active" | "inactive" | "pending";};// Transform function with type-safe input and outputconst transformUserBadge: ColumnDataTransformer< UserBadgeInput, UserBadgeOutput> = async ( data, // Array of UserBadgeInput objects context // Access to database, config, etc.) => { // Extract user IDs from input data const userIds = data.map((item) => item.value); // Batch fetch to avoid N+1 queries - CRITICAL FOR PERFORMANCE! const users = await context.serviceContext.drizzleClient .select() .from(userTable) .where(inArray(userTable.id, userIds)); // Transform to your output format with full type safety const result = data.map(({ value }) => { const user = users.find((u) => u.id === value); return { id: value, name: user?.name || "Unknown User", email: user?.email || "", avatarUrl: user?.avatar_url || null, status: user?.status || "pending", }; }); return result;};// Define your service plugin with type parametersexport const userBadgeServerPlugin: DataTypeServicePlugin< UserBadgeInput, UserBadgeOutput> = { id: "user-badge-server", // Unique server plugin ID type: PluginTypes.SERVICE, // Plugin type name: "User Badge Server", // Human-readable name description: "Fetches and transforms user data for badge display", supportedDataTypes: ["user-badge"], // Data types this plugin handles transformer: transformUserBadge, // Your transform function metadata: { version: "1.0.0", author: "Your Team", },};
DataTypePlugin (Client-Side)
The client plugin renders the formatted data from the server with type-safe value handling.
The client plugin needs the following methods:
renderCell
: renders the value of a column within a tabular viewrenderInput
: renders the input for editing/creating the value of the columnrenderField
: renders the value of the column when viewing the full recordrenderConfig
: renders the configuration of the field when editing its metadata. Allows you to add configuration to the column.
// packages/plugins/user-badge/src/client/user-badge-client.tsximport type { DataTypePlugin } from '@kit/plugins/client';import { PluginTypes } from '@kit/plugins/shared';// Import the output type from your server pluginimport type { UserBadgeOutput } from '../server/user-badge-server';// Define your client plugin with the output typeexport const userBadgeDataTypePlugin: DataTypePlugin<UserBadgeOutput> = { id: 'user-badge', // Unique client plugin ID type: PluginTypes.DATA_TYPE, // Plugin type name: 'User Badge', // Human-readable name for UI description: 'Displays user information as a badge with status', compatibleTypes: ['uuid', 'text'], // PostgreSQL column types this works with dataType: { label: 'User Badge', // Label shown in settings UI value: 'user-badge', // Internal value (matches server supportedDataTypes) }, servicePluginId: 'user-badge-server', // Links to server plugin // Render in table cells - value is type-safe! renderCell: ({ value }) => { if (!value) return <span className="text-muted-foreground">-</span>; // TypeScript knows value is UserBadgeOutput return ( <div className="flex items-center gap-2"> {value.avatarUrl && ( <img src={value.avatarUrl} alt={value.name} className="w-6 h-6 rounded-full" /> )} <div className="flex items-center gap-1"> <span>{value.name}</span> <StatusIndicator status={value.status} /> </div> </div> ); }, // Render in detail view - value is type-safe! renderField: ({ value }) => { if (!value) return <div className="text-muted-foreground">No user data</div>; return ( <div className="p-4 border rounded-lg"> <div className="flex items-center gap-3"> {value.avatarUrl && ( <img src={value.avatarUrl} alt={value.name} className="w-12 h-12 rounded-full" /> )} <div> <div className="font-semibold">{value.name}</div> <div className="text-sm text-muted-foreground">{value.email}</div> <div className="flex items-center gap-1 mt-1"> <StatusIndicator status={value.status} /> <span className="text-xs capitalize">{value.status}</span> </div> </div> </div> </div> ); }, // Form input for editing - handles raw string values renderInput: ({ field }) => { // In forms, we work with raw IDs (strings), not transformed objects return ( <div className="space-y-2"> <input type="text" value={typeof field.value === 'string' ? field.value : field.value?.id || ''} onChange={(e) => field.onChange(e.target.value)} onBlur={field.onBlur} placeholder="Enter User ID" className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm" /> <p className="text-xs text-muted-foreground"> Enter a valid user ID </p> </div> ); }, metadata: { version: '1.0.0', author: 'Your Team', },};// Helper component for status indicatorconst StatusIndicator = ({ status }: { status: string }) => { const colors = { active: 'bg-green-500', inactive: 'bg-gray-400', pending: 'bg-yellow-500', }; return ( <div className={`w-2 h-2 rounded-full ${colors[status as keyof typeof colors] || colors.pending}`} /> );};
Registration Process
Step 1: Register Plugin Instances
Server Registration
In the apps/api/src/plugins/index.ts
file, register the plugin with type safety:
// apps/api/src/plugins/index.tsimport { servicePluginRegistry } from "@kit/plugins/server";import { userBadgeServerPlugin } from "@kit/plugins-user-badge/server";export function registerPlugins() { // Register with full type inference servicePluginRegistry.register(userBadgeServerPlugin);}
Client Registration
Next, register the plugin in the client app:
// apps/app/src/plugins/index.tsimport { clientPluginRegistry } from "@kit/plugins/client";import { userBadgeDataTypePlugin } from "@kit/plugins-user-badge/client";export function registerPlugins() { // Register with full type inference clientPluginRegistry.register(userBadgeDataTypePlugin);}
Type Safety Benefits
The updated plugin system provides several type safety benefits:
1. Input/Output Type Validation
// TypeScript ensures your transformer returns the correct typeconst transformer: ColumnDataTransformer<InputType, OutputType> = async (data) => { // data is InputType[] const result: OutputType[] = /* transform logic */; return result; // Must return OutputType[]};
2. Type-Safe Rendering
// In your render functions, TypeScript knows the exact typerenderCell: ({ value }) => { // value is UserBadgeOutput - no type assertions needed! return <span>{value.name}</span>; // TypeScript validates property access};
3. Generic Plugin Support
If you don't need type safety, you can omit type parameters:
// Plugin without specific types - uses 'unknown'export const myPlugin: DataTypeServicePlugin = { // ... plugin definition};
Complete Working Example
Here's how the complete user badge plugin works with type safety:
1. Column Configuration
Configure a column to use your plugin:
- Navigate to Settings → Resources → Your Table
- Select the user ID column
- Change "UI Data Type" to "User Badge"
- Save configuration
2. Type-Safe Data Flow
Database Column: user_id (string) ↓[{value: 'uuid1'}, {value: 'uuid2'}, {value: 'uuid3'}] ← Type: UserBadgeInput[] ↓ServicePlugin.transformer() ← Batch fetch and transform ↓[{id: 'uuid1', name: 'John', email: 'john@example.com', status: 'active'}, ...] ← Type: UserBadgeOutput[] ↓DataTypePlugin.renderCell() ← Type-safe rendering ↓<div>👤 John ✅</div> ← Final UI with full type safety
Best Practices
Define Clear Input/Output Types
// ✅ GOOD - Clear, specific typestype ProductInput = { value: string };type ProductOutput = { id: string; name: string; price: { amount: number; currency: string };};// ❌ BAD - Using 'any' or unclear typestype ProductData = any;
Handle Edge Cases
renderCell: ({ value }) => { // Always handle null/undefined if (!value) return <span className="text-muted-foreground">-</span>; // Type-safe property access return <div>{value.name}</div>;};
Advanced Features
Configuration Support
Add configuration options to your plugins:
// Server plugin with configurationexport const configurablePlugin: DataTypeServicePlugin<Input, Output> = { // ... other properties configSchema: { showEmail: { type: "boolean", default: true }, statusColors: { type: "object", default: { active: "green", inactive: "gray" }, }, }, transformer: async (data, context) => { const config = context.config as PluginConfig; // Use config.showEmail, config.statusColors, etc. },};
Registration Issues
- Ensure plugins are registered before use
- Check that servicePluginId matches server plugin ID
- Verify supportedDataTypes matches dataType.value