plugins.json in TableProApp/plugins, and TablePro’s release workflow writes it: a plugin gets there by being contributed to TablePro’s own repository and tagged there. A plugin of yours ships the other way: signed with your own Developer ID, it installs from a file or from a manifest you host in the same format. For what install and update look like to a user, see Plugins & Themes; to build the plugin in the first place, Plugin Development.
The app fetches the manifest to fill Settings > Plugins > Browse and to auto-install a driver when someone picks a database type with no plugin loaded.
Manifest format
Each entry in
binaries:
v1 manifests carried top-level
downloadURL, sha256, and minPluginKitVersion in place of binaries. The app still decodes them, synthesizing one entry per architecture. Write new entries with binaries.Binary selection
For a driver, the app filtersbinaries to the running architecture, keeps those whose pluginKitVersion falls inside [minimumCompatiblePluginKitVersion, currentPluginKitVersion], and installs the highest. Both bounds are declared in PluginManager.swift. A driver binary with no pluginKitVersion never resolves, and the install fails with noCompatibleBinary.
Themes carry no native code, so they match on architecture alone.
Example entry
Plugin metadata
The optionalmetadata object makes an entry self-describing, so the app renders the connection form, sidebar, and editor for a database type before the plugin is installed. Carry it on every driver entry: without it, someone picking your database type stares at a bare form until the download finishes.
RegistryPluginMetadata in TablePro/Core/Plugins/Registry/RegistryModels.swift is the full field list. update-registry.py copies an existing metadata block forward on every release, so it is edited by hand in the registry repository and never regenerated.
Publishing a plugin
Every plugin CI can publish has an entry in.github/plugin-registry.json in TablePro’s repository, keyed by the slug that appears in its tag. That file maps the slug to a build target, so the mapping has no derivation rule: mssql builds MSSQLDriver and cloudflare-d1 builds CloudflareD1DriverPlugin. Add the entry before the first tag, or the workflow exits with Unknown plugin.
tags input takes comma-separated tag:pluginKitVersion pairs, and dropping the : part makes the workflow read currentPluginKitVersion from PluginManager.swift:
plugins.json to TableProApp/plugins through .github/scripts/update-registry.py, which writes atomically and rebases on a retry when the matrix jobs collide.
Bundled plugins ride with the app release. Ten of them also have an entry in .github/plugin-registry.json (SQLite, ClickHouse, Redis, XLSX export, XLSX import, MQL export, SQL import, HTML export, Markdown export, XML export), so a fix to one can be tagged and published to the registry before the next app release. A bulk ABI re-release skips them. The other seven have no entry and never reach plugins.json.
PluginKit compatibility
A plugin built against any PluginKit version inside the app’s[minimum, current] range loads, and the runtime fills newer requirements from their defaults. What that means for releases:
- Additive change (a new requirement with a default, a new field on a non-frozen type): raise
currentPluginKitVersionand theTableProPluginKitVersionof every plugin rebuilt against it, leaveminimumCompatiblePluginKitVersionalone, and re-publish nothing. The binaries already out there keep serving, because the runtime fills a requirement they predate from its default. The bump is for the other direction: a plugin rebuilt against the new SDK references symbols an older app does not carry, and a manifest left at the old number passes that app’s version check and then fails to load at all. Leaving the manifest behind is the one mistake this rule exists to stop. - Breaking change (a removed or changed requirement, a frozen-layout change, a requirement without a default): raise
currentPluginKitVersionandminimumCompatiblePluginKitVersiontogether, then runscripts/release-all-plugins.sh <newVersion>. It reads the registry-only plugins out of.github/plugin-registry.json, bumps each one’s patch version, and fires a singleworkflow_dispatchso they all build as one matrix. - Retention:
update-registry.pykeeps binaries for the three newest PluginKit versions per plugin. Older ones are pruned, and an app below every remaining binary resolves nothing at all, for every plugin. - Bump the kit at most once per release cycle. Retention counts kit versions, not days, so the window’s length in wall-clock time is set by how often the number moves. Eleven bumps between 2026-09-02 and 2026-09-12 took it from 20 to 30 and left the oldest published binary at kit 21, while v0.65.0 to v0.70.0 ship kit 19 and v0.71.0 ships kit 20.
main:
currentPluginKitVersion and dispatches the workflow with baseRef set, so the binary links against that release’s PluginKit and declares the matching TableProPluginKitVersion. Both have to come from the same tree. Stamping a binary built from main with an older number makes the older app accept it and then fail Bundle.loadAndReturnError.
The app’s own release workflow runs scripts/check-registry-readiness.py --floor <min> --current <current> and fails until every registry driver has a compatible binary, so the app cannot ship ahead of its plugins. When an installed driver predates a breaking bump, the app repairs it in the background on the next connect. See After an app update.
Caching
The app fetches the manifest fromraw.githubusercontent.com/TableProApp/plugins/main/plugins.json, which caches at the edge for about five minutes. Every fetch revalidates conditionally, the list refreshes at launch and when the plugin browser opens (throttled to one check per five minutes), and an install prompt forces a fresh fetch before it reports a plugin missing. CI also purges the jsDelivr cache after each registry push, for older app versions that still fetch from there. A newly published plugin shows up in the app within minutes.
Theme distribution
Themes use the same manifest withcategory: "theme". Four things differ from a driver:
- Pure JSON data. No executable code, no code signing, no
.tablepluginbundle - The ZIP holds
.jsonfiles, each a validThemeDefinition. Packs with several themes work - They install to
~/Library/Application Support/TablePro/Themes/Registry/ - No
pluginKitVersionis needed, and the flat v1 fields still decode

