Skip to content
ZyncDocsGitHub Download Zync

Plugin Best Practices

Build maintainable Zync plugins with scoped server actions, responsive panes, bounded work, and reliable releases.

These practices follow Zync’s worker/pane separation and the patterns used by Docker Manager, PM2 Monitor, and Zedit. Those projects are examples, not a license to depend on private desktop internals.

Separate logic, authority, and presentation

Section titled “Separate logic, authority, and presentation”

Keep parsing and command construction in small pure modules. Put Zync API calls, permission handling, and server operations in the worker. Let the pane render state and request named operations through a typed client interface.

src/domain/ parsing, validation, command builders, state transitions
src/worker/ Zync API adapter, pane routing, confirmations, bounded jobs
src/ui/ components, local interaction state, theme and accessibility
src/protocol/ message types and runtime validators shared by UI and worker
tests/ pure logic, worker behavior, browser UI, package checks

This is a suggested structure, not required filenames. Keep the plugin small; avoid a single component that owns server commands, parsing, polling, and layout. Types do not validate incoming JSON: check it at runtime in the worker.

  • Route results to the host-provided paneInstanceId. Two panes may show different servers or different selections on the same server.
  • Use pane-bound SSH APIs. Do not accept a UI-supplied connection ID as authority.
  • Allowlist operations and validate their values. A UI message such as { "type": "restart", "service": "example" } should map to a worker-owned command, not arbitrary shell text.
  • Before a mutation, show a Zync-owned confirmation describing the action and target. Carry the read operation’s connectionToken into the confirmed command.
  • If a connection changes, discard stale results and require a fresh read and confirmation. Do not silently retarget work to a replacement connection.

ssh.command.execute grants the connected account’s authority. Docker access may be root-equivalent. Explain this in the permission reason. Quoted arguments prevent accidental shell expansion; they do not make arbitrary programs safe.

Design for incomplete and failed operations

Section titled “Design for incomplete and failed operations”

A timeout, canceled channel, or lost acknowledgement does not prove a remote mutation failed. Refresh server state before retrying. Avoid automatic retries for delete, restart, deploy, or write actions unless you have a real idempotency design.

Give users distinct loading, empty, stale, disconnected, permission-denied, and failed states. Keep the last successful result visibly marked as stale rather than replacing it with a success-looking empty list. Bound retained logs and results so a long session does not grow memory indefinitely.

Use one in-flight request per pane for SSH commands. Schedule the next refresh after the previous one completes; avoid overlapping setInterval jobs. Pause automatic reads while the pane is hidden using the optional visibility API, and back off on disconnects and failures. Feature-detect this API for older hosts.

Use request IDs or generation counters so late results cannot overwrite a newer selection. Dispose listeners, observers, timers, and terminal surfaces when their owners are torn down. Worker-wide state must not leak selections between panes. Design below the documented limits, including message size, file size, request rate, and SSH output limits.

Build for narrow split panes and container resizing, not only a full browser viewport. Let Zync own workspace tabs, docking, and terminal controls. Avoid an inner workspace tab bar that duplicates the host’s navigation.

For a bundler-based UI:

Terminal window
npm install --save-exact @zync-sh/plugin-ui@0.1.0-beta.1
npm install --save-dev --save-exact @zync-sh/plugin-sdk@2.1.0-beta.4
Pane module, bundled into your packaged assets
import { installThemeBridge, enhanceSelects, installTooltips } from '@zync-sh/plugin-ui';
import '@zync-sh/plugin-ui/styles.css';
document.body.classList.add('zui-root');
const disposeTheme = installThemeBridge();
const disposeSelects = enhanceSelects();
const disposeTooltips = installTooltips();
window.addEventListener('pagehide', () => {
disposeTheme();
disposeSelects();
disposeTooltips();
}, { once: true });

Use --zui-background, --zui-surface, --zui-border, --zui-text, --zui-muted, and --zui-primary, plus the semantic status colors. Keep CSS inside the pane. Package fonts and dependencies; do not depend on CDN access. The basic starter is a file copier, so add a bundler before using npm imports or CSS imports like those above.

Give controls labels, keyboard access, visible focus, and sufficient contrast in light, dark, and custom themes. Test menus in narrow panes and near container edges. Respect reduced motion. Render untrusted server output as text, not HTML. Mark destructive controls clearly and reserve the host’s confirmation for the action.

For embedded host terminals, reserve an untransformed rectangular slot and dispose it on removal. Register only real popup elements with the SDK overlay helper, then dispose those registrations on close. Never put an invisible overlay over host approval controls or rely on arbitrary z-index to cover the terminal.

Request only the permissions the plugin needs. Optional features should still leave the core workflow usable when declined. Declare exact network hosts and explain what leaves the device. Do not collect credentials, install identifiers, command histories, or telemetry simply because they might be useful later.

Plugin-private storage is not the Local Vault. Do not store passwords, private keys, or access tokens there. Keep records small and versioned; preserve backward readability for rollback. Make clear whether data belongs to a device, server, or pane, and what uninstall/delete-data does. Avoid secrets in logs and issue reports.

Before submission, record results for:

  • Logic: malformed messages, argument validation, parsing unusual output, empty results, nonzero exit codes, output limits, and stale replies.
  • Permissions: required/optional review, denial, revocation, and disabling the plugin.
  • Lifecycle: two panes on different servers, reconnect during confirmation, pane close during work, hidden-pane polling, and app restart.
  • UI: narrow and wide panes, keyboard navigation, focus, themes, long labels, loading/error states, and no HTML execution from server output.
  • Packaging: clean build, host-version preflight, signed ZIP extraction, signature verification, checksum of the final uploaded ZIP, and no secrets/mocks.
  • Installed app: supported Windows/macOS/Linux WebViews as applicable, declared minimum Zync version, install/update, and rollback where retained.

Use mocked servers for repeatable tests and a disposable real server for integration. Never make a documentation test mutate a user’s production server. Record what was simulated versus tested on a packaged app. Browser tests cannot establish native permission or filesystem behavior.

Keep source and release tags reviewable, dependencies locked, license notices present, and support instructions current. Describe behavior and permission changes in release notes. Update the manifest compatibility range when you adopt a newer host feature. Do not rename the plugin ID just to work around an update failure or replace release assets under an already published version.

Report suspected key compromise promptly to registry maintainers, and use the publishing workflow for new versions and key rotation. Do not claim a signature or automated test proves a plugin is safe in every environment.