Plugin Development
A TablePro database driver is a.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 forty 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, removing a requirement that defaulted to
nil. - Breaking, bump required: changing or removing an existing requirement’s signature, adding a requirement without a default, changing a
@frozentype’s layout. BumpcurrentPluginKitVersionand every plugin’sTableProPluginKitVersion, then re-release all registry plugins. - 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.
Test locally
A locally built plugin is not signed by TablePro, so installing it like a registry plugin fails. Add it to the Copy Plug-Ins build phase instead, or allow unsigned plugins in a debug build. See Testing a Custom Plugin.Distribute
Registry plugins are published through CI: tagplugin-<name>-v<version>, and the workflow builds both architectures, signs, notarizes, and updates plugins.json. See Plugin Registry for the manifest format, the self-describing metadata block, and the publishing flow.