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 transitionssrc/worker/ Zync API adapter, pane routing, confirmations, bounded jobssrc/ui/ components, local interaction state, theme and accessibilitysrc/protocol/ message types and runtime validators shared by UI and workertests/ pure logic, worker behavior, browser UI, package checksThis 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.
Keep authority tied to the pane
Section titled “Keep authority tied to the pane”- 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
connectionTokeninto 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.
Keep work bounded
Section titled “Keep work bounded”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.
Fit the Zync workspace
Section titled “Fit the Zync workspace”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:
npm install --save-exact @zync-sh/plugin-ui@0.1.0-beta.1npm install --save-dev --save-exact @zync-sh/plugin-sdk@2.1.0-beta.4import { 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.
Treat data deliberately
Section titled “Treat data deliberately”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.
Test the release, not only the preview
Section titled “Test the release, not only the preview”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.
Maintain a community release
Section titled “Maintain a community release”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.