DocsColumns & Plugins
Developer Reference & Guides

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.

Critical Performance & Cost Constraint: No Persistent Iframes

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.

1. Unfocused State (Static Preview)

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.

2. Focused / Editing State (Mounted Sandbox)

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.

PermissionScopeDescription & Granted Capabilities
row:readActive RowGrants read access to the values and metadata of the current row containing the cell.
row:writeActive Cell / RowGrants write permissions to update the cell’s value and emit mutations to the board engine.
board:readBoard ContextGrants access to read column headers, board metadata, and other column schemas.
networkOutbound HTTPSGrants 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.

1. Declares only permissions it uses

Do not request network or board:read if your column only operates on its own values.

2. Works read-only when unfocused

Cells must render clean static HTML without requiring an interactive iframe to show their state.

3. Handles null / missing value safely

Never throw runtime exceptions when rendered with null or undefined.

4. Respects theme tokens & styles

Utilize CSS variables provided by Oakbase or adhere to neutral contrast guidelines for both dark and light modes.

5. Real and reachable author details

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
};
CellRendererProps<TValue>
{
  value: TValue;
  config?: any;
  columnType: string;
  isEditing?: boolean;
}
CellEditorProps<TValue>
{
  value: TValue;
  config?: any;
  onSave: (val: TValue) => void;
  onCancel: () => void;
}