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
2 changes: 1 addition & 1 deletion resources/blog/open-source-db-clients-2026.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ Status: actively maintained. Used by enterprise data teams that need schema docu

### TablePro (AGPLv3)

Native macOS app written in Swift and AppKit. 25 databases through a plugin architecture (MySQL, Postgres, MongoDB, Redis, ClickHouse, BigQuery, DynamoDB, Cloudflare D1, Turso, etc.). Free, open source, with paid features (license server, advanced AI usage).
Native macOS app written in Swift and AppKit. 28 databases through a plugin architecture (MySQL, Postgres, MongoDB, Redis, ClickHouse, BigQuery, DynamoDB, Cloudflare D1, Turso, etc.). Free, open source, with paid features (license server, advanced AI usage).

Best at: native macOS performance with broad database coverage. ~80 MB at idle, ~1 second cold start. AI assistant in the free tier. iOS app with iCloud sync.

Expand Down
84 changes: 42 additions & 42 deletions resources/data/comparisons.json

Large diffs are not rendered by default.

34 changes: 32 additions & 2 deletions resources/data/database-grid.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,16 @@
"category": "relational",
"href": "/oracle-client"
},
{
"name": "Dameng",
"icon": null,
"monogram": "DM",
"port": "5236",
"distribution": "plugin",
"driverGroup": "dameng",
"category": "relational",
"href": null
},
{
"name": "DuckDB",
"icon": "/images/databases/duckdb.svg",
Expand Down Expand Up @@ -179,6 +189,16 @@
"category": "document-kv",
"href": "/surrealdb-client"
},
{
"name": "Kafka",
"icon": null,
"monogram": "KA",
"port": "9092",
"distribution": "plugin",
"driverGroup": "kafka",
"category": "document-kv",
"href": null
},
{
"name": "Cloudflare D1",
"icon": "/images/databases/cloudflare-d1.svg",
Expand Down Expand Up @@ -220,15 +240,25 @@
"href": "/snowflake-client"
},
{
"name": "libSQL / Turso",
"icon": "/images/databases/turso.svg",
"name": "libSQL",
"icon": null,
"monogram": "LS",
"port": "API",
"distribution": "plugin",
"driverGroup": "libsql",
"category": "file-embedded",
"href": "/turso-client"
},
{
"name": "Turso",
"icon": "/images/databases/turso.svg",
"monogram": "TU",
"port": "API",
"distribution": "plugin",
"driverGroup": "libsql",
"category": "file-embedded",
"href": "/turso-client"
},
{
"name": "Beancount",
"icon": "/images/databases/beancount.svg",
Expand Down
5 changes: 3 additions & 2 deletions resources/js/components/landing/database-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import DatabaseMark from '@/components/ui/database-mark';
import SectionShell from '@/components/ui/section-shell';
import { CELL_DENSITY, cellBorders, GridCell, type ColumnMap } from '@/components/ui/grid-cell';
import { GITHUB_REPO_URL } from '@/data/links';
import { BUNDLED_ENGINE_COUNT, ENGINE_COUNT, ON_DEMAND_ENGINE_COUNT } from '@/data/engines';

interface DatabaseTile {
name: string;
Expand Down Expand Up @@ -182,9 +183,9 @@ export default function DatabaseGrid() {
tone="raised"
id="databases"
label="Databases"
headline="25 databases."
headline={`${ENGINE_COUNT} databases.`}
headlineMuted="One native driver each. No JDBC."
lede="Nine drivers ship inside the app. The other sixteen download the first time you pick one, signature verified, with no restart."
lede={`${BUNDLED_ENGINE_COUNT} drivers ship inside the app. The other ${ON_DEMAND_ENGINE_COUNT} download the first time you pick one, signature verified, with no restart.`}
>
<FullLine />
<Container>
Expand Down
3 changes: 2 additions & 1 deletion resources/js/components/landing/hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Button from '@/components/ui/button';
import { AppleGlyph } from '@/components/ui/glyph';
import { trackDownload } from '@/lib/analytics';
import { GITHUB_REPO_URL } from '@/data/links';
import { ENGINE_COUNT } from '@/data/engines';

interface Props {
githubStars?: number | null;
Expand Down Expand Up @@ -89,7 +90,7 @@ export default function Hero({ githubStars, latestRelease }: Props) {
* sections below read as evidence for a claim already made.
*/}
<p className="max-w-[58ch] px-4 py-5 text-lg text-muted-foreground text-pretty sm:py-6">
TablePro is a free, open source database client for macOS. 25 engines through native drivers, and
TablePro is a free, open source database client for macOS. {ENGINE_COUNT} engines through native drivers, and
nothing runs against your database that you have not read first.
</p>
<FullLine />
Expand Down
3 changes: 2 additions & 1 deletion resources/js/components/landing/spec-strip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import DataTable, { TABLE_COLUMN_RULE, TABLE_ROW_RULE } from '@/components/ui/da
import FootNote from '@/components/ui/footnote';
import { FullLine } from '@/components/ui/full-line';
import { GITHUB_REPO_URL } from '@/data/links';
import { BUNDLED_ENGINE_COUNT, ENGINE_COUNT, ON_DEMAND_ENGINE_COUNT } from '@/data/engines';

interface Props {
latestRelease?: { version: string | null; publishedAt: string | null; countLast30Days: number | null } | null;
Expand Down Expand Up @@ -55,7 +56,7 @@ function formatReleaseDate(iso: string): string {
*/
export default function SpecStrip({ latestRelease, downloads }: Props) {
const specs: Spec[] = [
{ label: 'databases', value: '25', sub: '9 bundled, 16 on demand', numeric: true },
{ label: 'databases', value: String(ENGINE_COUNT), sub: `${BUNDLED_ENGINE_COUNT} bundled, ${ON_DEMAND_ENGINE_COUNT} on demand`, numeric: true },
{ label: 'cold_start', value: 'Under 1s', sub: 'Cold, to first window', numeric: true },
{ label: 'idle_rss', value: '~80 MB', sub: 'One connection, open and idle', numeric: true },
downloads?.total
Expand Down
56 changes: 56 additions & 0 deletions resources/js/data/engines.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import gridData from '../../data/database-grid.json';

/**
* Every engine-count claim on the site, derived from one dataset.
*
* The figure was written out as "25" in eight places — the hero, the grid
* headline and its lede, the spec strip, the licence block, an FAQ answer, the
* JSON-LD publisher blurb and two fields on Home.tsx — plus roughly thirty more
* across `comparisons.json`. Three of those were stale in a way nobody could
* see from the source: the app had reached 28 registered types while the site
* still said 25 and the docs said 27.
*
* The trap in the old copy was that "25" was not arbitrary. It counted distinct
* `driverGroup` values in `database-grid.json`, which is a *driver* count, while
* the headline above it promised *databases*. Two different quantities wearing
* the same number, so raising one silently broke the other.
*
* These derive from the grid instead. The grid is what a reader can actually
* count on the page, so a claim that disagrees with it is a claim the page
* disproves two rows down. `EngineCountTest` pins the derived value, so adding
* a tile is a deliberate change to the marketing copy rather than a side effect.
*/
interface DatabaseTile {
distribution: 'builtin' | 'plugin';
}

const tiles = gridData as DatabaseTile[];

/**
* Engines a connection can be opened against — the app's registered type ids,
* one grid tile each.
*
* Turso was the open question and is now counted. It had its own `DatabaseType`
* and its own `/turso-client` page but no snapshot, resolving through
* `reverseTypeIndex["Turso"] = "libSQL"`, so it shared a `libSQL / Turso` tile
* and the app returned 28. It now follows the ScyllaDB precedent — an alias that
* also carries an entry of its own — so libSQL and Turso are two tiles over one
* driver, exactly as Cassandra and ScyllaDB are.
*/
export const ENGINE_COUNT: number = tiles.length;

/** Drivers compiled into the app, ready on first launch. */
export const BUNDLED_ENGINE_COUNT: number = tiles.filter((tile) => tile.distribution === 'builtin').length;

/** The rest, fetched and signature-verified the first time one is picked. */
export const ON_DEMAND_ENGINE_COUNT: number = ENGINE_COUNT - BUNDLED_ENGINE_COUNT;

/**
* Driver plugin targets in the app repository, which is fewer than the engines
* they serve: PostgreSQL also drives Redshift, CockroachDB and PGlite, Cassandra
* drives ScyllaDB, and libSQL drives Turso.
*
* Nothing in this repository can derive this — it is a count of build targets in
* an application this site cannot see — so it is a literal, and the only one.
*/
export const DRIVER_PLUGIN_COUNT = 23;
14 changes: 11 additions & 3 deletions resources/js/data/faqs.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { DRIVER_PLUGIN_COUNT, ENGINE_COUNT } from '@/data/engines';

export interface FaqItem {
question: string;
answer: string;
Expand All @@ -23,7 +25,7 @@ export interface FaqItem {
export const faqs: FaqItem[] = [
{
question: 'Is it really free, or free for now?',
answer: 'Free, permanently. TablePro is AGPLv3 and the whole app works without a license, with no trial countdown and no per-Mac limit. All 25 databases, the SQL editor, the data grid, the AI assistant, the MCP server, Safe Mode with Touch ID, SSH tunnels, ER diagrams and XLSX export cost nothing. Starter adds seven: Compare & Sync, Query Insights, result charts, iCloud Sync, Linked Folders, encrypted connection export, and environment variables in connection fields. Team adds two more, the Team Catalog and the Team Library.',
answer: `Free, permanently. TablePro is AGPLv3 and the whole app works without a license, with no trial countdown and no per-Mac limit. All ${ENGINE_COUNT} databases, the SQL editor, the data grid, the AI assistant, the MCP server, Safe Mode with Touch ID, SSH tunnels, ER diagrams and XLSX export cost nothing. Starter adds seven: Compare & Sync, Query Insights, result charts, iCloud Sync, Linked Folders, encrypted connection export, and environment variables in connection fields. Team adds two more, the Team Catalog and the Team Library.`,
},
{
question: 'Can I use TablePro at work under AGPLv3?',
Expand All @@ -42,8 +44,14 @@ export const faqs: FaqItem[] = [
answer: 'In the macOS Keychain. Connection details live in a plain JSON file with no secrets in it. A connection can also skip storage entirely and resolve its password at connect time from a file, an environment variable, a shell command, 1Password, HashiCorp Vault or AWS Secrets Manager.',
},
{
question: 'Why do Cassandra and ScyllaDB appear separately when you say 25?',
answer: 'The grid shows 26 tiles because Cassandra and ScyllaDB are two entries in the connection chooser. They share one driver, which is why the driver count is 25.',
/*
* This used to reconcile 26 tiles against a claim of 25, because the
* headline quoted driver groups while the grid rendered entries. The
* grid now renders one tile per engine, so that gap is gone and the
* only one left is the honest one: a driver can serve several engines.
*/
question: `Why do Cassandra and ScyllaDB appear separately when you say ${ENGINE_COUNT}?`,
answer: `Because they are two entries in the connection chooser, and the grid shows one tile per entry. The ${ENGINE_COUNT} engines run on ${DRIVER_PLUGIN_COUNT} native driver plugins, because one driver can serve several engines: PostgreSQL also drives Redshift, CockroachDB and PGlite, Cassandra drives ScyllaDB, and libSQL drives Turso.`,
},
{
question: 'Is this abandoned like Sequel Pro?',
Expand Down
3 changes: 2 additions & 1 deletion resources/js/lib/structured-data.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { GITHUB_REPO_URL } from '@/data/links';
import { ENGINE_COUNT } from '@/data/engines';

export interface BreadcrumbCrumb {
name: string;
Expand Down Expand Up @@ -42,7 +43,7 @@ export function buildOrganizationJsonLd(baseUrl: string): object {
height: 256,
},
description:
'TablePro builds a native database client for macOS, iPadOS and iOS covering 25 engines, released as open source under AGPLv3.',
`TablePro builds a native database client for macOS, iPadOS and iOS covering ${ENGINE_COUNT} engines, released as open source under AGPLv3.`,
// All five profiles the footer links, not two of them.
sameAs: [
GITHUB_REPO_URL,
Expand Down
7 changes: 4 additions & 3 deletions resources/js/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import SEOHead from '@/components/seo/seo-head';
import { buildOrganizationJsonLd } from '@/lib/structured-data';
import { GITHUB_REPO_URL } from '@/data/links';
import { STARTER_PRICE, TEAM_SEAT_PRICE } from '@/data/pricing';
import { ENGINE_COUNT } from '@/data/engines';

interface LatestRelease {
version: string | null;
Expand Down Expand Up @@ -61,10 +62,10 @@ const HOME_TITLE = 'TablePro - Free, Open Source Database Client for Mac';
* answer engine lifts for "what is TablePro".
*/
const HOME_DESCRIPTION =
'TablePro is a free, open source database client for macOS. 25 engines through native drivers, in a 20 MB Swift app that opens in under a second.';
`TablePro is a free, open source database client for macOS. ${ENGINE_COUNT} engines through native drivers, in a 20 MB Swift app that opens in under a second.`;

const FEATURE_LIST = [
'25 databases through native drivers',
`${ENGINE_COUNT} databases through native drivers`,
'SQL editor with tree-sitter highlighting and Vim mode',
'Editable data grid with deferred commit',
'13 AI providers, bring your own key',
Expand Down Expand Up @@ -121,7 +122,7 @@ function buildAppJsonLd(
softwareRequirements: 'macOS 14.0 or later, Apple Silicon or Intel',
description: HOME_DESCRIPTION,
disambiguatingDescription:
'TablePro is a native macOS GUI client for 25 database engines, written in Swift rather than Electron or Java, and released as open source under AGPLv3.',
`TablePro is a native macOS GUI client for ${ENGINE_COUNT} database engines, written in Swift rather than Electron or Java, and released as open source under AGPLv3.`,
// Every URL from canonicalBaseUrl. Four of these used to hardcode the
// production origin while `screenshot` did not, so one object carried
// two origins on any non-production host.
Expand Down
Loading
Loading