> ## Documentation Index
> Fetch the complete documentation index at: https://ngquct-docs-fix-500-query-results.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Plugin Registry

> Registry manifest format, binary selection, publishing, and PluginKit compatibility

The registry lives in TablePro's repository, so publishing there means a pull request. Signing a plugin with your own Developer ID and serving it from a manifest you host is the other route, and the format on this page is the same either way. For what install and update look like to a user, see [Plugins & Themes](/features/plugins); to build the plugin in the first place, [Plugin Development](/development/plugin-development).

TablePro's manifest is `plugins.json` at [github.com/TableProApp/plugins](https://github.com/TableProApp/plugins). The app fetches it to fill **Settings > Plugins > Browse** and to auto-install a driver when someone picks a database type with no plugin loaded.

## Manifest format

```json theme={null}
{
  "schemaVersion": 2,
  "plugins": [ … ]
}
```

Schema version 2 is current. A manifest declaring a higher version is rejected and the app falls back to its cached copy.

| Field             | Type      | Required | Description                                                           |
| ----------------- | --------- | -------- | --------------------------------------------------------------------- |
| `id`              | string    | Yes      | Bundle identifier, such as `com.TablePro.OracleDriver`                |
| `name`            | string    | Yes      | Display name                                                          |
| `version`         | string    | Yes      | Semantic version                                                      |
| `summary`         | string    | Yes      | One-line description                                                  |
| `author`          | object    | Yes      | `{ "name": "…", "url": "…" }`, `url` optional                         |
| `homepage`        | string    | No       | Project URL                                                           |
| `category`        | string    | Yes      | `database-driver`, `export-format`, `import-format`, `theme`, `other` |
| `databaseTypeIds` | \[string] | No       | `DatabaseType.pluginTypeId` values, which is what drives auto-install |
| `binaries`        | \[object] | Yes      | Per-architecture binaries                                             |
| `minAppVersion`   | string    | No       | Below this the install fails before any download                      |
| `iconName`        | string    | No       | SF Symbol or bundled icon name                                        |
| `isVerified`      | bool      | No       | Defaults to `false`                                                   |
| `metadata`        | object    | No       | Self-describing plugin metadata                                       |

Each entry in `binaries`:

| Field              | Type   | Required        | Description                                    |
| ------------------ | ------ | --------------- | ---------------------------------------------- |
| `architecture`     | string | Yes             | `arm64` or `x86_64`                            |
| `pluginKitVersion` | int    | Yes for drivers | The PluginKit ABI the binary was built against |
| `downloadURL`      | string | Yes             | Direct URL to the `.zip`                       |
| `sha256`           | string | Yes             | SHA-256 hex of the ZIP                         |

<Note>
  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`.
</Note>

## Binary selection

For a driver, the app filters `binaries` 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

```json theme={null}
{
  "id": "com.TablePro.OracleDriver",
  "name": "Oracle Driver",
  "version": "1.0.26",
  "summary": "Oracle Database 12c+ driver via OracleNIO",
  "author": { "name": "TablePro", "url": "https://tablepro.app" },
  "homepage": "https://docs.tablepro.app/databases/oracle",
  "category": "database-driver",
  "databaseTypeIds": ["Oracle"],
  "binaries": [
    {
      "architecture": "arm64",
      "pluginKitVersion": 19,
      "downloadURL": "https://github.com/TableProApp/TablePro/releases/download/plugin-oracle-v1.0.26/OracleDriver-arm64.zip",
      "sha256": "<sha256>"
    },
    {
      "architecture": "x86_64",
      "pluginKitVersion": 19,
      "downloadURL": "https://github.com/TableProApp/TablePro/releases/download/plugin-oracle-v1.0.26/OracleDriver-x86_64.zip",
      "sha256": "<sha256>"
    }
  ],
  "minAppVersion": "0.57.0",
  "iconName": "server.rack",
  "isVerified": true
}
```

## Plugin metadata

The optional `metadata` 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.

| Group                 | Fields                                                                                                                                                                                                                    |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Identity and form     | `displayName`, `iconName`, `defaultPort`, `brandColorHex`, `connectionMode`, `requiresAuthentication`, `additionalConnectionFields`, `postConnectActions`, `urlSchemes`, `fileExtensions`                                 |
| Capabilities          | `supportsSSH`, `supportsSSL`, `supportsForeignKeys`, `supportsSchemaEditing`, `supportsDatabaseSwitching`, `supportsImport`, `supportsExport`, `supportsReadOnlyMode`, `supportsHealthMonitor`, and the rest of the flags |
| Naming and navigation | `systemDatabaseNames`, `systemSchemaNames`, `defaultSchemaName`, `tableEntityName`, `containerEntityName`, `navigationModel`, `databaseGroupingStrategy`                                                                  |
| Editor                | `editorLanguage`, `queryLanguageName`, `sqlDialect` (keywords, functions, data types, pagination style), `statementCompletions`, `explainVariants`, `columnTypesByCategory`                                               |

`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`, 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`.

```bash theme={null}
git tag -a plugin-oracle-v1.0.26 -m "plugin-oracle-v1.0.26"
git push origin plugin-oracle-v1.0.26
```

<Warning>
  Push plugin tags one at a time. A push carrying more than three tags creates no push events on GitHub, so no workflow fires and nothing is published.
</Warning>

Dispatching the workflow works too. Its `tags` input takes comma-separated `tag:pluginKitVersion` pairs, and dropping the `:` part makes the workflow read `currentPluginKitVersion` from `PluginManager.swift`:

```bash theme={null}
gh workflow run build-plugin.yml --field "tags=plugin-oracle-v1.0.26"
```

Either way CI builds both architectures, signs and notarizes the bundles, checks each bundle's declared PluginKit version against the release label, creates the GitHub release, re-verifies the published assets, and updates `plugins.json` through `.github/scripts/update-registry.py`, which writes atomically and rebases on a retry when the matrix jobs collide.

Most bundled plugins never appear in the registry at all, because their binaries ride with the app release. Six of them keep a registry arm anyway (SQLite, ClickHouse, Redis, XLSX export, MQL export, SQL import), so a fix can reach users who are already on a shipped app without waiting for the next one. A bulk ABI re-release skips those six.

## 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): no bump, no re-publish. The binaries already out there keep serving.
* **Breaking change** (a removed or changed requirement, a frozen-layout change, a requirement without a default): raise `currentPluginKitVersion` and `minimumCompatiblePluginKitVersion` together, then run `scripts/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 single `workflow_dispatch` so they all build as one matrix.
* **Retention**: `update-registry.py` keeps binaries for the two newest PluginKit versions per plugin. Older ones are pruned, so a user two or more versions behind hits `noCompatibleBinary` and has to update the app.

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](/features/plugins#after-an-app-update).

## Caching

The app fetches the manifest from `raw.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 with `category: "theme"`. Four things differ from a driver:

* Pure JSON data. No executable code, no code signing, no `.tableplugin` bundle
* The ZIP holds `.json` files, each a valid `ThemeDefinition`. Packs with several themes work
* They install to `~/Library/Application Support/TablePro/Themes/Registry/`
* No `pluginKitVersion` is needed, and the flat v1 fields still decode

```json theme={null}
{
  "id": "com.example.monokai-theme",
  "name": "Monokai Theme",
  "version": "1.0.0",
  "summary": "Classic Monokai color scheme for TablePro",
  "author": { "name": "Theme Author" },
  "category": "theme",
  "downloadURL": "https://example.com/monokai-theme.zip",
  "sha256": "<sha256-of-zip>",
  "iconName": "paintpalette"
}
```

## Custom registry URL

Point the app at a private or enterprise manifest, which is also how a plugin signed with your own Developer ID reaches your users:

```bash theme={null}
defaults write com.TablePro com.TablePro.customRegistryURL "https://your-registry.example.com/plugins.json"

defaults delete com.TablePro com.TablePro.customRegistryURL
```

HTTP caching keys on the full URL, so a changed registry URL takes effect on the next fetch. A plugin served this way installs once the user agrees to trust its signing team by name. Both commands are also listed in [Settings](/customization/settings).
