Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added public/og/compare/tableplus.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
151 changes: 151 additions & 0 deletions resources/data/comparisons.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,155 @@
[
{
"slug": "tableplus",
"name": "TablePlus",
"tagline": "Free, open source TablePlus alternative for Mac.",
"description": "TablePlus is $99 a device and closed source. TablePro is free, open source under AGPLv3, and covers 25 databases. Both are native Mac apps — that part is a tie.",
"longDescription": "TablePlus is the closest thing to TablePro on the Mac: a genuinely native AppKit app, fast, well made, and shipping since 2019. It also has an LLM chat panel and, since June 2026, an MCP server — so the usual \"native versus Electron\" and \"we have AI\" arguments do not apply here. What separates them is the licence. TablePlus costs $99 for one device or $129 for two, includes one year of updates, and asks $59 per device to keep receiving them. TablePro is free, AGPLv3, and the source is public.",
"rows": [
{
"label": "Price",
"tablePro": "Free",
"competitor": "$99 one device, $129 two"
},
{
"label": "Updates",
"tablePro": "Always free",
"competitor": "1 year, then $59 per device"
},
{
"label": "Open Source",
"tablePro": true,
"competitor": false
},
{
"label": "Technology",
"tablePro": "Native (SwiftUI)",
"competitor": "Native (AppKit)"
},
{
"label": "Databases",
"tablePro": "25 drivers",
"competitor": "16"
},
{
"label": "AI Assistant",
"tablePro": true,
"competitor": true
},
{
"label": "MCP Server",
"tablePro": true,
"competitor": true
},
{
"label": "Safe Mode",
"tablePro": "6 levels, per connection",
"competitor": "5 levels"
},
{
"label": "Touch ID",
"tablePro": true,
"competitor": true
},
{
"label": "SSH Tunneling",
"tablePro": true,
"competitor": true
},
{
"label": "Inline Editing",
"tablePro": true,
"competitor": true
},
{
"label": "Export Formats",
"tablePro": "CSV, JSON, XLSX, Markdown",
"competitor": "CSV, JSON, SQL"
},
{
"label": "ER Diagrams",
"tablePro": true,
"competitor": "Via plugin"
},
{
"label": "iOS App",
"tablePro": true,
"competitor": true
},
{
"label": "Windows and Linux",
"tablePro": false,
"competitor": true
}
],
"prosTablePro": [
"Free forever, against $99 a device",
"Updates never expire",
"Open source under AGPLv3",
"25 database drivers, against 16",
"Six Safe Mode levels, set per connection",
"XLSX export"
],
"prosCompetitor": [
"Windows and Linux builds",
"Available on Setapp",
"Perpetual licence — your last covered build keeps working",
"Shipping and refined since 2019",
"One seat covers any OS"
],
"chooseTablePro": "you want the same native Mac experience without a per-device licence, or you want to read the source.",
"chooseCompetitor": "you need Windows or Linux, you already own a licence, or you prefer paying once for a build you can freeze.",
"verdict": "TablePlus is a good native Mac client and the closest match to TablePro in design and speed. The difference is not performance and it is no longer AI — both ship an MCP server. It is the licence: $99 a device and closed, against free and AGPLv3.",
"keywords": [
"tableplus alternative",
"tablepro vs tableplus",
"tableplus vs tablepro",
"free tableplus alternative",
"open source tableplus alternative",
"tableplus alternative mac"
],
"ogTableProMetaHtml": "<strong>FREE</strong> · AGPLv3<br>25 databases<br>Native (SwiftUI)",
"ogCompetitorMetaHtml": "<strong>$99</strong> per device<br>+$59 to keep updates<br>Closed source",
"ogPunchline": "Both native. One is free and open source.",
"ogStatValue": "$0",
"ogStatLabel": "PER DEVICE",
"migrationSteps": [
{
"title": "Open the importer",
"description": "File > Import Connections, then pick TablePlus. The Setapp edition is detected as well, and TablePlus does not need to be running."
},
{
"title": "Passwords come across",
"description": "Connections, groups, SSH tunnels and SSL settings are read directly, and passwords are copied from the macOS Keychain into your own Keychain."
},
{
"title": "Check the tunnels",
"description": "Open one connection that uses an SSH tunnel and connect once. Everything else carries over untouched, so nothing needs retyping."
}
],
"faqs": [
{
"question": "Is TablePro slower than TablePlus?",
"answer": "No, and neither is meaningfully slower than the other. Both are native Mac apps — TablePro in SwiftUI and AppKit, TablePlus in AppKit — so neither pays the startup or memory cost of Electron or a JVM. Anyone claiming a large speed gap between two native clients is guessing."
},
{
"question": "Does TablePlus have AI and MCP?",
"answer": "Yes to both. TablePlus ships an LLM chat panel that you point at OpenAI, Anthropic, GitHub Copilot or a local model, and it added an MCP server in version 7.1.8 on 10 June 2026. TablePro has the same two capabilities. This is not a difference between them."
},
{
"question": "Why do you say 16 databases when TablePlus documents 13?",
"answer": "Their documentation lists 13 engines and their homepage shows 14 logos, and the two lists do not match — BigQuery, ClickHouse and Turso appear only on the homepage, Snowflake and Vertica only in the docs. Sixteen is the union of both, which is the most generous reading. TablePro ships 25 drivers."
},
{
"question": "What does the TablePlus licence actually cost over time?",
"answer": "A licence is $99 for one device or $129 for two, one time, and includes a year of updates. After that the app keeps working forever on the last build your term covered, with no feature lockout. To carry on receiving updates it is $59 per device. The bundled iOS unlock, however, stops when the term lapses."
},
{
"question": "Can I import my TablePlus connections?",
"answer": "Yes. TablePro reads TablePlus connections directly, including the Setapp edition, and takes passwords from the macOS Keychain along with SSH tunnels and SSL settings. TablePlus does not need to be running, and groups and folders carry over."
}
]
},
{
"slug": "dbeaver",
"name": "DBeaver",
Expand Down
39 changes: 25 additions & 14 deletions resources/js/components/landing/compare-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,26 @@ import SectionShell from '@/components/ui/section-shell';
import { getComparisonBySlug, type ComparisonInfo } from '@/data/comparisons';

/**
* Three competitors, one per runtime: a JVM desktop app, a JVM IDE, and an
* Electron app. The point of the table is the category, not the vendor, which
* is also the shape least likely to compete with the ten /compare/* pages that
* each own their competitor's name.
* Four competitors: a JVM desktop app, a JVM IDE, an Electron app, and the one
* other native Mac client. The first three lose the runtime argument on the
* Technology row; TablePlus does not, and that is the honest reason it is here.
* Against TablePlus the row that separates the two is Price.
*
* TablePlus is deliberately absent. /compare/tableplus returns 410, and
* StaleClaimsTest fails the build on any landing component that links it. It
* appears in SwitchFrom as an import source only.
* Ordered so the runtime contrast reads first and the like-for-like comparison
* closes it.
*/
const SLUGS = ['dbeaver', 'datagrip', 'beekeeper-studio'] as const;
const SLUGS = ['dbeaver', 'datagrip', 'beekeeper-studio', 'tableplus'] as const;

/**
* Rendered where a competitor has no measurement for a metric.
*
* TablePlus ships without a `benchmarks` block, because it is native like
* TablePro and nothing published about either app's cold start survives
* scrutiny — the tidy figures in circulation trace back to competitor
* comparison pages, this site's included. An em-dash and a footnote say "not
* measured", which is true; a number here would not be.
*/
const UNMEASURED = '—';

interface Metric {
label: string;
Expand All @@ -39,7 +49,7 @@ const METRICS: Metric[] = [
*/
function valueFor(comparison: ComparisonInfo, metric: Metric, side: 'tablePro' | 'competitor'): string {
if (metric.benchmark) {
return comparison.benchmarks?.[side][metric.benchmark] ?? '';
return comparison.benchmarks?.[side][metric.benchmark] ?? UNMEASURED;
}

const row = comparison.rows.find((candidate) => candidate.label === metric.row);
Expand All @@ -66,7 +76,7 @@ export default function CompareTable() {
tone="raised"
id="compare"
label="Comparison"
headline="TablePro, DBeaver, DataGrip and Beekeeper Studio."
headline="TablePro, DBeaver, DataGrip, Beekeeper Studio and TablePlus."
headlineMuted="Startup, memory, runtime and price."
>
<FullLine />
Expand All @@ -77,10 +87,10 @@ export default function CompareTable() {
* and FullLine stays outside it, since its 200vw bleed would
* add scroll width in here.
*/}
<div className="overflow-x-auto" tabIndex={0} role="region" aria-label="TablePro compared with three other clients">
<div className="overflow-x-auto" tabIndex={0} role="region" aria-label="TablePro compared with four other clients">
<DataTable
className="min-w-[40rem]"
caption="Cold start, idle memory, runtime and price, compared across TablePro, DBeaver, DataGrip and Beekeeper Studio."
className="min-w-[48rem]"
caption="Cold start, idle memory, runtime and price, compared across TablePro, DBeaver, DataGrip, Beekeeper Studio and TablePlus."
>
<thead>
<tr>
Expand Down Expand Up @@ -140,7 +150,8 @@ export default function CompareTable() {
<Container>
<p className="px-4 py-3 font-mono text-xs text-muted-foreground">
Every competitor figure comes from its own comparison page, which is where the version tested is
written down.
written down. TablePlus is native like TablePro, and its cold start and idle memory are not
measured here rather than estimated.
</p>
</Container>
<FullLine />
Expand Down
1 change: 1 addition & 0 deletions resources/js/components/landing/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const columns = [
{ label: 'Features', href: '/#features' },
{ label: 'Pricing', href: '/#pricing' },
{ label: 'Download', href: '/download' },
{ label: 'vs TablePlus', href: '/compare/tableplus' },
{ label: 'vs DBeaver', href: '/compare/dbeaver' },
{ label: 'vs DataGrip', href: '/compare/datagrip' },
{ label: 'vs Beekeeper Studio', href: '/compare/beekeeper-studio' },
Expand Down
10 changes: 5 additions & 5 deletions resources/js/components/landing/switch-from.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ const SOURCES: ImportSource[] = [
*
* This section used to close with ten links to /compare/*, which is ten routes
* out of the page at the exact moment the reader has been told that moving costs
* them nothing. CompareTable directly below carries three of them with better
* anchor text, and the footer carries all ten — so the pages keep their inbound
* links and this section stops being an exit.
* them nothing. CompareTable directly below carries four of them with better
* anchor text, and the footer carries all eleven — so the pages keep their
* inbound links and this section stops being an exit.
*
* The H2 names two competitors without linking either, which is the same keyword
* capture at no exit cost. TablePlus stays a plain mention: /compare/tableplus
* returns 410, and StaleClaimsTest fails the build on a component that links it.
* capture at no exit cost. Both are linked from CompareTable one section down,
* so a link here would only be a second route out of the same screen.
*/
export default function SwitchFrom() {
return (
Expand Down
4 changes: 1 addition & 3 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,8 @@
->where('slug', '[a-z0-9-]+')
->name('landing.blog.show');

Route::get('/compare/tableplus', fn() => abort(410))->name('landing.compare.tableplus.gone');

Route::get('/compare/{slug}', [LandingController::class, 'compare'])
->where('slug', 'dbeaver|datagrip|navicat|beekeeper-studio|sequel-pro|postico|sequel-ace|heidisql|azimutt|phpmyadmin')
->where('slug', 'tableplus|dbeaver|datagrip|navicat|beekeeper-studio|sequel-pro|postico|sequel-ace|heidisql|azimutt|phpmyadmin')
->name('landing.compare');

Route::get('/{slug}', [LandingController::class, 'databaseClient'])
Expand Down
39 changes: 25 additions & 14 deletions tests/Feature/Landing/CompareTableTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ function comparisonEntries(): array
return json_decode(file_get_contents(base_path('resources/data/comparisons.json')), true);
}

it('renders three competitors that all exist in the data', function (): void {
it('renders four competitors that all exist in the data', function (): void {
$slugs = array_column(comparisonEntries(), 'slug');

foreach (['dbeaver', 'datagrip', 'beekeeper-studio'] as $slug) {
foreach (['dbeaver', 'datagrip', 'beekeeper-studio', 'tableplus'] as $slug) {
expect($slugs)->toContain($slug);
}
});
Expand All @@ -39,11 +39,14 @@ function comparisonEntries(): array
* comparison page start disagreeing the next time the JSON changes.
*/
$entries = collect(comparisonEntries())
->whereIn('slug', ['dbeaver', 'datagrip', 'beekeeper-studio']);
->whereIn('slug', ['dbeaver', 'datagrip', 'beekeeper-studio', 'tableplus']);

foreach ($entries as $entry) {
// TablePlus ships no benchmarks; the table renders an em-dash for it.
foreach (['startup', 'memory'] as $metric) {
expect($source)->not->toContain($entry['benchmarks']['competitor'][$metric]);
if (isset($entry['benchmarks'])) {
expect($source)->not->toContain($entry['benchmarks']['competitor'][$metric]);
}
}

foreach ($entry['rows'] as $row) {
Expand All @@ -61,16 +64,24 @@ function comparisonEntries(): array
->not->toContain('~80 MB');
});

it('never links the comparison page that returns 410', function (): void {
it('says "not measured" rather than inventing a number', function (): void {
/*
* Anchored to an opening quote, so this catches a link and not a mention —
* the docblock in the component explains why TablePlus is absent, and that
* comment is the documentation of this rule. Same anchoring as the guard in
* StaleClaimsTest, for the same reason.
* TablePlus is native like TablePro, so its cold start and idle memory are
* close — and nothing published about either survives scrutiny, because the
* quotable figures in circulation trace back to competitor comparison
* pages, this site's own included. The entry therefore ships without a
* `benchmarks` block and the table renders an em-dash.
*
* This guards the honest half: that the fallback is a dash and not a
* plausible-looking number somebody added later to tidy the column up.
*/
PHPUnit\Framework\Assert::assertDoesNotMatchRegularExpression(
'#["\']/compare/tableplus#',
compareTableSource(),
'compare-table.tsx links a URL that returns 410 Gone',
);
$tableplus = collect(comparisonEntries())->firstWhere('slug', 'tableplus');

expect($tableplus)->not->toBeNull();
expect($tableplus)->not->toHaveKey('benchmarks');

$source = compareTableSource();

expect($source)->toContain("const UNMEASURED = '—'");
expect($source)->toContain('?? UNMEASURED');
});
5 changes: 1 addition & 4 deletions tests/Feature/Seo/SeoSmokeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ function getOnWebDomain(string $path)
it('keeps the comparison data shape stable with required SEO fields', function (): void {
$entries = json_decode(file_get_contents(__DIR__ . '/../../../resources/data/comparisons.json'), true);

expect($entries)->toHaveCount(10);
expect($entries)->toHaveCount(11);

foreach ($entries as $entry) {
expect($entry)
Expand All @@ -70,9 +70,6 @@ function getOnWebDomain(string $path)
}
});

it('returns 410 Gone for the retired /compare/tableplus URL', function (): void {
getOnWebDomain('/compare/tableplus')->assertStatus(410);
});

it('keeps the database data shape stable with required SEO fields', function (): void {
$entries = json_decode(file_get_contents(__DIR__ . '/../../../resources/data/databases.json'), true);
Expand Down
Loading
Loading