Develop and Submit DBX Plugins
DBX plugins are independently installed .dbxp packages that can contribute connection types, workbenches, filesystems, and native sidecars. Plugin source, the DBX plugin platform, and the official Marketplace catalog are owned by different repositories.
Recommended path: create a project with dbx-plugin create, develop and publish unsigned candidates from the plugin's own repository, then let dbx-store create or update the candidate PR. A maintainer reviews and signs the candidates before they enter the official catalog.
Marketplace listing pull requests go to t8y2/dbx-store, not t8y2/dbx. A plugin's feature code normally remains in its author's source repository. Open a PR against t8y2/dbx only when changing the DBX host, SDK, CLI, protocol, schemas, documentation, or official examples.
Plugin Shortcuts
Manage shortcuts in Plugin Center → Settings → Global settings. They are enabled by default at the top right. Choose the top or bottom of either side, below the connection tree, or a floating strip before Update. The floating strip shows 3 icons by default, configurable from 0 to 10; remaining entries appear in a dropdown, one icon and name per row. Limited space moves additional entries into the dropdown.
Below the connection tree, Plugin settings sits at the top right of the Plugins header. Other positions include it as a fixed entry at the end by default. It opens the Plugin Center settings page directly and cannot be reordered. When the floating toolbar overflows, this entry is last in its dropdown. Disable Show plugin settings shortcut in global settings to hide it; you can enable it again from Plugin Center → Settings. When no plugin shortcuts are visible, the entire shortcut area, including the settings entry, is hidden and takes up no layout space.
Each function has its own draggable icon. Visibility switches apply to a whole plugin. All positions share the saved order. Hiding or uninstalling a plugin retains its recorded positions, so the same entries return to those positions; unrecorded entries follow saved entries. Entry identity consists of the plugin ID, entry kind, and function ID. Changing these IDs creates a new entry.
Drag previews follow the cursor and adjust their direction and position near window edges. Right-docked shortcuts prefer the left side of the cursor; long names are truncated to the available width.
Choose Beside Plugin Center (dropdown) to show all shortcuts as icon-and-name rows in the menu beside the Plugin Center button. Rows support drag sorting; the floating toolbar icon count does not apply here.
Below the connection tree, shortcuts form a single-column list with an icon on the left and a name on the right, using the tree’s row style and font size. Automatic height fits one entry per row up to five rows, with vertical scrolling for remaining entries. Drag the divider to save a custom height; double-click it to restore automatic height. Escape, window blur, or releasing outside the sorting area cancels sorting. Failed saves show an error and roll back.
Shortcuts honor declared appToolbar visibility and command enablement. Standalone plugins also expose existing workbenches, preferring a matching command when available. Legacy plugins without commands can expose filesystem entries. Connection plugins only expose explicitly declared global commands; the host does not invent workbench entries without connection context. Incompatible plugins are excluded.
Position affects icons only: content retains its existing panel/tab presentation. Hiding shortcuts, changing position, or hiding a plugin does not close running sessions. A panel shortcut toggles only the matching selected command panel and restores that same instance. No Manifest or backend protocol changes are required. Legacy left/right preferences migrate to the top of the corresponding side.
Which Repository Receives the Change
| Change | Destination | Official DBX PR? |
|---|---|---|
| Your plugin frontend, Rust/Go backend, tests, and release scripts | The plugin's source repository | Do not submit it to dbx; develop and release it in its own repository |
| A new Marketplace listing, version update, icon, or description | t8y2/dbx-store | Yes, target main |
| DBX plugin host, Manifest/Marketplace schemas, SDK, CLI, or packager | t8y2/dbx | Yes, target main |
| Official examples or plugin-development documentation | t8y2/dbx | Yes |
| A bug in a specific third-party plugin | That plugin's source repository | Do not submit it to dbx-store |
| A custom or enterprise private repository | Your private repository | No official Marketplace submission is required |
| A vendor JDBC driver JAR | Do not commit it to either repository | Users import it locally or on the server |
DBX-maintained plugins that need an independent release cadence should normally use separate source repositories as well. Only tightly coupled host examples, SDK verification projects, or components explicitly accepted by maintainers belong in t8y2/dbx.
Plugin Project Layout
my-plugin/
├── manifest.json
├── dbx-plugin.toml
├── assets/
├── ui/
├── backend/ # absent from the frontend template
├── .github/workflows/
│ └── plugin-release.yml
└── dist/ # generated; do not commitmanifest.jsondeclares identity, version, permissions, entrypoints, and contributions.ui/runs in a sandboxed workbench and talks to DBX through Host APIs.backend/is an optional native sidecar. Rust and Go SDKs are provided, while other languages may implement the DBX JSON-RPC protocol directly..dbxpis an installable artifact, not the source repository.- Frontend-only plugins can publish one
universalpackage. Native sidecars require packages for each operating-system and CPU target.
Complete Manifest Reference
manifest.json is the runtime contract and must be at the root of the plugin package. Manifest v1 rejects undeclared fields. Reference the repository's plugins/manifest.schema.json from your editor. A minimal frontend-only Manifest looks like this:
{
"$schema": "https://raw.githubusercontent.com/t8y2/dbx/main/plugins/manifest.schema.json",
"manifest_version": 1,
"id": "com.example.files",
"name": "Example Files",
"version": "0.1.0",
"publisher": "example",
"description": "Browse files from an example service.",
"icon": "assets/plugin.svg",
"source": "https://github.com/example/dbx-plugin-files",
"homepage": "https://github.com/example/dbx-plugin-files",
"engines": { "host_api": "1" },
"permissions": [],
"entrypoints": { "ui": { "root": "ui", "entry": "ui/index.html" } },
"contributions": [
{ "type": "workbench", "id": "com.example.files.main", "label": "Files", "icon": "assets/plugin.svg" }
]
}Manifest paths are relative to the package root. They cannot start with /, contain .., backslashes, or duplicate slashes. ui.entry must be inside ui.root: with root ui, use ui/index.html, not index.html. signingKeyId belongs to Marketplace artifact metadata, not the Manifest.
| Field | Required | Meaning |
|---|---|---|
$schema | No | JSON Schema URL for editor support; it does not establish identity. |
manifest_version | Yes | Must currently be the number 1. |
id | Yes | Stable global ID using lowercase letters, digits, ., _, and -, such as io.github.example.files; never change it after publishing. |
name | Yes | Default display name. |
icon | No | Relative path to the default package icon; SVG or PNG is recommended. |
version | Yes | Semantic version major.minor.patch, optionally with prerelease/build suffixes. Official packages cannot overwrite the same ID and version. |
publisher | Yes | Publisher identifier; keep it stable after the first listing. |
description | No | Plugin description. |
source | No | HTTPS source repository URL, also shown from installed-plugin details. |
homepage | No | Project, documentation, or support URL. |
engines | Yes | Compatibility declaration; it must contain host_api. |
permissions | No | Minimal capability set, described below. |
entrypoints | No | UI and native-sidecar entrypoints. |
contributions | No | Connection, workbench, filesystem, connection-menu, result-view, command/menu, and MCP tool-surface declarations. |
localizations | No | Locale-specific names, descriptions, fields, and action labels. |
Engines, Permissions, and Entrypoints
{
"engines": { "dbx": ">=0.6.0", "host_api": "1" },
"permissions": [
"host.workbench",
"host.events",
"host.filesystem",
"host.binary",
"host.plans:read",
"host.schema:read",
"host.storage",
"host.clipboard:read",
"host.data:read",
"host.network:https://s3.example.com:443"
],
"entrypoints": {
"ui": { "root": "ui", "entry": "ui/index.html" },
"backend": {
"protocol_versions": [1],
"transport": "stdio-jsonl",
"executable": "bin/linux-x64/backend"
}
}
}engines.dbxis an optional DBX version range;engines.host_apiis required.host.workbenchopens another workbench,host.eventsreceives host events,host.binaryenables binary channels, andhost.filesystemenables filesystem entrypoints.host.plans:readallows reading estimated execution plans for DBX's own connections (see Estimated Execution Plans). It is read-only: it never grants SQL execution, writes, DDL, or actual plans, and it is the only plan scope that exists.host.schema:readallowshost.getTableMetadatato read narrow table schema metadata from an already-open connection; it grants no arbitrary SQL, writes, or implicit reconnect (see Table Schema Metadata).host.storagegives the workbench UI a persistent key-value store scoped to this plugin (see Persistent UI State). Values never leave the plugin's own data directory.host.ailets a plugin use the global DBX AI panel for Ask snapshot conversations, optional Agent conversations backed by live plugin tools, and workbench-specific quick-question recommendations. Older DBX builds reject the unknown permission at install time, so plugins that must install everywhere should treathost.aias optional and checkcapabilities.ai/capabilities.aiRecommendationsat runtime.host.clipboard:readallows reading the system clipboard from the workbench UI (see System Clipboard). Clipboard writes need no permission; reads hand user data (passwords, tokens) to plugin code, so they are gated.host.data:readallowswindow.dbxPlugin.queryDatato run one read-only SQL statement on a connection the user granted to the plugin (see Read-only Data Queries). Every connection needs the user's consent; it never grants writes, DDL, or reconnects.host.network:https://host[:port]declares a browser-accessible HTTPS origin added to CSPconnect-src; at most eight origins are allowed, with no paths, wildcards, or tokens. The service's CORS rules still apply. This permission is not a native Sidecar network firewall.backend.transportdefaults tostdio-jsonl. Usestdio-framedfor binary frames and declarehost.binaryas well.backend.executable,ui.root, andui.entrymust point to package files. Native packages need a matching executable for every target.
Connection Providers
A connection provider declares a form; DBX renders the form and owns the connection lifecycle, while the plugin handles its protocol. binding gives a field its meaning: config is persisted as external configuration, secret goes to the Secret Store, and name, host, port, username, password, and database map to standard connection fields.
{
"type": "connection-provider",
"id": "com.example.files.connection",
"label": "Example Service",
"icon": "assets/connection.svg",
"database_type": "example-files",
"description": "Connect to an Example Service.",
"fields": [
{ "key": "display_name", "label": "Name", "type": "text", "binding": "name", "required": true },
{ "key": "endpoint", "label": "Endpoint", "type": "text", "binding": "host", "required": true },
{ "key": "port", "label": "Port", "type": "number", "binding": "port", "default": 443 },
{ "key": "region", "label": "Region", "type": "radio", "options": [{ "label": "US", "value": "us" }, { "label": "EU", "value": "eu" }], "binding": "config" },
{ "key": "token", "label": "Access token", "type": "password", "binding": "secret", "required": true }
],
"workbench": "com.example.files.main",
"capabilities": ["test", "connect", "disconnect"],
"actions": [
{ "id": "refresh", "label": "Refresh metadata", "variant": "outline", "when": "edit", "requires_valid_form": true, "timeout_ms": 30000 }
]
}Field types are text, password, number, boolean, select, radio, and textarea. select and radio require non-empty options, each with a label and string value. binding: "port" requires number. Never put passwords, tokens, or private keys in config, workbench context, events, or logs.
Fields require key, label, and type; optional fields are description, placeholder, required, default, options, and binding. Defaults must match the field type. A password field without an explicit binding defaults to secret. database_type is a plugin-defined identifier, not an extension to DBX's built-in database enum.
Fields can also be conditional. visible_when and required_when take a clause { "field": "mode", "one_of": ["custom"] } — matching when the sibling holds one of the listed values, compared as strings so [false] and ["false"] both match a boolean false — and compose clauses with all_of, any_of, and not:
{
"key": "sudo_command",
"label": "Sudo command",
"type": "text",
"visible_when": {
"all_of": [
{ "field": "sudo_source", "one_of": ["custom"] },
{ "field": "read_only", "one_of": [false] }
]
}
}Conditions cascade: while the field a clause reads is hidden, the clause does not count, so a hidden option's stored default never surfaces a grandchild field. DBX evaluates the same expression for the dialog and for save/test/connect validation, an unset or empty value never matches, and composite trees are capped at 8 levels and 64 nodes.
Text, password, and textarea fields can offer a local file action — for example an SSH private key or a keystore — with picker:
{
"key": "private_key_path",
"label": "Private key path",
"type": "text",
"binding": "config",
"picker": { "kind": "file", "accept": [".pem", ".key"], "content_field": "private_key" }
}The desktop app opens a native picker and stores the chosen absolute path, which the plugin backend (same machine) can read. The browser build cannot resolve a client path, so the action becomes an upload: DBX reads the file and stores its content in the declared content_field sibling, and clears the path field — picking a path clears the uploaded content and vice versa, so the two alternatives never disagree. kind also accepts directory (desktop only), accept takes up to 16 extension or MIME filters, and uploads are capped at 1 MiB.
When a plugin needs the user mid-flight — a bastion's keyboard-interactive MFA code, a host-key confirmation, a choice of account — it can call the Host API method host/requestUserInput (Host API 1.1) and receive the typed answer. The request goes through the same blocking dialog DBX uses for its own prompts, including during connection/test and connection/connect:
{
"jsonrpc": "2.0",
"id": "prompt-1",
"method": "host/requestUserInput",
"params": { "prompt": "Verification code (6 digits)", "title": "JumpServer login", "echo": false, "timeoutSecs": 300 }
}The result is { "action": "submit", "value": "123456" }, { "action": "cancel" }, or { "action": "timeout" }; only submit carries a value and the plugin must fail closed on the other two. Plugin-initiated requests use string ids while DBX keeps numeric ids, and DBX pauses the request deadline of the call waiting on a prompt, so a user typing a code is not mistaken for a connect timeout. An error code of -32001 means no user interface is attached (headless/MCP), -32602 invalid params, -32601 unsupported method — degrade gracefully in every case. Gate the call on plugin/initialize advertising hostApiVersion 1.1.0 (or host.requestUserInput in host.features). With the Rust SDK that is HostClient::supports("host/requestUserInput") plus HostClient::request_user_input(&UserInputPrompt::secret("Verification code")) from dbx_plugin_sdk::host_client().
The fixed lifecycle methods are connection/test, connection/connect, and connection/disconnect; custom form actions use connection/action. Parameters include provider, connection, and runtime, plus action: { id } for actions. Keep sessions keyed by connection.id and connect to runtime.host/runtime.port, which include DBX tunnel/proxy resolution. Hydrated secrets are provided only to backend lifecycle requests; the frontend uses connectionId to access an established session.
Multi-endpoint protocols (Kafka advertised.listeners, cluster discovery) declare proxy_route on the contribution. With transport layers configured, DBX then delivers a SOCKS5 route in the payload instead of a static forward, keeping runtime.host/runtime.port at the logical endpoint while the plugin dials every advertised endpoint through runtime.proxy ({ "type": "socks5", "host": "...", "port": 1080, "username": "...", "password": "..." }; an SSH final hop exposes its dynamic SOCKS5 endpoint, a SOCKS5 proxy layer is used directly, and proxy credentials ride the same encrypted channel as connection secrets and must never be logged). Without the flag, transport layers keep the static-tunnel behavior, which requires a single remote endpoint from the standard host/port fields; DBX rejects plugin connections that would tunnel to an empty endpoint instead of timing out silently.
Action when accepts always, create, or edit; variant accepts default, outline, secondary, destructive, or ghost; timeout_ms is 1–120000. requires_valid_form requires a complete form and close_on_success closes the dialog on success. Return { "success": true, "message": "...", "fieldValues": { "port": 443 } } to update declared fields; success: false is a failure.
Other Contributions
| Type | Required fields | Purpose |
|---|---|---|
workbench | type, id, label | Registers a workbench opened from the sidebar or plugin entry. |
filesystem-provider | type, id, label, schemes | Registers a virtual filesystem; root_uri can be s3://bucket, with optional read, write, delete, rename, and mkdir capabilities. |
context-menu | type, id, label, menu: "connection" or "table", optional action | Adds a backend action or opens a declared Workbench from a Sidebar Tree context menu. |
result-view | type, id, label | Registers a query or task result view. |
A menu: "table" contribution is currently exposed when the user opens a context menu on a concrete table node in the Sidebar Tree. A legacy contribution without action invokes the required backend entrypoint as contextMenu/<contribution-id> with the table identity:
{
"table": {
"connectionId": "connection-id",
"database": "example",
"schema": "public",
"table": "users"
}
}database and schema are optional and are omitted when unavailable. The context contains no credential, connection string, or raw connection configuration. Object Browser can reuse this identity contract in a later contribution surface, but it is not a table menu surface in this release.
A context-menu contribution can instead declare a host-handled Workbench action:
{
"type": "context-menu",
"id": "vendor.example.generate",
"label": "Generate test data",
"menu": "table",
"action": { "type": "open-workbench", "workbench": "vendor.example.main" }
}action.workbench must reference a workbench contribution in the same plugin. The host opens it directly, without invoking the sidecar, and passes the current invocation context as Workbench context. For menu: "table", this is the TableContext object above directly (not the backend { "table": ... } envelope); for menu: "connection", it is the existing non-secret connection summary (id, dbType, name, database). The host may also provide the standard connectionId for tab association. No host, port, username, password, connection string, or raw connection configuration is exposed. Reopening the Workbench refreshes its context with the latest invocation.
Only legacy context-menu contributions without action require a backend entrypoint; their { "message": "..." } response continues to show a toast.
Reference contributions by ID: a connection provider's workbench must point to a declared workbench and filesystem_provider to a declared filesystem provider. A contribution icon takes precedence over the plugin-level icon.
Choose workbench for a custom file browser: the plugin owns its list, virtualization, previews, context menus, and split panes. Choose filesystem_provider only to use DBX's generic file manager; declaring both defaults to the workbench. A legacy context-menu invokes contextMenu/<contribution-id> on the backend, while a declarative open-workbench action opens its same-plugin Workbench directly; result-view receives a result snapshot in workbench context, and the opened contribution id arrives in the dbx-plugin-init payload so one UI entrypoint can serve several workbenches and result views. See the full contribution protocol for payloads.
Filesystem Protocol
Implement these backend methods when using the DBX generic file manager. A custom workbench can use them through its own RPC calls as well; declaring a contribution does not implement business logic.
| Method | Parameters and return values |
|---|---|
filesystem/list | providerId, optional connectionId, uri, optional cursor, limit; returns { entries, nextCursor? }. |
filesystem/read | The same identity fields, uri, maxBytes; returns { dataBase64, contentType?, truncated, etag? }. |
filesystem/write | uri, dataBase64, create, overwrite, optional etag; requires write. |
filesystem/createDirectory | uri; requires mkdir. |
filesystem/delete | uri, recursive; requires delete. |
filesystem/rename | sourceUri, targetUri, overwrite; requires rename. |
Every method carries providerId and optional connectionId. Entries contain a single filename name, full uri, kind (file, directory, symlink, or other), and optional size, modifiedAt, and contentType. Names cannot include / or \\, or equal ./..; put full paths in uri. Omit nextCursor on the last page and never repeat a consumed cursor. Image previews need the actual MIME type and base64 bytes, not UTF-8-decoded binary content.
Mutations return { success, message?, entry? }; inline read/write payloads are capped at 4 MiB. Use a chunked transfer protocol with progress/cancellation for large files, not one giant JSON/base64 value.
Localization
Use locale keys such as zh-CN and en in localizations. You can override the plugin name, description, and every contribution's labels, descriptions, fields, and actions. This does not translate text inside the plugin UI.
{
"localizations": {
"zh-CN": {
"name": "示例文件",
"contributions": {
"com.example.files.connection": {
"label": "示例服务",
"fields": { "token": { "label": "访问令牌" } }
}
}
}
}
}Workbench Context Contract
Workbench context is a JSON data snapshot across the DBX/plugin boundary. The host recursively removes Vue reactivity and sends an independent copy, so plugins must not expect Vue refs, proxies, DOM nodes, functions, component instances, or credentials.
Context may contain null, booleans, finite numbers, strings, arrays, and plain objects. Undefined object properties are omitted and undefined array entries become null. Dates, Maps, Sets, symbols, bigints, non-finite numbers, circular references, and custom class instances are rejected with an error. The UTF-8 encoded context is limited to 2 MiB.
Connection provider fields using binding: "config" are persisted in external_config; fields using binding: "secret" are persisted in the Secret Store. Plugin connection configuration keeps external_config through create, edit, save, and reconnect flows.
Frontend Host API
The UI runs in an isolated iframe. DBX exposes the bridge as window.dbxPlugin; do not import DBX Vue/Tauri modules or assume that Node.js, local files, or arbitrary network access are available.
The following custom objects/list and objects/changed methods must be implemented by your backend. The asset example assumes the package contains ui/assets/empty-state.svg:
await window.dbxPlugin.ready;
const context = window.dbxPlugin.context;
const result = await window.dbxPlugin.invoke("objects/list", { prefix: "docs/" }, { timeoutMs: 30000 });
const off = window.dbxPlugin.onContext((nextContext) => render(nextContext));
await window.dbxPlugin.notify("objects/changed", { count: result.items.length });
const assetUrl = await window.dbxPlugin.readAssetUrl("assets/empty-state.svg");| API | Purpose |
|---|---|
ready | Waits for Host initialization; start application logic after await dbxPlugin.ready. |
context / onContext(fn) | Reads or observes the current workbench context. |
locale | Reads the current DBX locale; the plugin translates its own UI. |
theme | Reads appearance and DBX design tokens for light/dark support. |
request(method, params) | Calls a Host API method, including host.getContext and ui.readAsset. |
invoke(method, params, { timeoutMs }) | Sends an RPC request to the plugin's Sidecar. |
notify(method, params) | Sends a notification without a business result. |
sendBinary(channel, data) / onBinary(fn) | Sends or receives binary data; requires framed transport and host.binary. |
readAsset(path) / readAssetUrl(path) | Reads a packaged asset within the plugin resource root. |
openWorkbench(id, context) | Opens another workbench from this plugin; requires host.workbench. |
executeCommand(commandId, context?) | Executes one of this plugin's own declared commands through the same path as a menu placement — re-checks enablement, applies singleton reuse, and follows the declared presentation (panel commands dock, tab commands open tabs). context merges over the command context and scopes instance_key {{path}} placeholders, so instance_key: "logs:{{connectionId}}" yields one panel instance per connection. Resolves { error } for expected outcomes; requires host.workbench. |
openFilesystem(id, context) | Opens a filesystem entry; requires host.filesystem. |
getPlanCapabilities(connectionId) | Reads what the host and this connection can plan; requires host.plans:read. |
explainPlan({ connectionId, database?, schema?, sql, mode, timeoutMs? }) | Returns the estimated plan for sql; requires host.plans:read. |
getTableMetadata({ connectionId, database?, schema?, table }) | Reads narrow table schema metadata from an already-open connection; requires host.schema:read. |
queryData({ connectionId, database?, schema?, sql, maxRows?, timeoutMs? }) | Runs one read-only SQL statement on a connection the user granted to the plugin; requires host.data:read. |
capabilities | { downloadFile, planApi, schemaMetadataApi, dataApi, storage, ai, aiRecommendations, clipboardWrite, clipboardRead } from the init message. A missing or false entry means that Host API group is unavailable here, so gate the matching call on it instead of probing with a request. |
ai.openConversation({ title, prompt, context, send?, mode? }) | Opens a new plugin conversation in the built-in AI panel; send defaults to false and mode defaults to ask. mode: "agent" uses live plugin tools when context includes an open plugin connection; requires host.ai and is advertised as capabilities.ai. |
ai.setRecommendations({ context, items }) | Replaces the current workbench's default recommendations and refreshes the global AI panel; up to five items are shown and {{path.to.value}} placeholders are supported; requires host.ai and capabilities.aiRecommendations. |
ai.clearRecommendations() | Clears recommendations for the current workbench; requires host.ai and capabilities.aiRecommendations. |
onInit(fn) / onEvent(fn) | Observes initialization, environment changes, or backend events; forwarding backend events requires host.events. |
storage | Persistent per-plugin key-value state; requires host.storage; see below. |
fileTransfer | Streams local files, downloads, and (desktop) OS drops; see below. |
clipboard | System clipboard access from the sandbox; see below. |
Failed calls reject their Promise; show a useful error and offer retry. Host methods, parameters, and results are versioned API surface—do not call undocumented DBX internals.
readAsset paths are relative to ui.root: assets/empty-state.svg resolves to the package's ui/assets/empty-state.svg. Call URL.revokeObjectURL(assetUrl) when done, and call the unsubscribe functions returned by onContext/onEvent when a component is destroyed.
Estimated Execution Plans
A plugin can read the estimated execution plan of a query without owning a driver, a connection pool, or a credential. It sends its own SQL and a connection reference; DBX builds the EXPLAIN statement with its own dialect rules, applies the same read-only safety gate it uses for its own plan view, and runs it on the host connection. The plugin receives only the raw plan, which it parses, normalizes, and visualizes itself.
{
"permissions": ["host.plans:read"]
}if (window.dbxPlugin.capabilities.planApi) {
const capabilities = await window.dbxPlugin.getPlanCapabilities(connectionId);
if (capabilities.supports.estimatedPlan) {
const plan = await window.dbxPlugin.explainPlan({ connectionId, database, sql, mode: "estimated", timeoutMs: 15000 });
render(plan.rawPlan, plan.format);
}
}getPlanCapabilities(connectionId) returns { dbType, dbVersion?, supports: { estimatedPlan }, limits: { maxTimeoutMs, maxPlanBytes } }:
supports.estimatedPlanisfalsewhen this connection's dialect has no estimated plan path in DBX; disable the feature instead of callingexplainPlan.limits.maxTimeoutMsalready accounts for the connection's own query timeout, and a requestedtimeoutMsis clamped to it.limits.maxPlanBytescaps the plan payload.dbVersionis present only when DBX already learned the product version for this connection; the host never probes the server on the plugin's behalf.- The call only reads the stored connection config. It does not connect, but the connection must already be open: a saved connection that is currently disconnected is rejected instead of being opened for the plugin.
explainPlan(request) returns { dbType, dbVersion?, format, rawPlan, truncated, warnings }:
formatisjson,xml(SQL ServerShowPlanXML), ortext.rawPlanis a parsed JSON document forjsonand the plan text otherwise.warningscarriesplan_not_jsonwhen the server answered with something that is not JSON (the payload is then reported astextinstead of pretending it is JSON),plan_truncatedwhen the host cut the plan to respectmaxPlanBytes, andplan_rows_truncatedwhen the driver stopped collecting plan rows.truncatedistruewhenever the host cut the plan for either reason.
The plan API is bounded by design:
- Estimated plans only.
modemust be"estimated"; any other value is rejected. Actual plans (EXPLAIN ANALYZE,SET STATISTICS XML) execute the statement and are not part of this API. - The plugin never supplies SQL to execute. Only the source
sqlis accepted and the host builds theEXPLAINstatement itself, so a plugin cannot pass anEXPLAINstatement, a driver command, or an execution mode. - Read-only targets only. The same gate DBX uses for its own plan view rejects multi-statement input, DDL, DML, and dangerous keywords. Oracle is the one dialect where DBX also plans DML, because
EXPLAIN PLAN FORdoes not execute it. - No credentials, no result set. The response carries the plan and metadata only; a password, credential, connection string, or driver internals never cross this boundary.
- The connection must already be open. DBX does not connect on a plugin's behalf, and it does not create one for the plugin. Both
getPlanCapabilitiesandexplainPlanreject a saved-but-disconnected connection withConnection is not open; only a connection DBX already holds open can be planned.
Gate on capability rather than probing: read dbxPlugin.capabilities.planApi from the init message (an older host omits it), then confirm per-connection support with getPlanCapabilities before calling explainPlan. Submitting EXPLAIN text of your own is neither necessary nor accepted.
The plan API is Host API 1.2. A plugin that cannot work without it declares the floor in its manifest with "engines": { "host_api": "^1.2" }. The manifest range is a compatibility floor and capabilities.planApi is the runtime check; keep both.
Table Schema Metadata
A plugin can read narrow schema metadata for one table on a DBX connection that is already open, without owning a driver, connection pool, credential, or SQL string. The request reuses the canonical PluginTableContext and contains table identity only:
{
"permissions": ["host.schema:read"]
}if (window.dbxPlugin.capabilities.schemaMetadataApi) {
const metadata = await window.dbxPlugin.getTableMetadata({
connectionId,
database,
schema,
table: "users"
});
for (const column of metadata.columns) {
renderColumn(column.name, column.dataType, column.nullable);
}
}getTableMetadata({ connectionId, database?, schema?, table }) returns:
{
"columns": [
{
"name": "id",
"dataType": "integer",
"nullable": false,
"precision": 32,
"default": "nextval('users_id_seq'::regclass)"
}
],
"fieldCapabilities": {
"length": "supported",
"precision": "supported",
"scale": "supported",
"default": "supported"
}
}columnscontains onlyname,dataType,nullable, and optionallength,precision,scale, anddefault. It never returns comments, keys/indexes, credentials, connection strings, driver objects, or arbitrary SQL results.- Optional values remain
nullor are omitted when no value exists; they are never fabricated as0or an empty string.fieldCapabilitiesusessupported,unsupported, andunknownto distinguish provider support, an explicitly unavailable field, and missing reliable provenance; plugins must not treatunknownas supported. databaseandschemaare optional and omitted when unavailable; do not use empty strings as public identity.connectionIdandtablemust be non-empty identity values no longer than 256 characters.- This API is read-only. DBX reuses only an already-open Host connection/session; a plugin cannot trigger an implicit reconnect, create a pool, or execute arbitrary SQL. A saved-but-disconnected connection is rejected with
Connection is not open; a requested database without a matching open session is also rejected instead of being routed to another connection.
This API is part of Host API 1.3. A plugin that cannot work without it should declare "engines": { "host_api": "^1.3" } and still read capabilities.schemaMetadataApi, because older hosts omit that field. Without host.schema:read, the host rejects the request before it reaches the backend.
Read-only Data Queries
A plugin can run one read-only SQL statement on a DBX connection the user granted to it, without a driver, a pool, a credential, or a connection string. Declare the permission and gate the call on the capability:
{
"engines": { "host_api": "^1.4" },
"permissions": ["host.data:read"]
}if (window.dbxPlugin.capabilities.dataApi) {
const result = await window.dbxPlugin.queryData({
connectionId,
database,
sql: "SELECT status, count(*) AS total FROM orders GROUP BY status",
maxRows: 200
});
renderChart(result.columns, result.rows);
}queryData({ connectionId, database?, schema?, sql, maxRows?, timeoutMs? }) returns:
{
"dbType": "postgres",
"columns": [{ "name": "status", "dataType": "text" }, { "name": "total", "dataType": "int8" }],
"rows": [["paid", 1204], ["refunded", 37]],
"truncated": false,
"elapsedMs": 12
}- Consent per connection. The first query for a connection opens a host dialog that names the plugin and the connection. An allow is persisted and listed under the plugin in Plugin Center → Installed, where the user can revoke it at any time; a denial is remembered for the workbench session. A plugin cannot grant itself access.
- Read-only, one statement. The host accepts exactly one statement that DBX's SQL risk classifier rates read-only — the same gate as MCP read-only access and the AI agent. Writes, DDL,
SELECT … FOR UPDATE, multiple statements, and session database switches such asUSEare rejected. Passdatabaseinstead of switching. - Open connections only. Like the plan and schema metadata APIs, DBX never connects on the plugin's behalf: a saved but closed connection is rejected with
Connection is not open. Redis, MongoDB, search engines, and other non-SQL connections are not served. - Bounded.
maxRowsdefaults to 500 and is capped at 5000; the serialized rows are capped at 8 MiB;timeoutMsis clamped to the connection timeout and a 60-second ceiling.truncatedistruewhenever rows were cut. - No credentials. The response carries columns and rows only. A revoked grant fails with an error that starts with
PLUGIN_DATA_ACCESS_NOT_GRANTED; the next query asks the user again.
This API is Host API 1.4. Keep both the engines.host_api floor and the capabilities.dataApi runtime check; web hosts that cannot show a consent dialog deny the request instead of granting it.
System Clipboard
The plugin sandbox has an opaque origin and no clipboard permission, so navigator.clipboard is unavailable there. The host bridges the system clipboard instead:
window.dbxPlugin.copy(text)/window.dbxPlugin.clipboard.writeText(text)write to the system clipboard. Both ride the same ungated bridge method: a write has the user's data as its input and is the low-risk half of the surface.window.dbxPlugin.clipboard.readText()reads the system clipboard. It requires thehost.clipboard:readpermission — a read hands user data (passwords, tokens) to plugin code with no further user interaction — and is Host API 1.3: declare"engines": { "host_api": "^1.3" }if your workbench cannot function without it.
Gate reads on the init capability rather than probing: capabilities.clipboardRead (and capabilities.clipboardWrite for writes) are absent on older hosts, which makes them falsy. A rejected read (missing permission, web host without native clipboard access) rejects its Promise — degrade gracefully, e.g. fall back to a keyboard-paste path instead of failing the interaction.
await window.dbxPlugin.ready;
const canRead = !!window.dbxPlugin.capabilities.clipboardRead;
const canWrite = !!window.dbxPlugin.capabilities.clipboardWrite;
async function pasteFromClipboard() {
if (!canRead) return null;
try {
return await window.dbxPlugin.clipboard.readText();
} catch {
// Permission missing or the clipboard read failed: keep the keyboard path.
return null;
}
}File Transfer and OS Drops
Desktop hosts stream local files into plugin sandboxes through window.dbxPlugin.fileTransfer. Handles are opened only after explicit user consent: a native open/save dialog (pick / beginSave) or a file dropped from the OS onto this plugin's workbench area (onDrop). Transfers are chunked, so multi-gigabyte files never load into memory.
const fileTransfer = window.dbxPlugin.fileTransfer;
if (!fileTransfer) {
// Web host: fall back to <input type="file"> and sidecar disk writes.
}
// Upload: the user picks files, the plugin streams chunks to its sidecar.
const { files } = await fileTransfer.pick({ multiple: true });
for (const file of files) {
let offset = 0;
for (;;) {
const chunk = await fileTransfer.read(file.handleId, offset, 256 * 1024);
await uploadChunk(file.name, offset, chunk.dataBase64); // your own RPC
if (chunk.eof) break;
offset += chunk.length;
}
await fileTransfer.cancel(file.handleId);
}
// Download: open a save target, stream chunks, then flush with finish.
const target = await fileTransfer.beginSave({ name: "export.csv", size });
await fileTransfer.write(target.handleId, 0, bytes);
await fileTransfer.finish(target.handleId);
// OS drag & drop (desktop only): dropped files arrive as opened handles.
const offDrop = fileTransfer.onDrop((files) => uploadAll(files));
const offDrag = fileTransfer.onDragState((active) => showDropOverlay(active));| API | Purpose |
|---|---|
pick({ multiple }) | Native open dialog; resolves opened read handles { handleId, name, size, contentType }. |
read(handleId, offset, length?) | Streams a chunk { dataBase64, length, eof } from a read handle. |
beginSave({ name, contentType?, size? }) | Native save dialog + write handle; resolves { handleId, chunkBytes }, or null when the user cancels. |
write(handleId, offset, data) | Writes one chunk (transferred binary or base64); resolves { written, nextOffset }. |
finish(handleId) / cancel(handleId) | Flush-closes a write handle / discards any handle. |
onDrop(fn) | Files dropped onto this workbench arrive as read handles. A dropped folder is expanded into the regular files it contains (dotfiles and dot-directories like .git are skipped); each expanded file carries relativePath — its '/'-separated path starting with the dropped folder's own name (folder/nested/a.csv) — so the plugin can rebuild the dragged structure and tell same-named files under different dropped roots apart. Listeners receive (files, { dropId, truncated }): dropId groups this drop's entries (cancellation and progress bookkeeping), truncated is true when the 2000-file or 8-depth expansion cap cut the delivery short. |
onDragState(fn) | Whether an OS drag is currently over this workbench; drive drop overlays with it. |
Desktop and web hosts differ in what the namespace can do — gate on the additive capabilities.fileTransfer flags (pick, beginSave, read, drop, folderExpansion) instead of probing calls. On desktop, pick / beginSave use native dialogs and onDrop receives OS drops; dropped entries are opened lazily and streamed in chunks, so very large folders and multi-gigabyte files never exhaust handles or memory, and picking more files than the shared handle registry holds downgrades them to lazily-opened entries instead of dropping any. On the web host, pick falls back to a browser file input, beginSave buffers in memory and downloads through the browser, onDrop / onDragState never fire (drop/folderExpansion are false there), and handles carry no relativePath. onDrop only fires for drops that land inside this plugin's workbench area; drops elsewhere keep the host's own behavior.
Persistent UI State
The sandbox has an opaque origin, so localStorage throws there. Workbenches that need to survive a reload or an app restart — last visited folder, panel sizes, drafts — use window.dbxPlugin.storage instead. It requires the host.storage permission and is advertised as capabilities.storage in the init message; gate on both before use.
if (window.dbxPlugin.capabilities.storage) {
const storage = window.dbxPlugin.storage;
await storage.set("layout", { sidebar: "collapsed", sort: "name" });
const layout = await storage.get("layout"); // { sidebar: "collapsed", sort: "name" }, or null when unset
await storage.delete("layout");
}| API | Purpose |
|---|---|
get(key) | Resolves the stored JSON value, or null when the key is unset. |
set(key, value) | Persists any JSON value (undefined normalizes to null). |
delete(key) | Removes one key; deleting an unset key succeeds. |
Keys are strings up to 256 characters; values are capped at 256 KiB serialized and the whole store at 1 MiB — this is UI state, not a data sink. Entries live in the plugin's own plugin-data/<id> directory, isolated per plugin, and survive upgrades and uninstall. Bulk data, caches, and anything the sidecar produces belong in the sidecar's data directory instead (see Native Sidecars and RPC). On the web host the same API is backed by the browser profile, so identical code works in both hosts.
Analyse plugin data in DBX AI
Declare "permissions": ["host.ai"] in the manifest. A plugin can open the global AI panel directly, or register quick recommendations for each Workbench. Both paths use DBX's model selector, history, follow-up messages, cancellation and export.
Declare default Workbench recommendations
Add optional ai.recommendations under a workbench contribution. label is shown in the global DBX AI panel, prompt is sent when the user clicks it, and lower order values appear first.
{
"type": "workbench",
"id": "com.example.files.main",
"label": "Files",
"ai": {
"recommendations": [
{
"id": "health",
"label": "Check {{resource.name}} health",
"prompt": "Analyze the health and risks of {{resource.kind}}/{{resource.name}}",
"order": 10
}
]
}
}Placeholders are resolved from the current Workbench context. Object properties and array indexes are supported, such as {{resource.name}} and {{items.0.status}}. A recommendation with an unresolved placeholder is hidden; paths containing __proto__, prototype, or constructor are rejected. At most five recommendations are displayed.
Update recommendations when the resource changes
When the plugin switches pages or resources, it can publish runtime recommendations. Runtime items replace the Manifest defaults; an empty array clears the current recommendations. Runtime context usually needs to contain only resource fields. The Host preserves the Workbench's connectionId, database, and instance identity so a click remains bound to the correct plugin connection.
function updateRecommendations(resource) {
if (!dbxPlugin.capabilities.aiRecommendations) return;
return dbxPlugin.ai.setRecommendations({
context: { resource },
items: [
{
id: "health",
label: `Check ${resource.name} health`,
prompt: "Analyze the health and risks of {{resource.kind}}/{{resource.name}}",
order: 10,
},
{
id: "events",
label: "Review recent events",
prompt: "List the important recent events for this resource and suggest next steps",
order: 20,
},
],
});
}
await dbxPlugin.ai.clearRecommendations();Recommendations appear in the global DBX AI conversation window, without adding a separate AI entry point inside the plugin page. Clicking one creates a new Agent conversation with the selected text and sends it immediately; the Agent uses tools exposed by the current open plugin connection to retrieve live data. The recommendation list belongs only to the current Workbench context and is not written to conversation history; the selected prompt is saved normally after it is sent.
Open an Ask or Agent conversation directly
ai.openConversation remains useful for a plugin button or another explicit action. mode defaults to ask, where the Host copies the plugin's JSON snapshot into the conversation; the snapshot is retained with the conversation and later resource refreshes do not replace it. To use live tools, pass mode: "agent" and include the connectionId of the current open plugin connection in the context:
await dbxPlugin.ai.openConversation({
title: "Watchlist analysis",
prompt: "Compare the current quotes and explain risks and missing data.",
context: {
connectionId,
resource: { kind: "watchlist", name: "primary" },
dataTimestamp,
},
mode: "agent",
send: true,
});title allows 200 characters and prompt allows 32000. context must be a plain JSON object and is limited to 2 MiB. An Agent can use only tools exposed by the Host for that plugin connection; the plugin receives no model response or model configuration and gains no database execution capability. Older plugins without recommendations continue to use the existing AI API, and older hosts omit the new capability fields, so disable only the unavailable entry points and keep the rest of the plugin usable.
Themes, Layout, and Static Assets
DBX injects theme tokens on the document root and updates data-dbx-theme. Custom libraries, including Svelte and shadcn-svelte, should consume these variables; parent-page CSS does not cross the iframe:
body {
margin: 0;
background: var(--color-background, #fff);
color: var(--color-foreground, #18181b);
}
button {
background: var(--color-primary, #2563eb);
color: var(--color-primary-foreground, #fff);
cursor: pointer;
}Listen with window.addEventListener("dbx-plugin-env", handler) for locale/theme changes. Initialize from dbxPlugin.locale/theme, then refresh your state on events. DBX includes lightweight dbx-btn and dbx-input styles, but the development host does not emulate the complete component kit; custom token-based styles work more consistently in both.
What the theme payload carries
The env message's theme is { appearance, tokens, editor? }. appearance is "light" | "dark"; tokens are the resolved root design variables swept by prefix (--color-*, --radius-*, --font-*); editor is a structured snapshot of the SQL editor settings that have no CSS-token carrier. Delivered settings:
| DBX setting | Delivered as |
|---|---|
| Light/dark/system mode | theme.appearance |
| Palette + custom color sets | --color-* tokens |
| Corner style | --radius-* tokens |
| UI font family | --font-sans token |
| Editor (mono) font family | --font-mono token + theme.editor.fontFamily |
| Editor font size | theme.editor.fontSize |
| SQL editor syntax theme | theme.editor.theme |
| UI scale (host window zoom) | not delivered — plugins own their own zoom |
| Data-grid font / type colors, editor background image | not delivered — DB-domain (--dbx-* tokens are excluded from the sweep) |
Treat terminal.fontSize-style concerns as plugin-owned: the host never dictates your font size; consume theme.editor.fontSize as a default only if it suits your UI. All payload changes push live on the env channel — font-family edits via the theme revision, size/theme via the explicit editor watcher — so no refresh is needed.
Build the UI as packaged static assets, not references to a Vite server or CDN scripts. The host converts the entry and assets into sandbox-loadable content. For CSP errors, verify build outputs and confinement under ui.root, and reproduce with the matching CLI version. Use plugin-owned dialogs for delete/rename rather than relying on native alert/confirm/prompt inside the sandbox.
Lazy loading and code splitting
The host inlines the entry script into the sandbox document; further ui/ assets stay addressable at runtime through the dbx-plugin scheme — dbx-plugin://localhost/<plugin-id>/<path-under-ui.root> (WebView2 maps it to http://dbx-plugin.localhost/<plugin-id>/…). The host injects the platform-correct form as the sandbox document's <base> and allows it in the resource CSP, so dynamic import() of code-split chunks works in packaged plugins — provided the build emits relative asset URLs: set base: "./" (Vite) or publicPath: "./" (webpack) so chunk and CSS asset URLs resolve against the injected base instead of the host origin. Absolute /assets/… references cannot carry the plugin id and will 404. The scheme serves files only, confined to each plugin's ui.root, and responses are never cached across plugin updates.
Native Sidecars and RPC
Use a native backend only when the browser sandbox cannot provide the capability, such as S3/SSH protocols, system credentials, long-lived connections, or CPU-heavy work. A Sidecar is not an OS sandbox and runs with the current user's privileges.
The Rust and Go SDKs implement Sidecar Protocol v1:
{"jsonrpc":"2.0","id":1,"method":"plugin/initialize","params":{"host":{"protocolVersions":[1]}}}
{"jsonrpc":"2.0","id":2,"method":"objects/list","params":{"prefix":"docs/"}}Responses must reuse the request id; success uses result and failures use a JSON-RPC error. Initialization includes protocolVersion, capabilities, and plugin: { id, version }. ID/version must match the Manifest and the protocol must be supported by both sides, otherwise Sidecar identity or protocol does not match manifest is reported. Backend capabilities are not the Manifest's Host permission list.
use dbx_plugin_sdk::{PluginEmitter, PluginError, PluginHandler, PluginMetadata, PluginServer, RequestContext};
use serde_json::{json, Value};
struct Plugin;
impl PluginHandler for Plugin {
fn handle(&self, _context: RequestContext, method: &str, _params: Value, _emitter: &PluginEmitter) -> Result<Value, PluginError> {
match method {
"example/ping" => Ok(json!({ "ok": true })),
_ => Err(PluginError::method_not_found(method)),
}
}
}
fn main() -> std::io::Result<()> {
let metadata = PluginMetadata::new("com.example.files", env!("CARGO_PKG_VERSION"));
PluginServer::new(metadata, Plugin).serve()
}Sidecar rules: reserve stdout for protocol messages and write logs to stderr; correlate concurrent requests by ID; make connect/disconnect idempotent; design timeouts, cancellation, and chunk acknowledgements for long tasks; never expose secrets through events, context, or error messages.
Every sidecar starts with DBX_PLUGIN_DATA_DIR set to a persistent directory reserved for this plugin (plugin-data/<id>, a sibling of the installed package). It survives version upgrades and uninstall, is shared by all of the plugin's sidecar runs, and is the right home for caches, tokens, and databases the backend manages — create it before the first write; the host does not pre-create it. The Go SDK exposes it as dbxpluginsdk.DataDir() / EnsureDataDir(). Workbench UIs reach a small JSON-backed store inside the same directory through host.storage instead of touching these files (see Persistent UI State).
The example can replace the Rust template's backend/src/main.rs; keep its Cargo package version equal to the Manifest. Go templates use dbxpluginsdk.NewServer(metadata, handler).Serve(). The current Go SDK supports JSONL; the Rust SDK also supports framed transport. Changing a Go Manifest's transport alone does not implement binary framing. See the Rust SDK and Go SDK for complete interfaces.
Tools for the DBX AI Assistant
A plugin with a native backend can expose tools that the built-in DBX AI assistant calls in Agent mode, so a question like "why is the orders API slow?" can combine database tools with your plugin's metrics, logs, or shell access. The sidecar implements two methods:
mcp/toolsreturns{ "tools": [{ "name", "description", "inputSchema", "annotations"? }] }in MCP tool format. DBX passes{ "connectionId" }for the connection it is binding, so a plugin may hide write tools on read-only connections.mcp/callreceives{ "tool", "arguments", "lifecycle" }and returns an MCPCallToolResult({ "content": [{ "type": "text", "text": "..." }], "isError": false }).lifecycleis the same payload asconnection/connectfor the open connection — credentials resolved by the host and the runtime endpoint after SSH or proxy layers — so tools never take secrets as arguments.
What the host adds on top:
- Opt-in, revocable. A plugin contributes AI tools only after the user enabled it in Plugin Center (Installed → Built-in AI tools); installing alone never exposes anything. The same panel lists the tools and marks which ones need approval, and switching a plugin off there turns it off for good. A manifest
mcpcontribution withai_tools: falseopts a plugin out of this surface even when opted in. - Open connections only. Tools are offered for the plugin connections the user currently has open. DBX binds the connection itself: it strips
connectionId/connectionNamefrom the schema the model sees and injects the boundconnectionIdinto the forwarded arguments when your schema declares it. With several open connections the model picks one through an addeddbx_connectionargument. - Approval by default. Only tools whose entry sets
"annotations": { "readOnlyHint": true }run without asking. Every other call pauses the run and shows the exact arguments to the user, who allows it once or denies it; an unanswered request is denied after five minutes. Mark genuinely read-only tools, and keep your own guards (read-only connection flags, two-phase confirmations) — the approval covers model mistakes, not your plugin's own safety rules. - Portable schemas. Tool names are exposed as
<prefix>__<tool>(for examplessh__ssh_exec), and schemas are reduced to the subset every supported model provider accepts:type,description,properties,required,items, stringenum, and numeric/length/item bounds. Keep argument names to letters, digits, and underscores. - Bounded results. Calls time out after 120 seconds and results are compacted before they reach the model. Start long jobs as background tasks and return a handle instead of blocking.
The built-in agent treats tool output as untrusted data. Return facts, not instructions, and never echo secrets in results.
Create a Plugin
Install the precompiled CLI without Rust or a DBX source checkout:
npm install --global @dbx-app/plugin-cli
dbx-plugin --helpOr run it directly through npx:
npx @dbx-app/plugin-cli create my-ui-plugin --template frontendThe npm package selects the precompiled binary for the current platform and bundles the matching Rust and Go plugin SDK sources. Frontend-only plugins do not need Rust or Go; native toolchains are required only to compile the plugin's own sidecar.
Create a project:
# Frontend only and cross-platform
dbx-plugin create my-ui-plugin --template frontend
# Svelte + Vite and cross-platform
dbx-plugin create my-svelte-plugin --template svelte
# Rust sidecar plus frontend
dbx-plugin create my-rust-plugin --template rust
# Go sidecar plus frontend
dbx-plugin create my-go-plugin --template goChoose frontend for plain HTML or a self-managed Vue/React UI, svelte for the Svelte 5 + Vite starter, or rust/go when a native Sidecar is needed. --backend none|svelte|rust|go is a template alias and --language rust|go remains a native-project compatibility alias. Use --yes for scripts and CI:
dbx-plugin create my-plugin \
--template svelte \
--id com.example.my-plugin \
--name "My Plugin" \
--publisher example \
--description "A DBX plugin." \
--version 0.1.0 \
--yesThe Svelte template writes Vite output to ui/ and emits DBX_UI_BUILD_SUCCESS after a successful build:
cd my-svelte-plugin
npm install
npm run build
dbx-plugin dev --port 5190Develop, test, and version the generated project in the plugin's own source repository. Do not copy an ordinary plugin source tree into t8y2/dbx merely to publish it.
Build Review Candidates
From the plugin project root, run:
dbx-plugin package .The command produces unsigned review candidates only:
dist/<plugin-id>-<version>-<target>.dbxp
dist/<plugin-id>-<version>-<target>.artifact.jsonFrontend-only projects default to the universal target; Rust/Go projects use the current platform target. Use --output-dir to change the output location and --artifact-url to record the eventual HTTPS URL:
dbx-plugin package . --target universal --output-dir dist --artifact-url https://downloads.example.com/my-plugin.dbxpThe CLI packages only directories declared by [package].include in dbx-plugin.toml and rejects symlinks, traversal paths, .dbx-dev, oversized files, and unsafe output locations. Do not use dist/ or development data as a nested package input.
Official plugin authors do not create or possess the DBX Store private key. The generated GitHub Release workflow builds candidates for each target and merges their metadata into release-candidates.json.
To install an unsigned package during local development, explicitly enable Allow unsigned development packages in Plugin Center. This option never relaxes official Marketplace verification.
Local Development and Verification
Run the standalone browser development host from the plugin root when you need live UI and Sidecar diagnostics; it does not start the DBX desktop application:
dbx-plugin dev --path . --port 5190Open the http://127.0.0.1:5190/ URL printed by the command. Use --port 0, or let a busy port fall back to an available one. Move development data outside the project with --data-dir:
dbx-plugin dev --path . --data-dir /tmp/my-plugin-devdbx-plugin dev does not install project dependencies. Configure frontend builds in dbx-plugin.toml:
[dev]
ui_build = ["npm", "run", "build"]
ui_watch = ["npm", "run", "build:watch"]When ui_watch is configured, it must print a standalone DBX_UI_BUILD_SUCCESS line only after a complete successful build has written every output. Do not print it on failure or from an unconditional exit hook.
The debug page can switch locale, theme, auto-reload, and diagnostic views. It keeps only the latest 500 in-memory entries, redacts passwords, tokens, and Manifest secret fields, and omits or truncates binary and oversized values. .dbx-dev/ may contain plaintext credentials: keep it in .gitignore and never share it.
Read redacted diagnostics from a script:
curl -sS 'http://127.0.0.1:5190/api/diagnostics?after=0&limit=100&level=error'Pass nextAfter and instanceId from the response to the next request. The development host implements a supported Host API subset; validate installation, signing, the real Secret Store, desktop lifecycle, and production permissions in DBX itself.
Local Acceptance Checklist
- Remove unused permissions and verify that declared permissions match actual calls.
- Test empty configuration, invalid credentials, timeouts, offline behavior, and Sidecar restarts.
- Test DBX light/dark themes and
zh-CN/en; the host does not translate plugin UI text. - Test reconnect, closing a workbench, opening the same workbench twice, and empty context.
- Check packaged paths, executable permissions, and that no development data or secrets enter the artifact.
Complete Official Marketplace Flow
Step 1: Publish Source and a Candidate Release
Create a version tag and GitHub Release in the plugin's source repository. The Release should contain:
- one unsigned
.dbxpcandidate for each supported target; - the corresponding
.artifact.jsonfiles; - merged
release-candidates.json; - release notes and the matching source tag.
Candidate packages may live in GitHub Releases, a CDN, or object storage. Do not commit .dbxp binaries to Git history.
Step 2: Let dbx-store Create the Candidate PR
The dbx-store synchronizer periodically checks public plugin repositories registered in automation/plugin-sources.json with autoUpdate: true. It reads the newest Release's release-candidates.json and creates or updates exactly one:
candidates/<plugin-id>.jsonFor an auto-updated plugin, authors do not need to create a manual Issue or PR for every version, and do not configure Marketplace signing secrets in the plugin repository. The source Release, candidate metadata, and an accessible source tag are the important inputs. For a first listing or an intentional Marketplace metadata change, follow the dbx-store contribution guide.
.dbx-store.json is optional Marketplace metadata. Use it for first registration or deliberate changes to the store name, tags, license, homepage, or similar listing fields. A version-only release does not need a new .dbx-store.json; it is not the runtime Manifest and must never contain a private signing key.
Example:
{
"name": "Example Files",
"description": "Browse files from an example service.",
"icon": "assets/plugin.svg",
"tags": ["files", "storage"],
"permissions": ["host.workbench"],
"source": "https://github.com/example/dbx-plugin-files",
"homepage": "https://github.com/example/dbx-plugin-files",
"license": "Apache-2.0",
"releaseNotes": "Initial release."
}Allowed store fields are name, description, icon, tags, permissions, source, homepage, license, releaseNotes, and localizations. A package-relative icon is converted to an HTTPS URL under the source tag during synchronization. autoUpdate belongs to the repository registration in dbx-store/automation/plugin-sources.json, not this file.
Automation only creates or updates a candidate PR; it never approves, signs, or merges it. The PR targets t8y2/dbx-store on main, not t8y2/dbx.
If the plugin is not registered for automatic synchronization, fork dbx-store and open one PR against main containing publishers/<publisher-id>.json for a first publisher submission and candidates/<plugin-id>.json. There is no separate submission Issue. Before signing, CI intentionally stays red with open candidate(s) awaiting DBX Store signing; the signing workflow writes the finalized catalog back to the PR and makes it green. Authors can submit a PR to register their public repository for synchronization or ask a maintainer to register it; no App private key belongs in the plugin repository.
The candidate's essential shape is below. It references unsigned assets from the plugin repository and must not contain an official signingKeyId:
{
"schemaVersion": 1,
"id": "com.example.files",
"publisher": "example",
"version": "0.1.0",
"name": "Example Files",
"source": "https://github.com/example/dbx-plugin-files/tree/v0.1.0",
"license": "Apache-2.0",
"targets": [{
"target": "universal",
"url": "https://github.com/example/dbx-plugin-files/releases/download/v0.1.0/com.example.files-0.1.0-universal.dbxp",
"sha256": "<64 hexadecimal SHA-256 characters>",
"size": 123456
}]
}For a new version, submit only the new version's candidate data. Never hand-edit generated plugins/<plugin-id>.json or catalog/index.json.
Step 3: DBX Store Reviews and Signs
Maintainers review the source, Manifest, permissions, candidate SHA-256 values, sizes, and native behavior. After approval, the protected DBX Store workflow:
- downloads the candidate using its reviewed SHA-256 and size;
- confirms that it is unsigned and matches the expected plugin ID and version;
- adds an Ed25519 signature using the DBX Store repository key;
- publishes the final
.dbxp, final artifact metadata, and signing receipt.
Plugin authors never receive the official repository private key.
Step 4: Merge the Candidate PR
After signing, the workflow writes finalized plugins/<plugin-id>.json and generated catalog/index.json back to the same candidate PR. It publishes the signed package, artifact metadata, and signing receipt to a dbx-store Release. Once checks pass, a maintainer reviews and merges the PR.
Do not include the following in a submission or update PR:
.dbxpbinaries;- a copied plugin source tree;
- Ed25519 private keys, tokens, or download credentials;
- unsigned candidate URLs as final install artifacts;
- a self-assigned
verified: truevalue.
Update an Existing Plugin
Every update uses a new semantic version. Existing Release assets cannot be replaced:
- Change the source and increment
manifest.json'sversion; never reuse a published version. - Publish a new source tag and candidate Release; existing Release assets are immutable.
- Wait for the synchronizer to create or update
automation/plugin-release/<plugin-id>/<version>. - A maintainer reviews, signs, and merges the PR; the catalog then contains the new version and target artifacts.
If the repository is not registered for automatic synchronization, submit candidates/<plugin-id>.json manually according to the dbx-store contribution guide. For an update, submit only the new version's candidate data; do not hand-edit generated plugins/ or catalog/ files.
Fix plugin-code problems in the plugin source repository. Only catalog metadata, review status, final URLs, hashes, sizes, and Marketplace copy belong in dbx-store.
Signatures and Publisher Identity
DBX currently uses one repository signature:
publisherrecords authorship and Marketplace ownership;signingKeyIdidentifies the repository key that published the final installable package;- DBX Store reviews and signs official plugins centrally;
- custom and private repository operators manage their own repository keys;
- developer signatures and dual signatures are not current listing requirements.
Human review determines whether a plugin may appear in the Marketplace. Ed25519 repository signing ensures that the reviewed package was not replaced afterward. A signature does not replace source review and is not an operating-system sandbox for native sidecars.
Official Marketplace plugin authors must not run keygen or invent a signingKeyId. Private or custom repository operators can use the advanced CLI signing tool:
dbx-plugin keygen company.plugins.release
source .dbx-repository-signing-key.envThe command creates a protected 32-byte Ed25519 seed environment file (mode 0600 on Unix) and prints the Key ID and Base64 public key; never commit the private file. A custom repository must add the public key to its own DBX trust configuration. The official Marketplace private key remains in a protected dbx-store Environment Secret, while DBX ships the public key in its official trust list.
artifact.json fields target, url, sha256, and size bind the exact bytes. Official signing adds signingKeyId only afterward. Published assets are immutable; any byte change requires a new plugin version and another review.