New to plugins? Start with
PLUGIN_TUTORIAL.md— a 20-minute walkthrough that ends with a working Google Sheets driver. This document is a reference, not a tutorial.📚 The canonical, browsable version of the plugin docs lives at tabularis.dev/wiki/plugins.
Tabularis supports extending its capabilities via a JSON-RPC based external plugin system. By building a standalone executable that implements the JSON-RPC interface, you can add support for virtually any SQL or NoSQL database (such as DuckDB, MongoDB, etc.) using the programming language of your choice.
This guide details how to implement and register a custom external plugin.
An external plugin in Tabularis is a separate executable (binary or script) that runs alongside the main application. Tabularis communicates with the plugin using JSON-RPC 2.0 over standard input/output (stdin / stdout).
- Requests: Tabularis writes JSON-RPC request objects to the plugin's
stdin, separated by a newline (\n). - Responses: The plugin processes the request and writes a JSON-RPC response object to its
stdout, followed by a newline (\n). - Logging: Any output to
stderrfrom the plugin is inherited/logged by Tabularis without interfering with the JSON-RPC communication.
- Tabularis discovers plugins in its data folder at startup:
- Linux:
~/.local/share/tabularis/plugins/ - macOS:
~/Library/Application Support/tabularis/plugins/ - Windows:
%APPDATA%\tabularis\plugins\
- Linux:
- It reads each plugin's
.tabulariummanifest (or a legacymanifest.json) to discover its capabilities and data types. - The plugin is registered as a driver and appears in the "Database Type" list.
- When the user opens a connection using the plugin's driver, Tabularis spawns the executable and begins sending JSON-RPC messages.
- The same process instance is reused for all operations in that session.
A Tabularis plugin is distributed as a .zip file. When extracted into the plugins folder, it must have the following structure:
plugins/
└── duckdb/
├── .tabularium (or legacy manifest.json)
└── duckdb-plugin (or duckdb-plugin.exe on Windows)
One manifest tells Tabularis everything about your plugin — and, when you publish, tells the Tabularium registry how to list it. Its canonical name is .tabularium; the host still reads a legacy manifest.json as a fallback. In a .tabularium, name is the lowercase slug that identifies the plugin (legacy manifests may keep a separate id and a display name). When publishing, the registry resolves the manifest from your release assets — upload .tabularium as a standalone asset (GitHub silently renames the dotfile to default.tabularium; the registry accepts both names).
JSON Schema available: point
$schemaat the registry's live merged schema (as below) for IDE autocompletion and validation, or at the localplugins/manifest.schema.jsonfor legacymanifest.jsonfiles.
{
"$schema": "https://registry.tabularis.dev/manifest.schema.json?kind=driver",
"name": "duckdb",
"version": "1.0.0",
"description": "DuckDB file-based analytical database",
"default_port": null,
"executable": "duckdb-plugin",
"capabilities": {
"schemas": false,
"views": true,
"routines": false,
"file_based": true,
"connection_string": false,
"identifier_quote": "\"",
"alter_primary_key": false
},
"data_types": [
{
"name": "INTEGER",
"category": "numeric",
"requires_length": false,
"requires_precision": false
},
{
"name": "VARCHAR",
"category": "string",
"requires_length": true,
"requires_precision": false
}
]
}| Field | Type | Description |
|---|---|---|
name |
string | Lowercase slug identifying the plugin (e.g., "duckdb"). Must match the folder name and the registry pattern ^[a-z][a-z0-9-]*$; it becomes the registry slug and is pinned at first submit. |
id |
string | Legacy identifier from manifest.json-era plugins. Optional — when absent, identity falls back to name. Omit in new .tabularium manifests; the registry ignores it. |
version |
string | Plugin version (semver, no leading v). Must equal the release tag with any v prefix stripped — the registry rejects tag/manifest mismatches. |
description |
string | Short description shown in the plugins list. Optional for the registry; max 280 chars. |
default_port |
number | null | Default TCP port. Use null for file-based databases. |
executable |
string | Relative path to the executable inside the plugin folder. |
capabilities |
object | Feature flags (see below). |
data_types |
array | List of supported data types (see below). |
type_mappings |
object | null | Optional map of generic inferred type names to driver-specific types. Used during paste/import to map generic types (e.g. DATETIME) to driver-native equivalents (e.g. TIMESTAMP). See Type Mappings below. |
| Flag | Type | Description |
|---|---|---|
schemas |
bool | true if the database supports named schemas (like PostgreSQL). Controls whether the schema selector is shown in the UI. |
views |
bool | true if the database supports views. Enables the views section in the explorer. |
materialized_views |
bool | true if the database supports materialized views. Enables the materialized views section in the explorer (see Materialized Views). Defaults to false. |
routines |
bool | true if the database supports stored procedures/functions. |
triggers |
bool | true if the database supports triggers. Enables trigger-related UI for drivers that implement the trigger RPCs. |
file_based |
bool | true for local file databases (e.g., SQLite, DuckDB). Replaces host/port with a file path input in the connection form. |
folder_based |
bool | true for plugins that connect to a directory rather than a single file (e.g. CSV plugin). Replaces host/port with a folder picker. |
no_connection_required |
bool | true for API-based plugins that need no host, port, or credentials (e.g. a public REST API). Hides the entire connection form — the user only fills in the connection name. |
connection_string |
bool | Set false to hide the connection string import UI for this driver. Defaults to true for network drivers. file_based and folder_based drivers skip the import UI automatically regardless of this flag. |
connection_string_example |
string | Optional placeholder example shown in the connection string import field (e.g. "clickhouse://user:pass@localhost:9000/db"). Also accepted as camelCase connectionStringExample. |
identifier_quote |
string | Character used to quote SQL identifiers. Use "\"" for ANSI standard or "`" for MySQL style. |
sql_dialect |
string | Optional statement-splitting dialect: postgres, mysql, mssql, sqlite, oracle, or generic. Oracle-like plugins, including DM/Dameng, should use "oracle". |
alter_primary_key |
bool | true if the database supports altering primary keys after table creation. |
manage_tables |
bool | true to enable table and column management UI (Create Table, Add/Modify/Drop Column, Drop Table). Does not control index or FK operations. Defaults to true. |
readonly |
bool | When true, the driver is read-only: all data modification operations (INSERT, UPDATE, DELETE) are disabled in the UI. The add/delete row buttons, inline cell editing, and context menu edit actions are hidden. Table and column management is also hidden regardless of manage_tables. Defaults to false. |
explain |
bool | true if the driver implements the explain_query method (EXPLAIN / query plan support). Enables the Visual EXPLAIN button in the SQL editor and notebook cells; when false or omitted, the Visual EXPLAIN UI is hidden for connections using this driver. Defaults to false. |
supports_ssl |
bool | true to show the SSL/TLS configuration tab (mode + CA/client cert/key) in the connection modal. The values are forwarded to the plugin as ssl_mode, ssl_ca, ssl_cert, and ssl_key in ConnectionParams. Network drivers only. Also accepted as camelCase supportsSsl. Defaults to false. |
Each entry in data_types describes a type the driver supports for column creation in the UI:
| Field | Type | Description |
|---|---|---|
name |
string | SQL type name (e.g., "VARCHAR", "BIGINT"). |
category |
string | UI grouping category (see below). |
requires_length |
bool | true if this type requires a length argument (e.g., VARCHAR(255)). |
requires_precision |
bool | true if this type requires a precision/scale argument (e.g., DECIMAL(10,2)). |
default_length |
string? | Optional default length pre-filled in the UI (e.g., "255" for VARCHAR). |
Type Categories:
| Category | Examples |
|---|---|
numeric |
INTEGER, BIGINT, DECIMAL, FLOAT, DOUBLE |
string |
VARCHAR, TEXT, CHAR |
date |
DATE, TIME, TIMESTAMP, DATETIME |
binary |
BLOB, BYTEA, VARBINARY |
json |
JSON, JSONB |
spatial |
GEOMETRY, POINT, POLYGON |
other |
BOOLEAN, UUID |
The optional type_mappings field lets your plugin declare how generic inferred type names should be mapped to driver-specific types. This is used during paste/import operations where Tabularis infers column types from data (e.g., detects a date column as DATETIME) and needs to translate that to a driver-native type.
Since map_inferred_type() is a synchronous trait method that cannot issue an RPC call, the mapping is declared statically in the manifest and resolved by the host at lookup time.
Example for a PostgreSQL plugin:
{
"type_mappings": {
"DATETIME": "TIMESTAMP",
"JSON": "JSONB"
}
}Rules:
- Keys are uppercase generic type names (the inferred type before mapping)
- Values are the driver-native equivalents to use instead
- Lookup is case-insensitive (the host uppercases the input before matching)
- If a type has no mapping, it passes through unchanged
- If
type_mappingsis omitted or empty, all types pass through unchanged
Plugins can declare custom configuration fields that Tabularis renders in the Settings → gear icon modal for that plugin. Users fill in the values, Tabularis persists them in config.json, and passes them to the plugin at startup via an initialize RPC call.
Add an optional settings array at the top level of your manifest:
{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"description": "A custom plugin with settings",
"executable": "my-plugin",
"capabilities": { ... },
"data_types": [ ... ],
"settings": [
{
"key": "api_key",
"label": "API Key",
"type": "string",
"required": true,
"description": "Your API key for authentication."
},
{
"key": "region",
"label": "Region",
"type": "select",
"options": ["us-east-1", "eu-west-1", "ap-southeast-1"],
"default": "us-east-1",
"description": "Deployment region."
},
{
"key": "max_connections",
"label": "Max Connections",
"type": "number",
"default": 10
},
{
"key": "ssl",
"label": "Enable SSL",
"type": "boolean",
"default": true
}
]
}| Field | Type | Required | Description |
|---|---|---|---|
key |
string | yes | Unique identifier used as the key in the settings map. |
label |
string | yes | Human-readable label shown in the UI. |
type |
string | yes | One of: "string", "boolean", "number", "select". |
default |
any | no | Default value pre-filled when no saved value exists. |
description |
string | no | Optional hint displayed below the field. |
required |
boolean | no | If true, saving the modal is blocked until the field is filled. |
options |
string[] | no | For "select" type: the list of choices shown in the dropdown. |
Immediately after spawning the plugin process, Tabularis sends an initialize call:
{
"jsonrpc": "2.0",
"method": "initialize",
"params": {
"settings": {
"api_key": "abc123",
"region": "eu-west-1",
"max_connections": 10,
"ssl": true
}
},
"id": 1
}- The
settingsobject contains only the keys the user has configured (merged with defaults). - Returning an error response is safe — Tabularis silently ignores any
initializefailure. - Plugins that do not implement
initializeare unaffected (the error is ignored). - Use
initializeto store settings in your plugin's state before any query arrives.
"initialize" => {
let settings = ¶ms["settings"];
// Store settings in your plugin state, e.g.:
// API_KEY.set(settings["api_key"].as_str().unwrap_or("").to_string());
json!({
"jsonrpc": "2.0",
"result": null,
"id": id
})
}elif method == "initialize":
settings = params.get("settings", {})
# Store settings for later use:
# api_key = settings.get("api_key", "")
send_response({"result": None, "id": req_id})Plugins can inject custom React components into the host UI through a slot-based extension system. This is entirely optional — plugins without UI extensions continue to work as before.
Add an optional ui_extensions array to your manifest:
{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"ui_extensions": [
{
"slot": "row-editor-sidebar.field.after",
"module": "ui/field-preview.js",
"order": 50
},
{
"slot": "data-grid.toolbar.actions",
"module": "ui/export-button.js"
},
{
"slot": "settings.plugin.before_settings",
"module": "ui/auth-panel.js",
"driver": "my-plugin"
}
]
}| Field | Type | Required | Description |
|---|---|---|---|
slot |
string | yes | Target slot name (see table below). |
module |
string | yes | Relative path to the pre-built IIFE JavaScript bundle inside the plugin folder. |
order |
number | no | Sort order within the slot. Lower values render first. Default: 100. |
driver |
string | no | If set, the contribution is only active when the active connection's driver matches this value. Useful for plugins that should only appear for their own driver. |
| Slot Name | Location | Context Data | Use Cases |
|---|---|---|---|
row-edit-modal.field.after |
After each field in New Row modal | connectionId, tableName, schema, driver, columnName, rowData, isInsertion |
Validation hints, field previews |
row-edit-modal.footer.before |
Before Save/Cancel in New Row modal | connectionId, tableName, schema, driver, rowData, isInsertion |
Batch actions, templates |
row-editor-sidebar.field.after |
After each field in Row Editor sidebar | connectionId, tableName, schema, driver, columnName, rowData, rowIndex |
Field-level previews, lookups |
row-editor-sidebar.header.actions |
Sidebar header action buttons | connectionId, tableName, schema, driver, rowData, rowIndex |
"Copy as JSON", audit links |
data-grid.toolbar.actions |
Table toolbar (right side) | connectionId, tableName, schema, driver |
Export buttons, analysis tools |
data-grid.context-menu.items |
Right-click context menu on grid rows | connectionId, tableName, schema, driver, columnName, rowIndex, rowData |
Row-level custom actions |
sidebar.footer.actions |
Explorer sidebar footer | connectionId, driver |
Status indicators, quick actions |
settings.plugin.actions |
Per-plugin actions in Settings modal | targetPluginId |
Diagnostics, re-auth buttons |
settings.plugin.before_settings |
Content above plugin settings form | targetPluginId |
OAuth panels, status banners |
connection-modal.connection_content |
Inside the connection form | driver |
Custom connection fields |
connection-modal.extra_fields |
Below host/port in the connection form | driver, extra, setExtraField |
Plugin-specific connection fields (e.g. AWS region) |
Every slot component receives a context object with the fields listed above. The available fields depend on the slot — for example, rowData is only present for row-level slots. All fields are optional.
interface SlotContext {
connectionId?: string | null;
tableName?: string | null;
schema?: string | null;
driver?: string | null;
rowData?: Record<string, unknown>;
columnName?: string;
rowIndex?: number;
isInsertion?: boolean;
targetPluginId?: string;
}Plugin UI components must be pre-built as IIFE bundles (Immediately Invoked Function Expression). The host provides React, ReactJSXRuntime, and the plugin API as globals — your bundle must not bundle its own copies of these.
The @tabularis/plugin-api package gives you TypeScript types, hook signatures, and the defineSlot helper for slot-aware type inference. Install it as a dev dependency — at runtime the host injects the real implementation, so the package itself ships as thin stubs.
npm install --save-dev @tabularis/plugin-api
# or: pnpm add -D @tabularis/plugin-apiThen react and @tabularis/plugin-api remain Vite externals in your build config — the installed package is used only for types and autocomplete in your editor.
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
build: {
lib: {
entry: "src/MyComponent.tsx",
formats: ["iife"],
name: "__tabularis_plugin__",
fileName: () => "ui/my-component.js",
},
rollupOptions: {
external: ["react", "react/jsx-runtime", "@tabularis/plugin-api"],
output: {
globals: {
react: "React",
"react/jsx-runtime": "ReactJSXRuntime",
"@tabularis/plugin-api": "__TABULARIS_API__",
},
},
},
},
});Key points:
- The
namefield must be"__tabularis_plugin__"— the host looks for this global.- The component must be the default export of the entry file.
- Multiple slots can reference the same
modulefile.
Recommended: use defineSlot for typed context. The helper infers the exact context shape for the slot you target, so fields like context.columnName are non-nullable where the host guarantees them:
// src/FieldPreview.tsx
import { defineSlot, usePluginConnection } from "@tabularis/plugin-api";
const FieldPreview = defineSlot(
"row-editor-sidebar.field.after",
({ context }) => {
const { driver } = usePluginConnection();
if (context.columnName !== "geometry") return null;
return (
<div style={{ padding: "4px 0", fontSize: "11px", color: "#888" }}>
Geometry preview for {String(context.rowData[context.columnName])}
</div>
);
},
);
// The loader expects a default-exported React component.
export default FieldPreview.component;Legacy form (no typed context). Older bundles used the loose SlotComponentProps shape. Still supported; new plugins should prefer defineSlot.
import { usePluginConnection } from "@tabularis/plugin-api";
import type { SlotComponentProps } from "@tabularis/plugin-api";
export default function FieldPreview({ context }: SlotComponentProps) {
const { driver } = usePluginConnection();
if (context.columnName !== "geometry") return null;
return <div>Geometry preview for {String(context.rowData?.[context.columnName!])}</div>;
}Slot components can import these hooks from @tabularis/plugin-api:
| Hook | Returns | Purpose |
|---|---|---|
usePluginQuery() |
(query: string) => Promise<{ columns, rows }> |
Execute read-only queries on the active connection |
usePluginConnection() |
{ connectionId, driver, schema } |
Access active connection metadata |
usePluginToast() |
{ showInfo(), showError(), showWarning() } |
Show toast notifications |
usePluginModal() |
{ openModal(options), closeModal() } |
Open host-managed modals with custom content |
usePluginSetting(pluginId) |
{ getSetting(key), setSetting(key, value) } |
Read/write plugin settings |
usePluginTheme() |
{ themeId, themeName, isDark, colors } |
Access current theme info |
usePluginTranslation(pluginId) |
t(key) |
Access plugin-specific i18n translations |
openUrl(url) |
Promise<void> |
Open a URL in the system browser |
usePluginModal() lets you open a host-managed modal from within a slot component:
const { openModal, closeModal } = usePluginModal();
openModal({
title: "OAuth Setup",
content: <MyOAuthForm onDone={closeModal} />,
size: "md", // "sm" | "md" | "lg" | "xl"
});Plugins can ship locale files at locales/{lang}.json inside their plugin folder. The host loads them automatically and registers them under the plugin's namespace.
my-plugin/
├── .tabularium
├── my-plugin-binary
├── locales/
│ ├── en.json
│ └── it.json
└── ui/
└── my-component.js
Use usePluginTranslation("my-plugin") in components to access translations via t("key").
You can control when a contribution appears using two mechanisms:
driverfield in manifest: Set"driver": "my-plugin"to only render when the active connection uses that driver.- Component-level filtering: Return
nullfrom your component based oncontextvalues.
export default function PostgresOnly({ context }: SlotComponentProps) {
// Only render for PostgreSQL connections
if (context.driver !== "postgres") return null;
return <div>PostgreSQL-specific action</div>;
}Plugin components must not:
- Import from
@tauri-apps/*directly - Access
window.__TAURI__or invoke Tauri commands - Manipulate the DOM outside their subtree
All host interaction goes through @tabularis/plugin-api.
Each contribution is wrapped in a SlotErrorBoundary. If your component throws, a small error badge is shown instead — other plugins and the host continue working normally.
For the full specification, see plugin-ui-extensions-spec.md.
Your plugin must run an event loop that:
- Reads one JSON line from
stdin. - Parses the JSON-RPC request.
- Executes the requested database operation.
- Writes a JSON-RPC response to
stdoutfollowed by\n.
{
"jsonrpc": "2.0",
"method": "get_tables",
"params": {
"params": {
"driver": "duckdb",
"host": null,
"port": null,
"database": "/path/to/my_database.duckdb",
"username": null,
"password": null,
"ssl_mode": null
},
"schema": null
},
"id": 1
}The params.params object is a ConnectionParams — the same values the user entered in the connection form. The top-level params may contain additional method-specific fields (e.g., schema, table, column_name, etc.).
{
"jsonrpc": "2.0",
"result": [
{ "name": "users", "schema": "main", "comment": null }
],
"id": 1
}{
"jsonrpc": "2.0",
"error": {
"code": -32603,
"message": "Database file not found or inaccessible."
},
"id": 1
}Standard JSON-RPC error codes:
| Code | Meaning |
|---|---|
-32700 |
Parse error |
-32600 |
Invalid request |
-32601 |
Method not found |
-32602 |
Invalid params |
-32603 |
Internal error |
Your plugin must respond to the following JSON-RPC methods. For unsupported features, return an empty array [] or a -32601 (Method not found) error.
Test whether a connection can be established.
Params: { "params": ConnectionParams }
Result: { "success": true } or an error response.
Lightweight health check called periodically (every N seconds, configurable) on active connections. If Tabularis does not receive a successful response after 2 consecutive attempts, the connection is considered dead and automatically disconnected.
Params: { "params": ConnectionParams }
Result: null (or any value) on success, or an error response if the connection is no longer alive.
If your plugin does not implement
ping, Tabularis falls back to callingtest_connectioninstead. Implementingpingis recommended for plugins that can perform a cheaper connectivity check than a fulltest_connection(e.g. reusing an existing connection/session rather than opening a new one).
List available databases.
Params: { "params": ConnectionParams }
Result: ["db1", "db2"]
List schemas within the current database.
Params: { "params": ConnectionParams }
Result: ["public", "private"]
Return
[]ifcapabilities.schemasisfalse.
List tables in a schema/database.
Params: { "params": ConnectionParams, "schema": string | null }
Result:
[
{ "name": "users", "schema": "public", "comment": "User accounts" }
]Get column information for a table.
Params: { "params": ConnectionParams, "schema": string | null, "table": string }
Result:
[
{
"name": "id",
"data_type": "INTEGER",
"is_nullable": false,
"default_value": null,
"is_pk": true,
"is_auto_increment": true,
"comment": null
}
]JSON / JSONB columns: Set
data_typeto"JSON"or"JSONB"(matched case-insensitively) to make Tabularis render the cell with syntax highlighting and expose the JSON editor window. Inexecute_queryrow data, send the cell as either a native JSON value (object/array/scalar) or a JSON-formatted string — both are accepted. For text-typed columns that hold JSON, end users can opt in per connection via the Detect JSON in text columns setting; no plugin change required.
Get foreign key relationships for a table.
Params: { "params": ConnectionParams, "schema": string | null, "table": string }
Result:
[
{
"constraint_name": "fk_user_id",
"column_name": "user_id",
"referenced_table": "users",
"referenced_column": "id",
"on_update": "CASCADE",
"on_delete": "SET NULL"
}
]Get indexes for a table.
Params: { "params": ConnectionParams, "schema": string | null, "table": string }
Result:
[
{
"index_name": "idx_email",
"columns": ["email"],
"is_unique": true,
"is_primary": false
}
]List views in a schema/database.
Params: { "params": ConnectionParams, "schema": string | null }
Result: [{ "name": "active_users", "schema": "public" }]
Get the SQL definition of a view.
Params: { "params": ConnectionParams, "schema": string | null, "view": string }
Result: "SELECT * FROM users WHERE active = true"
Get column information for a view.
Params: { "params": ConnectionParams, "schema": string | null, "view": string }
Result: Same structure as get_columns.
Create a new view.
Params: { "params": ConnectionParams, "schema": string | null, "name": string, "definition": string }
Result: null on success, or an error.
Replace or modify an existing view.
Params: { "params": ConnectionParams, "schema": string | null, "name": string, "definition": string }
Result: null on success, or an error.
Drop a view.
Params: { "params": ConnectionParams, "schema": string | null, "name": string }
Result: null on success, or an error.
These methods are optional. Declare materialized_views: true in capabilities to enable the UI. If your plugin returns -32601 (method not found), the host falls back to empty results for get_materialized_views and get_materialized_view_columns; get_materialized_view_definition and refresh_materialized_view surface a "not supported by this driver" error instead.
List materialized views in a schema.
Params: { "params": ConnectionParams, "schema": string | null }
Result: [{ "name": string, "schema": string | null }]
Get columns of a materialized view.
Params: { "params": ConnectionParams, "view_name": string, "schema": string | null }
Result: [TableColumn] (same shape as get_columns)
Get the SQL definition of a materialized view.
Params: { "params": ConnectionParams, "view_name": string, "schema": string | null }
Result: string (the SQL definition)
Refresh a materialized view's data.
Params: { "params": ConnectionParams, "view_name": string, "schema": string | null }
Result: null on success, or an error.
List stored procedures and functions.
Params: { "params": ConnectionParams, "schema": string | null }
Result:
[
{ "name": "calculate_total", "routine_type": "FUNCTION", "schema": "public" }
]Get parameters of a stored routine.
Params: { "params": ConnectionParams, "schema": string | null, "routine": string }
Result:
[
{ "name": "p_user_id", "data_type": "INTEGER", "mode": "IN" }
]Get the SQL body of a stored routine.
Params: { "params": ConnectionParams, "schema": string | null, "routine": string }
Result: "BEGIN ... END"
Set capabilities.triggers to true when your driver implements the trigger RPCs. Tabularis uses this flag to show trigger-related UI.
List triggers in a schema/database.
Params: { "params": ConnectionParams, "schema": string | null }
Result:
[
{
"name": "users_audit_trg",
"table_name": "users",
"event": "INSERT OR UPDATE",
"timing": "AFTER",
"definition": "CREATE TRIGGER users_audit_trg ..."
}
]Get the SQL definition of a trigger.
Params: { "params": ConnectionParams, "schema": string | null, "trigger_name": string, "table_name": string }
Result: "CREATE TRIGGER users_audit_trg ..."
Create a trigger from SQL generated by the UI or entered in raw SQL mode.
Params: { "params": ConnectionParams, "schema": string | null, "trigger_sql": string }
Result: null on success, or an error.
Drop a trigger.
Params: { "params": ConnectionParams, "schema": string | null, "trigger_name": string, "table_name": string }
Result: null on success, or an error.
Execute a SQL query and return results.
Params:
{
"params": ConnectionParams,
"query": "SELECT * FROM users",
"page": 1,
"page_size": 100
}Result:
{
"columns": ["id", "name", "email"],
"rows": [
[1, "Alice", "alice@example.com"]
],
"total_count": 1,
"execution_time_ms": 12
}Insert a new row into a table.
Params:
{
"params": ConnectionParams,
"schema": null,
"table": "users",
"data": { "name": "Bob", "email": "bob@example.com" }
}Result: null on success, or an error.
Update a single field in a row.
Params:
{
"params": ConnectionParams,
"schema": null,
"table": "users",
"pk_col": "id",
"pk_val": 42,
"col_name": "name",
"new_val": "Robert"
}Result: Number of affected rows (e.g. 1), or an error.
Delete a row from a table.
Params:
{
"params": ConnectionParams,
"schema": null,
"table": "users",
"pk_col": "id",
"pk_val": 42
}Result: Number of affected rows (e.g. 1), or an error.
These methods are optional. If your plugin returns -32601 (method not found), the host shows "BLOB export/preview not supported" to the user.
Save a binary column value to a file on disk. The plugin receives the file path and is responsible for writing the file directly (since the plugin runs on the same machine as the host).
Params:
{
"params": ConnectionParams,
"table": "documents",
"col_name": "content",
"pk_map": { "id": 42 },
"schema": "public",
"file_path": "/tmp/export.pdf"
}Result: null on success, or an error.
Contract: The plugin must:
- Query the binary column value using the PK map to identify the row
- Write the raw bytes to
file_path - Return
nullon success or an error string on failure
Fetch a binary column value and return it as a displayable string (for preview in the row editor).
Params:
{
"params": ConnectionParams,
"table": "images",
"col_name": "data",
"pk_map": { "id": 7 },
"schema": "public"
}Result: A string in the BLOB wire format: "BLOB:<size_bytes>:<mime_type>:<base64_data>", or an error.
Example response:
{ "result": "BLOB:1024:image/png:iVBORw0KGgo..." }AI Query Assist obtains metadata through the active database driver. Plugins
that already implement get_tables, get_columns, and get_foreign_keys work
without changes: Tabularis calls those standard methods, limits the result, and
formats the provider-agnostic system prompt in the host.
Plugins may optionally implement get_ai_schema_context to replace those
per-table calls with a database-specific batch query. Return JSON-RPC error
-32601 when the method is not implemented; Tabularis then uses the standard
metadata fallback automatically.
Params:
{
"params": ConnectionParams,
"schema": "public",
"max_tables": 20
}Result:
{
"tables": [
{
"name": "users",
"columns": [ /* standard get_columns entries */ ],
"foreign_keys": [ /* standard get_foreign_keys entries */ ]
}
],
"total_table_count": 42
}The plugin must respect max_tables, but total_table_count reports the
number of tables before truncation. Plugins return structured metadata only;
the host owns prompt wording, identifier quoting, and provider dispatch.
These methods are used to build ER diagrams efficiently by loading all metadata in one call.
Return the complete schema structure (tables + columns + foreign keys).
Params: { "params": ConnectionParams, "schema": string | null }
Result:
[
{
"name": "users",
"columns": [ /* column list */ ],
"foreign_keys": [ /* FK list */ ]
}
]Return columns for all tables at once.
Params: { "params": ConnectionParams, "schema": string | null, "tables": ["users", "orders"] }
Result: { "users": [ /* columns */ ], "orders": [ /* columns */ ] }
Return foreign keys for all tables at once.
Params: { "params": ConnectionParams, "schema": string | null, "tables": ["users", "orders"] }
Result: { "users": [ /* FKs */ ], "orders": [ /* FKs */ ] }
These methods generate SQL statements. Tabularis may display the SQL to the user before executing it.
Params: { "params": ConnectionParams, "schema": string | null, "table": string }
Result: "CREATE TABLE users (...)"
Params: { "params": ConnectionParams, "schema": string | null, "table": string, "column": ColumnDefinition }
Result: "ALTER TABLE users ADD COLUMN ..."
Params: { "params": ConnectionParams, "schema": string | null, "table": string, "column": ColumnDefinition }
Result: "ALTER TABLE users MODIFY COLUMN ..."
Params: { "params": ConnectionParams, "schema": string | null, "table": string, "index": IndexDefinition }
Result: "CREATE INDEX idx_email ON users(email)"
Params: { "params": ConnectionParams, "schema": string | null, "table": string, "fk": ForeignKeyDefinition }
Result: "ALTER TABLE orders ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id)"
Params: { "params": ConnectionParams, "schema": string | null, "table": string, "index_name": string }
Result: null on success, or an error.
Params: { "params": ConnectionParams, "schema": string | null, "table": string, "constraint_name": string }
Result: null on success, or an error.
Here is a minimal but functional skeleton for a plugin executable in Rust.
use std::io::{self, BufRead, Write};
use serde_json::{json, Value};
fn main() {
let stdin = io::stdin();
let mut stdout = io::stdout();
for line in stdin.lock().lines() {
let line = line.unwrap();
if line.trim().is_empty() {
continue;
}
let req: Value = serde_json::from_str(&line).unwrap_or_else(|_| {
// Ignore unparseable lines
return Value::Null;
});
if req.is_null() {
continue;
}
let id = req["id"].clone();
let method = req["method"].as_str().unwrap_or("");
let params = &req["params"];
let response = dispatch(method, params, id);
let mut res_str = serde_json::to_string(&response).unwrap();
res_str.push('\n');
stdout.write_all(res_str.as_bytes()).unwrap();
stdout.flush().unwrap();
}
}
fn dispatch(method: &str, params: &Value, id: Value) -> Value {
match method {
"test_connection" => json!({
"jsonrpc": "2.0",
"result": { "success": true },
"id": id
}),
// Optional: lightweight health check (called periodically).
// If omitted, Tabularis falls back to test_connection.
"ping" => json!({
"jsonrpc": "2.0",
"result": null,
"id": id
}),
"get_databases" => json!({
"jsonrpc": "2.0",
"result": ["my_database"],
"id": id
}),
"get_schemas" => json!({
"jsonrpc": "2.0",
"result": [],
"id": id
}),
"get_tables" => {
// Connect to the database using params["params"]["database"], etc.
json!({
"jsonrpc": "2.0",
"result": [
{ "name": "example_table", "schema": null, "comment": null }
],
"id": id
})
},
"execute_query" => {
let query = params["query"].as_str().unwrap_or("");
// Execute query and return results
json!({
"jsonrpc": "2.0",
"result": {
"columns": ["id", "name"],
"rows": [[1, "Alice"]],
"total_count": 1,
"execution_time_ms": 5
},
"id": id
})
},
_ => json!({
"jsonrpc": "2.0",
"error": {
"code": -32601,
"message": format!("Method '{}' not implemented", method)
},
"id": id
}),
}
}Add serde_json to your Cargo.toml:
[dependencies]
serde_json = "1"You can test your plugin directly by piping JSON-RPC messages:
echo '{"jsonrpc":"2.0","method":"get_tables","params":{"params":{"driver":"duckdb","database":"/tmp/test.duckdb"},"schema":null},"id":1}' \
| ./duckdb-pluginYou should see a valid JSON-RPC response on stdout.
- Create the plugin directory in Tabularis's data folder:
- Linux:
~/.local/share/tabularis/plugins/myplugin/ - macOS:
~/Library/Application Support/tabularis/plugins/myplugin/ - Windows:
%APPDATA%\tabularis\plugins\myplugin\
- Linux:
- Place your
.tabularium(or legacymanifest.json) and the compiled executable in that directory. - On Linux/macOS, make the executable runnable:
chmod +x myplugin - Restart Tabularis (or install via Settings to hot-reload without restart).
- Open Settings → Installed Plugins — your driver should appear.
- Try creating a new connection using your driver from the connection form.
To make your plugin available in the official registry:
- Build release binaries for all supported platforms.
- Package each platform binary with your
.tabulariuminto a.zipfile (the scaffolded release workflow does this). - Create a GitHub Release with the ZIP files attached — plus
.tabulariumas a standalone asset (GitHub renames it todefault.tabularium; the registry accepts both). The registry resolves the manifest from release assets, and the tag stripped ofvmust equal the manifestversion. - Submit your plugin at registry.tabularis.dev/submit — ownership is verified via OAuth against your linked repository, and CI can pre-validate the manifest via
POST /api/manifest/validate. A manifest that fails validation is rejected with HTTP 422 — there is no silent fallback. The registry's plugin development page documents every.tabulariumfield; the full author docs live at docs.tabularium.wiki. - Installs are verified client-side: Tabularis checks the registry's JWS signature and the download's SHA-256 before anything runs.
The legacy path — a pull request adding an entry to plugins/registry.json (format in README.md) — still works during the transition; new plugins should submit to the registry directly.
Note:
min_tabularis_versionis specified per-release inside thereleases[]array, not at the root plugin level. This allows older Tabularis installs to install an older compatible release even when a newer release requires a higher app version.