.tableplugin bundle built against TableProPluginKit (Plugins/TableProPluginKit/). You implement two protocols: DriverPlugin describes the database (name, port, capabilities, SQL dialect) and creates driver instances; PluginDatabaseDriver is the connection itself (connect, query, fetch schema). The app bridges your driver to its internal DatabaseDriver protocol through PluginDriverAdapter and resolves it by database type ID, so a working plugin gets the full UI: connection form, sidebar, data grid, editor, import and export.
The best starting point is an existing plugin. Plugins/SurrealDBDriverPlugin/ is a compact reference with no C bridge: it talks HTTP, implements the schema-aware query-building hooks, and ships a custom editor language.
Bundle layout
A plugin is a macOS loadable bundle target withWRAPPER_EXTENSION = tableplugin, linking TableProPluginKit. The principal class (set via INFOPLIST_KEY_NSPrincipalClass) must be your DriverPlugin class. Info.plist keys:
Without
TableProProvidesDatabaseTypeIds, the plugin loads eagerly at app startup and PluginManager logs a warning. With it, the app registers the metadata from Info.plist and loads the binary on first use.
Implementing DriverPlugin
Your principal class conforms toTableProPlugin and DriverPlugin. Only a handful of members have no default:
DriverConnectionConfig carries host, port, username, password, database, SSL configuration, and an additionalFields dictionary populated from your additionalConnectionFields declarations.
Everything else on DriverPlugin is an optional static with a default: connection mode, URL schemes, brand color, editor language, sqlDialect (keywords, functions, completions), capability flags (supportsSSH, supportsSchemaEditing, supportsTriggers, and about twenty more), navigation model, and system database names. Override what differs from the defaults; see Plugins/TableProPluginKit/DriverPlugin.swift for the full list.
Implementing PluginDatabaseDriver
The driver protocol has around 80 requirements, but most have default implementations. You must implement:
Notable defaults you may want to override:
ping()runsSELECT 1; transactions runBEGIN/COMMIT/ROLLBACKviaexecute(query:).fetchAllColumns(schema:)andfetchAllForeignKeys(schema:)loop per table (N+1 round-trips). SQL drivers should replace them with one bulk query.quoteIdentifier,escapeStringLiteral,executeParameterized, andstreamRowshave generic SQL defaults.- Non-SQL databases implement the query-building hooks (
buildBrowseQuery,buildFilteredQuery,generateStatements) so browsing and editing work without SQL. Implement theschema:-aware overloads if your database has schemas; the schema-less defaults drop the schema.
How loading works
PluginManager loads plugins from two places: the app’s PlugIns/ directory (built-in, no signature check) and ~/Library/Application Support/TablePro/Plugins (user-installed, must be signed by TablePro’s signing team).
Before loading any bundle, validateBundleVersions checks TableProPluginKitVersion against the app’s compatible range, [minimumCompatiblePluginKitVersion, currentPluginKitVersion] in PluginManager.swift. A missing key, a version above the range, or a version below it all reject the plugin with a clear error instead of a crash. TableProMinAppVersion is checked next, then the bundle’s principal class must conform to TableProPlugin.
ABI compatibility
TableProPluginKit builds with Swift Library Evolution, so its public ABI is resilient: a plugin built against an older PluginKit keeps loading under a newer app, and the runtime fills protocol requirements the plugin does not implement from their defaults. If you change PluginKit itself, the rules are:- Additive, no version bump: a new protocol requirement with a default implementation, a new field on a non-frozen struct added through a new initializer overload, reordering requirements.
- Breaking, bump required: changing an existing requirement’s signature, removing a requirement, adding a requirement without a default, adding a case to a
@frozenenum, changing a@frozentype’s layout. BumpcurrentPluginKitVersionand every plugin’sTableProPluginKitVersion, then re-release all registry plugins. - Never remove a published requirement, even one that defaulted to
nil. Library Evolution fills in requirements added after a plugin was built, but it cannot rescue one removed out from under it. Removing a requirement deletes its method descriptor and its default-implementation symbol, and every shipped plugin hard-references both in its witness table, so it fails to load with “Bundle failed to load executable”. That shipped on 0.58 and broke MongoDB, Oracle, Cassandra, and Elasticsearch (#1917). If the app stops using a requirement, leave it in place with its default. - Never add a parameter to an existing public initializer or function, even with a default value. It replaces the mangled symbol and every already-built plugin fails to load. Add a new overload and mark the old one
@_disfavoredOverload.
scripts/check-pluginkit-abi.sh before merging any change under Plugins/TableProPluginKit/. It builds the public interface at your tree and at the base ref and diffs them. Commit or stash first; the script exits if the working tree is dirty. The base ref is an optional argument and defaults to origin/main, so pass the merge base when you review a branch.
Test locally
The app derives the required signing team from its own signature, so a plugin built by the same Xcode as your debug app passes verification. Build the plugin target in Xcode, then install it into the user plugins directory and relaunch TablePro:xcodebuild (that writes to build/Debug instead). Do not patch the copied Info.plist: any edit invalidates the signature and dyld refuses to load the executable. See Testing a Custom Plugin for the Copy Plug-Ins build phase and the unsigned-plugin escape hatch.
Distribute
Publishing to TablePro’s registry takes two steps in this repo:- Add a case for your plugin to
resolve_plugin_info()in.github/workflows/build-plugin.yml: target name, bundle ID, display name, summary, database type IDs, icon, category, homepage. Without it the workflow exits withUnknown plugin name. - Push the tag
plugin-<name>-v<version>. CI builds both architectures, signs, notarizes, and updatesplugins.json.
metadata block, and the publishing flow.