A native macOS database client (SwiftUI and 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 guide covers the macOS and iOS apps; another platform's app keeps its own guide beside its code. Claude Code loads it through CLAUDE.md, Codex directly.
- 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 layer, no web view for native UI. Follow the documented Apple API and the HIG.
- Fix the cause, not the symptom. Diagnose (add OSLog if needed), then fix the cause. No workaround left behind.
- Clean architecture. Separation of concerns, protocol-oriented design, dependency injection where it earns its place. Weigh each change's effect on the design.
- Tests define behavior. Every testable change has a test. When a test fails, fix the code, never the test. A test that pins behavior the PR changes on purpose is updated in the same PR and named in its description.
- Open plugin domain.
DatabaseTypeis a string-backed struct (static constants,allKnownTypes), so a future plugin's type round-trips. Aswitchover an open type keeps adefault:; one over a closed enum stays exhaustive.
Every capability is core, a module or a plugin. The first rule that fits decides:
- Used weekly by app or backend developers or AI-first users, or only a row that runs nothing until clicked: core, no toggle.
- UI for one engine: core, shown only for drivers that support it.
- A database driver or a heavy native dependency: plugin, registry-shipped, signed by TablePro.
- Any other advanced tool needing a session, the grid, the editor or native UI: module, compiled in, toggled as a unit.
- A duplicate, or not worth maintaining: delete it, never park it in the registry.
- A new engine ships only as a registry plugin: no engine-specific code in
TablePro/; engine UI uses a PluginKit capability or plugin metadata. - A new advanced tool ships as a module, off by default.
- A module PR declares its id, default, off-state sink, residue test (a behavior test) and docs page.
- An off module leaves nothing behind: no menu item, key equivalent, context item, Settings section or Keyboard row; no launch work, observer, timer, listener or child process; no Keychain read or network call. A URL, AppleScript or MCP request for it gets an error naming where to enable it; only a click in TablePro enables it.
TablePro/: the app (Core/,Views/,Models/,ViewModels/,Extensions/,Theme/).Plugins/: drivers and import/export formats as.tablepluginbundles, plusTableProPluginKit. Bundled plugins are the app'scopy: { destination: plugins }targets inproject.yml; the rest ship through TableProApp/plugins, and the app never offers a registry install or update over a bundled one.Packages/:TableProCore(both apps),TableProMacCore(macOS-only modules; never in the iOS project),TableProOracle,TableProEditor,TableProGrammars.TableProCore's PluginKit source is a symlink: editPlugins/TableProPluginKit/only. Each manifest sets its own Swift language mode, so never passSWIFT_VERSION=toxcodebuild.- Tests:
TableProTests/,TableProUITests/,Plugins/*Tests/,Packages/*/Tests/,TableProMobile/TableProMobileTests/; plans inTestPlans/. Which holds what:.claude/rules/tests.md. Native/: first-party Rust and Go bridges (.claude/rules/native-bridges.md).Libs/: static libraries and iOS xcframeworks fromscripts/download-libs.sh, not in git.- Docs site: TableProApp/docs, cloned beside this repo as
../tablepro-docs, under its ownAGENTS.mdandSTYLE.md. TablePro.xcodeprojandTableProMobile/TableProMobile.xcodeprojare generated by XcodeGen from theirproject.yml, gitignored except theirPackage.resolved; never hand-edit them. Signing overrides go inConfigs/Secrets.xcconfig.
- Plugins.
PluginManagerloadsDriverPluginbundles;DatabaseDriverFactoryfinds one bypluginTypeId;PluginDriverAdapterbridges itsPluginDatabaseDrivertoDatabaseDriver;DatabaseManagerowns sessions. A new requirement gets a default implementation, then the adapter (.claude/rules/plugin-system.md). - Main window.
MainContentCoordinator, split intoViews/Main/Extensions/; oneConnectionWorkspaceper connection (.claude/rules/connection-window.md). - Change tracking.
DataChangeManagerholds edits behindAnyChangeManager;SQLStatementGeneratorwrites them on Save. Undo:StructureChangeManager'sUndoManager,ConnectionWorkspace.undoManager. - Editor and theme.
TableProThemeholds slots, fallbacks, the file format andThemeResolver(picks the light and dark pair).ThemeCatalogowns and syncs the theme files;ThemeEngineowns the mode and theme ids, follows the catalog, and holds the palette and font caches; fonts come fromTypographySettings. A theme colors content and window backdrop, never chrome (.claude/rules/theme.md).QueryCompletionAdapterbridgesCompletionEngine;EditorTabStripdraws tabs (.claude/rules/editor.md). - Storage. Passwords in the Keychain, preferences in UserDefaults, query history in SQLite FTS5, tab state as JSON (
.claude/rules/data-sync-storage.md).
scripts/download-libs.sh # first time, and after a libs update
scripts/generate-project.sh [both|macos|ios] # needs the libs; rerun after editing a project.yml or Configs/, or adding, moving or deleting a source file
.claude/skills/fix-issue/scripts/verify.sh build
.claude/skills/fix-issue/scripts/verify.sh test [Test...] # no Test: the whole Unit plan
.claude/skills/fix-issue/scripts/verify.sh lint <file.swift>...verify.shlogs to disk and printsPASS,FAILorINCONCLUSIVE, exit 0, 1 or 2; 3 is a usage error that ran nothing.- A Test is
<Suite>(a Swift type name),<Suite>/<case>(), or either behind its target,<Target>/<Suite>. INCONCLUSIVEis never a pass.- A run that executed nothing is
FAIL, unless a named environment cause makes itINCONCLUSIVE. verify.sh --helplists every step. Steps per change:.claude/skills/fix-issue/references/platform-apple.md, Verify. Test names, verdicts, remedies, red CI checks:.claude/skills/fix-issue/references/verification-apple.md.- A raw
xcodebuildpasses-skipPackagePluginValidation, plus-testPlan UnitorUIfor macOS tests. Raw commands:CONTRIBUTING.md, Tests. - The macOS plans own their run settings: never add
-parallel-testing-enabled,-test-iterationsor-retry-tests-on-failure, except to reproduce a flake (.claude/rules/tests.md). iOS tests have no plan and pass-parallel-testing-enabled NO(.claude/rules/ci.md, iOS and Package Tests). - Releases:
RELEASING.mdand thereleaseskill.
- AppKit lifecycle.
main.swiftstarts the app;MainMenuBuilder.installbuilds the menus inapplicationWillFinishLaunching; windows areNSWindowControllers. A SwiftUIApprewritesNSApp.mainMenu: never add one. - A refresh never clears what it refreshes. Fetch, then commit over the old value;
.loadingonly when nothing is loaded; keep good data on failure;prepareForReloadto reload,invalidateon disconnect. - A canceled connect keeps running.
Task.cancel()cannot interrupt a blocking C call: poll the connect or resume throughrunCancellableBlocking(the late call closes its handle), then check theConnectionAttemptRegistrygeneration before adopting a driver. - Storage resolves through
AppStorageEnvironment.shared(stores, defaults suite, keychain); thestorage_environment_*SwiftLint rules fail other Application Support or standard-defaults use. - An AppKit autosave name (split view, window frame, table) goes through
SplitViewAutosaveName.current(_:): AppKit writes the record to the real defaults domain, which the sandbox does not redirect. - No new test-only branch in production code: tests use injection or
TABLEPRO_UI_TEST_SANDBOX. - macOS files the iOS app compiles (listed in
TableProMobile/project.yml) give each top-level declaration explicit isolation, usuallynonisolated, since iOS defaults toMainActor(scripts/ci/check-ios-shared-isolation.py). - No package product in a macOS binary depends on the
TableProPluginKitpackage: a plugin would carry its own PluginKit types (.claude/rules/plugin-system.md). - A source-text rule is a SwiftLint rule or a
scripts/cicheck, never a test reading source.
.swiftlint.ymlis the source of truth. Never runswiftlint --fix.- Comments say why, never what. A short
///only where code would surprise a reader (a platform quirk, an invariant, a workaround's reason). Delete what-comments in code you touch. - Early returns with
guard, small functions, self-explanatory names. - Explicit access control, declared on the extension.
- No force unwrap or force cast.
- Log with
Logger(subsystem: "com.TablePro", category: ...), neverprint(). - Imports: one alphabetical block (
sorted_imports). - Booleans start with
is,has,canorshould; factories withmake; acronyms all caps (SQLStatementGenerator). - Near a size limit, split into
TypeName+Category.swiftby domain.file_lengthskips comment-only lines: trustswiftlint lint --strict --no-cache <file>, notwc -l. - Formatting beyond SwiftLint:
CONTRIBUTING.md, Code style.
- 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 line can hold millions of characters: cap regex and highlight ranges at 10k.
- Tests. Every testable change has a test in the target owning the code. Read
.claude/rules/tests.mdbefore creating a test file: a path rule loads only once a matching file is open. A UI flow only for a new or changed end-to-end flow (.claude/rules/ui-tests.md). - CHANGELOG.md. A user-visible change gets one fragment under
[Unreleased]; format:.claude/rules/changelog.md. - Localization.
String(localized:)outside SwiftUI literals, never interpolated:String(format: String(localized: "Preview %@"), name). Technical terms stay unlocalized. A plugin or package string lives in the host app's catalog: add it withpython3 scripts/localization.py plugins --add, plus--target iosfor a package the iOS app links. Never hand-edit a.xcstrings. - Docs. A new shortcut, UI or settings change, or driver change gets a paired draft PR in TableProApp/docs naming this one; it merges after this one.
verify.sh docsruns its checks here first. - Lint each changed Swift file with
verify.sh lint. Runverify.sh agent-docswhenCLAUDE.md,AGENTS.md,CONTRIBUTING.md,RELEASING.md,.github/pull_request_template.mdor anything under.claude/changed. - Atomic API changes. A rename or signature change updates every caller and test in the same commit.
- Commits and PRs. The PR title is the squash commit subject; branch commits use the same format. The body follows
.github/pull_request_template.md, whose top comment holds the format, types and scopes. ThePR titlecheck (scripts/ci/check-pr-title.py) enforces the format and 72 characters, not scopes.
- All writing: short sentences, plain words, specific numbers and names, no em dash, no filler.
scripts/banned-words.txtis the list; never copy its terms.scripts/check-banned-words.sh --stagedchecks a staged change; thePR titlejob checks a PR's added lines and body.- One home per fact; others point to it by repo path and section. No drifting counts or timings; a plan is not a fact.
- Budgets: this file 16 KiB, a rule 200 lines, a
SKILL.md500, a skill description 1,536 characters. verify.sh agent-docschecks backticked names in the Every change 5 files. Never state a plugin count in prose.
- A PR merges on five green checks: macOS Tests, iOS and Package Tests, SwiftLint, Repo Hygiene and PR title. Only release deploy keys bypass them (
RELEASING.md, Keys and rulesets). - Read why a CI check failed before any re-run.
- Workflows:
.claude/rules/ci.md. Red checks:.claude/skills/fix-issue/references/verification-apple.md, Reading CI.
Claude Code loads a rule when it reads or edits a file the rule covers. Under Codex, before editing a file, read every rule whose row matches it. Each row is the rule's paths: with a trailing /**/* dropped; verify.sh agent-docs keeps them equal.
| Rule | Read before editing |
|---|---|
ai-mcp-security.md |
TablePro/Core/AI, TablePro/Core/MCP, Packages/TableProMacCore/Sources/TableProMCP |
changelog.md |
CHANGELOG.md |
ci.md |
.github/workflows, .github/actions, .github/scripts, .github/actionlint.yaml, .github/*baseline*, scripts/ci, .swiftlint.yml, **/.swiftlint.yml |
connection-window.md |
TablePro/Core/Services/Infrastructure, TablePro/Models/Connection, TablePro/Core/Database/DatabaseManager*.swift, Packages/TableProCore/Sources/TableProDatabase, TablePro/Views/Main |
data-grid.md |
TablePro/Views/Results, TablePro/Core/DataGrid, TableProTests/Views/Results |
data-sync-storage.md |
TablePro/Core/**/*Storage*.swift, TablePro/Core/**/*Sync*.swift, TablePro/Core/Database, TablePro/Core/Plugins/PluginMetadataRegistry*.swift, TablePro/Core/Services/Query, Packages/TableProCore/Sources/TableProSyncTransport, CloudKit |
editor.md |
Packages/TableProEditor, TablePro/Theme, TablePro/Views/Editor, TablePro/Views/RowInspector |
libs.md |
Libs, scripts/download-libs.sh, scripts/publish-libs.sh, scripts/publish-ios-libs.sh |
mongodb-driver.md |
Plugins/MongoDBDriverPlugin, Plugins/MQLExportPlugin |
mysql-driver.md |
Plugins/MySQLDriverPlugin, Packages/TableProCore/Sources/TableProMySQLCore, TableProMobile/**/*MySQL*, TableProMobile/**/*MariaDB* |
native-bridges.md |
Native, Plugins/DamengDriverPlugin, Plugins/HanaDriverPlugin, scripts/build-dameng.sh, scripts/build-hana.sh |
plugin-system.md |
Plugins, Packages/TableProCore/Sources/TableProPluginKit, Packages/TableProCore/Sources/TableProMySQLCore, Packages/TableProCore/Sources/TableProPostgreSQLCore, Packages/TableProCore/Sources/TableProRedisCore, Packages/*/Package.swift, project.yml, TableProMobile/project.yml, .github/workflows/build-plugin.yml, .github/plugin-registry.json, scripts/*plugin* |
redis-driver.md |
Plugins/RedisDriverPlugin, Packages/TableProCore/Sources/TableProRedisCore |
split-views.md |
TablePro/**/*Split*.swift, TablePro/Core/Services/Infrastructure/WorkspacePane*.swift |
tests.md |
TableProTests, TableProUITests, Plugins/*Tests, Packages/*/Tests, TableProMobile/TableProMobileTests, TestPlans, scripts/ci/check-test-plans.py, scripts/clean-test-residue.sh |
theme.md |
TablePro/Theme, TablePro/Views, TablePro/Core/Services/Infrastructure/*Window*, Packages/TableProCore/Sources/TableProTheme |
ui-lifecycle.md |
TablePro/Views, TablePro/ViewModels, TablePro/Core/Services/Infrastructure, Packages/TableProEditor |
ui-tests.md |
TableProUITests, TestPlans/UI.xctestplan |
welcome-library.md |
TablePro/Views/Welcome, TablePro/ViewModels/WelcomeViewModel*.swift, Packages/TableProCore/Sources/TableProConnectionLibrary |
fix-issue takes a problem to one green PR and holds verify.sh and its references; release ships a version. Both live in .claude/skills/; Codex sees them through .agents/skills/.