A native macOS database client (SwiftUI + AppKit), a lightweight alternative to TablePlus. macOS 13.0+, Swift 6 language mode for the app targets (Configs/Base.xcconfig), Universal Binary. TableProMobile/ is the iOS app.
This file is the project guide for every agent. Claude Code loads it through CLAUDE.md; Codex loads it directly. Area rules live in .claude/rules/ (index at the end), and Claude Code loads each one automatically when you touch a file its paths: names. Codex does not, so read the matching rule before editing those paths.
- Security first. Validate at system boundaries; never introduce injection, credential exposure or a widened permission by accident.
- Native only. AppKit, SwiftUI and system frameworks. No cross-platform layers, no web views for native UI. Follow the documented Apple API and the HIG, not a hand-rolled equivalent.
- Fix the cause, not the symptom. Diagnose (add OSLog if needed), then fix the actual cause. No temporary workarounds left in place.
- Clean architecture. Separation of concerns, protocol-oriented design, dependency injection where it earns its place. Consider the effect of every change on the design, not just the immediate problem.
- Tests define behavior. Every testable change has a test. When a test fails, fix the code, never the test.
- Open plugin domain.
DatabaseTypeis a string-backed struct, so unknown types from future plugins round-trip. Aswitchover an open type keeps adefault:; aswitchover a closed enum stays exhaustive so the compiler flags new cases.
TablePro/: the app.Core/(services, business logic),Views/,Models/,ViewModels/,Extensions/,Theme/.Plugins/: every database driver and import/export format is a.tablepluginbundle, plusTableProPluginKit, the shared framework. Bundled plugins are the targets in the app'scopy: { destination: plugins }phase inproject.yml; the rest are registry-only and ship through TableProApp/plugins. Some bundled plugins also have a registry entry ("bundled": truein.github/plugin-registry.json) so a fix can reach users before the next app release.Packages/: local SwiftPM packages (TableProCore,TableProOracle,TableProEditor,TableProGrammars). Each sets its own Swift language mode in its manifest;SWIFT_VERSIONin the xcconfig does not reach them, and passingSWIFT_VERSION=toxcodebuildreaches every package and reports their errors as yours.Packages/TableProCore/Sources/TableProPluginKitis a symlink: editPlugins/TableProPluginKit/only.Native/: first-party Rust and Go bridges (see.claude/rules/native-bridges.md).Libs/: prebuilt static libraries and iOS xcframeworks, downloaded byscripts/download-libs.sh, not in git.TablePro.xcodeprojandTableProMobile/TableProMobile.xcodeprojare generated fromproject.ymlandTableProMobile/project.ymlby XcodeGen. They are gitignored, except each one's trackedPackage.resolved. Never hand-edit them. Signing overrides and secrets go inConfigs/Secrets.xcconfig(template:Secrets.xcconfig.example).
scripts/download-libs.sh # first time, and after a libs update
scripts/generate-project.sh # after editing project.yml or Configs/, or adding, moving or deleting a source fileBuild, test and lint through the wrapper, which keeps the full log on disk and prints a short verdict (PASS, FAIL, INCONCLUSIVE):
.claude/skills/fix-issue/scripts/verify.sh build
.claude/skills/fix-issue/scripts/verify.sh test <SuiteType> [SuiteType...]
.claude/skills/fix-issue/scripts/verify.sh lint <file.swift> [...]It also has generate, uitest, package, ios, plugins, abi, l10n, docs and agent-docs; verify.sh --help lists them. The raw equivalents always pass -skipPackagePluginValidation:
xcodebuild -project TablePro.xcodeproj -scheme TablePro -configuration Debug build -skipPackagePluginValidation
xcodebuild -project TablePro.xcodeproj -scheme TablePro test -skipPackagePluginValidation -only-testing:TableProTests/<SuiteType>
swiftlint lint --strict <files>- Tests are Swift Testing.
-only-testingmatches the Swift type name of a suite (one case:<SuiteType>/<test>()), and a filter that matches nothing still printsTEST SUCCEEDED, so check the executed count. - A full
TableProTestsrun is not a gate: run the suites that own the types you changed. - Release builds:
scripts/build-release.sh arm64|x86_64|bothandscripts/create-dmg.sh; thereleaseskill runs the whole release. - SwiftLint (
.swiftlint.yml) is the enforced style and runs on every PR. Pass it file paths: a directory outside itsincluded:scope lints nothing and reports zero violations.
Plugins. PluginManager (Core/Plugins/) loads bundles at runtime. PluginDatabaseDriver and DriverPlugin live in TableProPluginKit; PluginDriverAdapter bridges a plugin driver to the app's DatabaseDriver; DatabaseDriverFactory finds a plugin by DatabaseType.pluginTypeId; DatabaseManager owns sessions and is what views and coordinators talk to. A new protocol method goes on PluginDatabaseDriver with a default implementation, then into PluginDriverAdapter. PluginKit is ABI-resilient and its compatibility rules are in .claude/rules/plugin-system.md; read them before touching it.
DatabaseType. A string-backed struct, not an enum. Use the static constants (.mysql, .postgresql) for known types and DatabaseType.allKnownTypes for the canonical list.
Main window. MainContentCoordinator is the central coordinator, split into extension files in Views/Main/Extensions/. Each connection a window hosts is a ConnectionWorkspace with its own phase; see .claude/rules/connection-window.md.
Change tracking. A cell edit goes to DataChangeManager; Save turns it into statements through SQLStatementGenerator; AnyChangeManager abstracts the concrete managers. Undo comes from StructureChangeManager's private UndoManager and ConnectionWorkspace.undoManager.
Editor. ThemeEngine owns the active theme, editor colors and fonts. CompletionEngine is framework-agnostic and QueryCompletionAdapter bridges it to the editor. Editor tabs are drawn by EditorTabStrip, not native window tabs. Details: .claude/rules/editor.md.
Storage. Passwords in the Keychain, preferences in UserDefaults, query history in SQLite FTS5, tab state as JSON. The full table of which store owns what is in .claude/rules/data-sync-storage.md.
- The app runs the AppKit lifecycle.
main.swiftstarts it,MainMenuBuilder.installbuilds the menu bar inapplicationWillFinishLaunching, and every window is anNSWindowController. Never add a SwiftUIApp: it rewritesNSApp.mainMenuafter launch. - A refresh never clears the cache it refreshes. Fetch, then commit over the old value. Enter
.loadingonly when nothing is loaded, keep the good data when a refresh fails, and useprepareForReloadfor a reload, keepinginvalidatefor disconnect or a database switch. - Canceling a connect does not stop the driver.
Task.cancel()cannot interrupt a blocking C call, so a connect must be abortable (poll it, or resume throughrunCancellableBlockingand let the late call close its own handle), and every attempt checks itsConnectionAttemptRegistrygeneration before adopting a driver.
.swiftlint.yml is the source of truth. Beyond it:
- Comments say why, never what. A short
///(one or two lines) belongs where the code would surprise a careful reader: a platform quirk, an invariant, a workaround and the reason for it. Never narrate what the code does, cite tickets, or describe callers. Remove a what-comment when you touch its code. - Early returns with
guard; small focused functions; self-explanatory names. - Explicit access control, declared on the extension rather than on each member.
- No force unwrapping or force casting.
- OSLog (
Logger(subsystem: "com.TablePro", category: "...")), neverprint(). - Imports: system frameworks alphabetically, then third-party, then local.
- Approaching a SwiftLint size limit: extract into
TypeName+Category.swift, grouped by domain.
- Never
ForEach($bindable.array) { $item in }on an@Observablearray that can shrink: index bindings crash. UseForEach(array)and a manual binding. - On large strings, use
(string as NSString).lengthandcharacter(at:), never.countorindex(_:offsetBy:)in a loop. - Never call
ensureLayout(forCharacterRange:); it defeats non-contiguous layout. - A SQL dump can have one line of millions of characters: cap regex and highlight ranges at 10k characters.
- Tests. Unit tests for testable behavior;
TableProUITestsautomation for a user flow that runs deterministically, or the reason in the PR. UI suites subclassUITestCase. Details:.claude/rules/tests.md. - CHANGELOG.md. A user-visible change gets one fragment under
[Unreleased], in the existing canonical section. Format:.claude/rules/changelog.md. - Localization.
String(localized:)for user-facing strings outside SwiftUI literals, never with interpolation (useString(format: String(localized: "Preview %@"), name)). Do not localize technical terms. Plugin messages must be in the app catalog:python3 scripts/localization.py plugins --add. - Docs. A new shortcut, UI or settings change, or driver change updates
docs/(Mintlify). Followdocs/STYLE.md. - Lint the changed Swift files with
swiftlint lint --strict. - Atomic API changes. A rename or signature change updates every caller and test in the same commit.
- Commits and pull requests. Squash merges use the PR title as the commit subject, so the title is the record: Conventional Commits (
<type>(<scope>): <description>,!for breaking), at most 72 characters, a canonical scope when one fits. CI checks it. Branch commits follow the same format; put the explanation in the PR description, which follows.github/pull_request_template.md.- Types:
feat,fix,refactor,perf,test,docs,build,ci,chore,style,revert. Thereleaseskill alone writesrelease: v<version>. - Scopes:
ai-chat,ai-providers,mcp,copilot,inline-suggest,editor,datagrid,tabs,coordinator,sidebar,connections,connection-form,welcome,settings,toolbar,hig,ssh,ios,windows,perf,launch,plugins,plugin-<name>,changelog,claude-md,docs,ci,release.
- Types:
For everything: docs, commits, CHANGELOG, UI strings, errors, PR descriptions. Short sentences, plain words, specific numbers and names. No em dashes, no filler. scripts/banned-words.txt is the list, and scripts/check-banned-words.sh --staged checks the added lines of a staged change.
- PRs:
macos-tests.yml,ios-tests.yml,docs.yml,swiftlint.yml(pinned,--strict),pr-title.yml(Conventional Commits, at most 72 characters) andrepo-hygiene.yml(actionlint, shellcheck and the repo's source-scanning checks). - Releases:
build.ymlruns onv*tags and produces the DMG, the ZIP and the Sparkle feed, with notes taken fromCHANGELOG.md. - Plugins:
build-plugin.ymlruns onplugin-<slug>-v*tags, where the slug is a key in.github/plugin-registry.json.
| Rule | Covers |
|---|---|
ai-mcp-security.md |
TablePro/Core/AI, TablePro/Core/MCP, the external API docs |
changelog.md |
CHANGELOG.md |
connection-window.md |
connection windows, workspaces, sessions, DatabaseManager |
data-grid.md |
the data grid under TablePro/Views/Results |
data-sync-storage.md |
storage, CloudKit sync, the database core |
docs-authoring.md |
docs/ |
editor.md |
Packages/TableProEditor, TablePro/Theme, editor views |
libs.md |
Libs/ and the libs scripts |
mongodb-driver.md, mysql-driver.md, redis-driver.md |
those drivers |
native-bridges.md |
Native/ and the Dameng and HANA plugins |
plugin-system.md |
Plugins/, TableProPluginKit, project.yml, plugin CI |
split-views.md |
split view controllers and workspace panes |
tests.md |
TableProTests, TableProUITests |
ui-lifecycle.md |
views, view models, UI tests |
welcome-library.md |
the welcome window's connection list |
Skills live in .claude/skills/; Codex sees them through .agents/skills/. fix-issue takes an issue to a pull request; release ships a version.