Oakbase Column Type System
Learn how to construct, test, and publish custom sandboxed column types and widgets using @nexus/column-types.
Your First Column Type Walkthrough
Step-by-step instructions for authoring a production-ready custom column plugin.
1 Install Dependencies
Install the official column types contract and schema validation library:
pnpm add @nexus/column-types zod react
2 Define Column Contract with defineColumnType
Export a default definition wrapping your column with defineColumnType. This provides compile-time type inference for your schemas, components, and serializers:
import { defineColumnType } from "@nexus/column-types";
import { z } from "zod";
import React from "react";
// 1. Define schemas
export const CurrencyConfigSchema = z.object({
currency: z.enum(["USD", "EUR", "GBP", "JPY"]).default("USD"),
decimals: z.number().int().min(0).max(4).default(2),
});
export const CurrencyValueSchema = z.number().nullable();
export type CurrencyConfig = z.infer<typeof CurrencyConfigSchema>;
export type CurrencyValue = z.infer<typeof CurrencyValueSchema>;
// 2. Export default ColumnTypeDefinition
export default defineColumnType<CurrencyConfig, CurrencyValue>({
id: "acme.currency",
category: "number",
configSchema: CurrencyConfigSchema,
valueSchema: CurrencyValueSchema,
defaultValue: null,
// Render static preview when cell is unfocused
CellRenderer: function CurrencyRenderer({ value, config }) {
if (value === null || value === undefined) {
return <span className="text-nexus-text-muted italic">—</span>;
}
const curr = config?.currency || "USD";
const formatted = new Intl.NumberFormat("en-US", {
style: "currency",
currency: curr,
maximumFractionDigits: config?.decimals ?? 2,
}).format(value);
return <span className="font-mono text-xs text-nexus-text">{formatted}</span>;
},
// Mounts inside isolated sandbox only when editing
CellEditor: function CurrencyEditor({ value, onSave, onCancel }) {
const [draft, setDraft] = React.useState(value !== null ? String(value) : "");
return (
<input
type="number"
autoFocus
value={draft}
onChange={(e) => setDraft(e.target.value)}
onBlur={() => onSave(draft ? Number(draft) : null)}
onKeyDown={(e) => {
if (e.key === "Enter") onSave(draft ? Number(draft) : null);
if (e.key === "Escape") onCancel();
}}
className="w-full px-2 py-1 bg-nexus-bg text-nexus-text border border-nexus-accent rounded text-xs font-mono"
/>
);
},
serialize: (val) => val,
deserialize: (raw) => (typeof raw === "number" ? raw : null),
getSearchableText: (val) => (val !== null ? String(val) : ""),
});3 Prepare Manifest and Subresource Integrity (SRI) Hash
Create a manifest.json and compute the SHA-384 digest of your bundle:
{
"id": "acme.currency",
"name": "Currency Column",
"version": "1.0.0",
"description": "Formatted monetary values with multi-currency support",
"author": {
"name": "Acme Software",
"url": "https://acme.example.com"
},
"entryPoint": "https://cdn.example.com/acme-currency.bundle.js",
"integrity": "sha384-xyz...",
"sandbox": "iframe",
"permissions": ["row:read", "row:write"],
"configSchema": {},
"pricing": {
"type": "free"
}
}The Focus/Preview Rendering Model
Why Oakbase uses an on-demand iframe mounting lifecycle and how to design for it.
On large boards virtualizing hundreds or thousands of visible cells, mounting a dedicated <iframe> element per cell creates severe browser memory bloat, catastrophic layout recalculations, and unacceptable frame drops.
When a cell is not actively being edited, Oakbase renders it as pure read-only HTML via CellRenderer or static text via getSearchableText. Zero iframes or workers are active for dormant cells.
The sandboxed iframe mounts ONLY for the single focused cell when clicked by the user. On blur or save, the result is saved back to the engine, and the iframe unmounts immediately.
Author Rule: Do not author a plugin assuming persistent state stored in the iframe’s DOM or local window. All state must be serialized into the cell value or row data contract.
The Permission Model & Sandboxes
Every plugin declares granular capabilities in its manifest. Adhere to the principle of least privilege.
| Permission | Scope | Description & Granted Capabilities |
|---|---|---|
| row:read | Active Row | Grants read access to the values and metadata of the current row containing the cell. |
| row:write | Active Cell / Row | Grants write permissions to update the cell’s value and emit mutations to the board engine. |
| board:read | Board Context | Grants access to read column headers, board metadata, and other column schemas. |
| network | Outbound HTTPS | Grants permission to call external HTTP/HTTPS endpoints via fetch(). Under heavy scrutiny during review queue evaluation. |
The Official Review Checklist
Every plugin is evaluated against these 5 criteria by human reviewers prior to approval.
Do not request network or board:read if your column only operates on its own values.
Cells must render clean static HTML without requiring an interactive iframe to show their state.
Never throw runtime exceptions when rendered with null or undefined.
Utilize CSS variables provided by Oakbase or adhere to neutral contrast guidelines for both dark and light modes.
Your author name and support website must be active, reachable, and provide accurate contact information.
API Reference: @nexus/column-types
TypeScript types and contracts exported by the column types package.
defineColumnType<TConfig, TValue>(def: ColumnTypeDefinition<TConfig, TValue>)
Identity function providing strict TypeScript contract inference for column plugins.
type ColumnTypeDefinition<TConfig, TValue>
Core shape required for all column definitions:
export type ColumnTypeDefinition<TConfig = unknown, TValue = unknown> = {
id: string; // Unique reverse-DNS identifier (e.g. acme.formula)
category: ColumnCategory; // 'text' | 'number' | 'date' | 'link' | 'select' | 'computed' | 'custom'
configSchema: ZodSchema<TConfig>; // Schema validating per-column settings
valueSchema: ZodSchema<TValue>; // Schema validating cell values
defaultValue: TValue; // Initial default value for empty cells
// UI Components
CellRenderer: ComponentType<CellRendererProps<TValue>>; // Lightweight preview rendered when unfocused
CellEditor: ComponentType<CellEditorProps<TValue>>; // Interactive editor mounted on focus
HeaderRenderer?: ComponentType<HeaderRendererProps>; // Optional custom header bar component
FilterRenderer?: ComponentType<FilterRendererProps<TValue>>; // Optional column filter component
// Serialization
serialize: (value: TValue) => unknown; // Serializes cell value to raw JSON
deserialize: (raw: unknown) => TValue; // Hydrates cell value from raw JSON
getSearchableText?: (value: TValue) => string; // Text representation for search index
};{
value: TValue;
config?: any;
columnType: string;
isEditing?: boolean;
}{
value: TValue;
config?: any;
onSave: (val: TValue) => void;
onCancel: () => void;
}