• Blog
  • Documentation
  • Courses
  • Changelog
  • AI Starters
  • UI Kit
  • FAQ
  • Supamode
    New
  • Pricing

Launch your next SaaS in record time with Makerkit, a React SaaS Boilerplate for Next.js and Supabase.

Makerkit is a product of Makerkit Pte Ltd (registered in the Republic of Singapore)Company Registration No: 202407149CFor support or inquiries, please contact us

About
  • FAQ
  • Contact
  • Verify your Discord
  • Consultation
  • Open Source
  • Become an Affiliate
Product
  • Documentation
  • Blog
  • Changelog
  • UI Blocks
  • Figma UI Kit
  • AI SaaS Starters
License
  • Activate License
  • Upgrade License
  • Invite Member
Legal
  • Terms of License
    • Branding
    • Data Explorer API
    • Storage Explorer API
    • Users API
    • Audit Logs API
    • Writing Packages
    • Writing Plugins

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 API is a work in progress

This feature is still in development and not yet in the main branch, 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:

text
┌─────────────────┐ 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:

bash
# From project root
turbo gen plugin

This creates a new package structure:

text
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.

bash
pnpm add "@kit/plugins-user-status@workspace:*" --filter @kit/app
pnpm 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:

typescript
// packages/plugins/user-badge/src/server/user-badge-server.ts
import type {
DataTypeServicePlugin,
ColumnDataTransformer,
} from "@kit/plugins/server";
import { PluginTypes } from "@kit/plugins/shared";
// Define your input type - what comes from the database
type UserBadgeInput = {
value: string; // User ID from the database column
};
// Define your output type - what your plugin returns
type UserBadgeOutput = {
id: string;
name: string;
email: string;
avatarUrl: string | null;
status: "active" | "inactive" | "pending";
};
// Transform function with type-safe input and output
const 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 parameters
export 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 view
  • renderInput: renders the input for editing/creating the value of the column
  • renderField: renders the value of the column when viewing the full record
  • renderConfig: renders the configuration of the field when editing its metadata. Allows you to add configuration to the column.
typescript
// packages/plugins/user-badge/src/client/user-badge-client.tsx
import type { DataTypePlugin } from '@kit/plugins/client';
import { PluginTypes } from '@kit/plugins/shared';
// Import the output type from your server plugin
import type { UserBadgeOutput } from '../server/user-badge-server';
// Define your client plugin with the output type
export 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 indicator
const 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:

typescript
// apps/api/src/plugins/index.ts
import { 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:

typescript
// apps/app/src/plugins/index.ts
import { 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
// TypeScript ensures your transformer returns the correct type
const transformer: ColumnDataTransformer<InputType, OutputType> = async (data) => {
// data is InputType[]
const result: OutputType[] = /* transform logic */;
return result; // Must return OutputType[]
};

2. Type-Safe Rendering

typescript
// In your render functions, TypeScript knows the exact type
renderCell: ({ 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:

typescript
// 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:

  1. Navigate to Settings → Resources → Your Table
  2. Select the user ID column
  3. Change "UI Data Type" to "User Badge"
  4. Save configuration

2. Type-Safe Data Flow

text
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

typescript
// ✅ GOOD - Clear, specific types
type ProductInput = { value: string };
type ProductOutput = {
id: string;
name: string;
price: { amount: number; currency: string };
};
// ❌ BAD - Using 'any' or unclear types
type ProductData = any;

Handle Edge Cases

typescript
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:

typescript
// Server plugin with configuration
export 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

  1. Ensure plugins are registered before use
  2. Check that servicePluginId matches server plugin ID
  3. Verify supportedDataTypes matches dataType.value
On this page
  1. Plugin Architecture Overview
    1. Type Safety in Plugins
      1. Creating a Plugin with Turbo Generator
        1. Step 1: Generate Plugin Package
        2. Step 2: Add Package to your apps
      2. Plugin Implementation
        1. ServicePlugin (Server-Side)
        2. DataTypePlugin (Client-Side)
      3. Registration Process
        1. Step 1: Register Plugin Instances
      4. Type Safety Benefits
        1. 1. Input/Output Type Validation
        2. 2. Type-Safe Rendering
        3. 3. Generic Plugin Support
      5. Complete Working Example
        1. 1. Column Configuration
        2. 2. Type-Safe Data Flow
      6. Best Practices
        1. Define Clear Input/Output Types
        2. Handle Edge Cases
      7. Advanced Features
        1. Configuration Support
        2. Registration Issues