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/com.debba.tabularis/plugins/ - Windows:
%APPDATA%\com.debba.tabularis\plugins\
- Linux:
- It reads the
manifest.jsonfor each plugin 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/
├── manifest.json
└── duckdb-plugin (or duckdb-plugin.exe on Windows)
The manifest tells Tabularis everything about your plugin.
JSON Schema available:
plugins/manifest.schema.json— add"$schema": "./manifest.schema.json"to your manifest for IDE autocompletion and validation.
{
"$schema": "https://tabularis.dev/schemas/plugin-manifest.json",
"id": "duckdb",
"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 |
|---|---|---|
id |
string | Unique driver identifier (lowercase, no spaces). Must match the folder name. |
name |
string | Display name shown in the UI (e.g., "DuckDB"). |
version |
string | Plugin version (semver). |
description |
string | Short description shown in the plugins list. |
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). |
| 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. |
routines |
bool | true if the database supports stored procedures/functions. |
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. |
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. |
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 |
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.json:
{
"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 |
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.
// 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.
Each component receives context (slot-specific data) and pluginId as props:
// src/FieldPreview.tsx
import { usePluginConnection } from "@tabularis/plugin-api";
export default function FieldPreview({ context, pluginId }: {
context: Record<string, unknown>;
pluginId: string;
}) {
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>
);
}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/
├── manifest.json
├── 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,
"column_default": null,
"is_primary_key": true,
"is_auto_increment": true,
"comment": null
}
]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.
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"
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 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:
{
"tables": [{ "name": "users", "schema": "public", "comment": null }],
"columns": { "users": [ /* column list */ ] },
"foreign_keys": { "users": [ /* 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/com.debba.tabularis/plugins/myplugin/ - Windows:
%APPDATA%\com.debba.tabularis\plugins\myplugin\
- Linux:
- Place your
manifest.jsonand 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
manifest.jsoninto a.zipfile. - Create a GitHub Release (or host on another URL) with the ZIP files.
- Open a pull request to this repository adding your plugin entry to
plugins/registry.json.
See README.md for the full registry.json format.
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.