Calíope app icon

How Calíope is built

The stack, the boundaries, and what was left out on purpose.

This page says how Calíope is put together: which frameworks it stands on, where the engine boundary sits, what the sandbox allows, and what the app deliberately does not do. Every figure comes from the source tree as of October 2026.

One app, three binaries

Calíope ships as a single app, org.caliope.caliope, with universal purchase: one purchase covers every Apple device it runs on. Behind that single app there are three binaries: one for macOS, one for iPhone, iPad and Apple Vision Pro (the same target, built for iOS and for visionOS — the Apple Watch app travels embedded in the iOS build), and one for Apple TV.

Most of the code is not written three times. Sources/Shared holds 367 Swift files and 277 SwiftUI views that compile into both the Mac binary and the iOS binary. The widgets are extensions of the same app and read what the app publishes through an App Group; the app writes, the widget only reads.

SwiftUI end to end

Every window is native SwiftUI — no web views, no cross-platform emulation. That sentence is on the home page and it is literally true: the 277 shared views are SwiftUI, and where a Mac and an iPad need different behaviour the difference is decided when the code is compiled, inside the same file, not by a compatibility layer at run time.

The engine boundary

Everything that speaks to a database engine lives behind 16 protocols in Sources/Models/Database/Protocols/, grouped by sub-area. Each engine has one implementation of those protocols, and nothing above the boundary — no view, no orchestrator — writes SQL or knows an engine’s error codes. Adding an engine means implementing the protocols, not editing the callers.

Calíope speaks to nine engines, and the code that talks to them comes from two places.

  • Seven speak through a client I wrote. Six from the protocol their makers publish: MySQL and MariaDB over their client/server protocol, PostgreSQL over its frontend/backend protocol, SQL Server over TDS, MongoDB over OP_MSG and BSON, ClickHouse over its HTTP or HTTPS interface, and Redis over RESP2 and RESP3. Oracle Database speaks TNS: Oracle publishes no specification of it, so I write from the code it names as the public reference, its own Python driver. Each implements what Calíope uses and nothing more; the seven wire layers fit in about 18,700 lines.
  • Two are not a driver at all: the engine itself runs inside the app. SQLite is the library that ships with the system, and the SQLite editor talks to it through its C API. DuckDB 1.5.5 is DuckDB Labs’ own source, compiled into the app as a local package with a thin Swift layer over its C API, and it downloads no extensions — the ones that ship inside are all there is.

The network code runs on SwiftNIO 2.101.0, Apple’s open-source networking library; the ClickHouse client is the exception, written on URLSession (versions as of October 2026). Queries with parameters use the prepared statement of each engine’s own protocol — COM_STMT_PREPARE and COM_STMT_EXECUTE on MySQL and MariaDB, the extended query protocol (Parse, Bind, Execute) on PostgreSQL, sp_executesql over RPC on SQL Server, :1 binds in the execute call on Oracle Database, typed {name:Type} query parameters on ClickHouse —: the values travel through the wire protocol and are never concatenated into the SQL. What you type in the editor is sent as you typed it.

The MongoDB client covers OP_MSG, BSON, SCRAM-SHA-1 and SCRAM-SHA-256, and the SRV lookup behind mongodb+srv://, done with the system DNS, and it supports MongoDB 4.0 or later. MongoDB sits outside the engine boundary on purpose: no view touches that wire layer, and the whole MongoDB layer is designed so that it could be removed without touching the relational side. Redis lives outside it too, with its own session.

Why I write the drivers

A database app whose drivers are all written by one person deserves a question: who keeps them working when a server changes? Here is my answer, and what you can check.

It is not new to me. Before Calíope I wrote gb.mysql, the MySQL component of the Gambas language; Gambas took it in in 2008 and still includes it under my name. Calíope itself started that same year as a Qt app that talked to MySQL and MariaDB by calling their C API directly, through a database layer I wrote; its source is on the Legacy Qt builds page. Of everything in this job, the layer between an application and its database is the part I have done the longest.

I write one when depending on someone else is the bigger risk. Calíope used a community MongoDB driver through version 1.5. When its development stalled, I replaced it with my own.

A narrow driver is a small one. A general-purpose driver has to serve every application; mine serve one. That is why they are small enough for me to know what every part does.

Owning the client means owning its behaviour. Every server engine gets the same TLS policy, verified by default, and the same SSH tunnel, and their errors are sorted the same way: an answer from the server is never mistaken for a lost connection.

They are tested against real servers, in containers: MySQL 8.0, 8.4 and 26.7, MariaDB at its latest release (whichever is current when I refresh the test servers), PostgreSQL 17 with PostGIS 3.5, SQL Server 2025, Oracle Database 26ai Free, ClickHouse 26.8, MongoDB 8.2 and Redis 7.4.

On MySQL and MariaDB, the community driver left out things you would notice. A CALL to a procedure that returns rows failed with error 1312, because the driver never asked the server for multiple results; LOAD DATA LOCAL INFILE failed with a message that pointed at the server’s settings; every statement without rows cost a second round trip just to learn how many rows it had changed; the warnings the server sends back — a text truncated to 255 characters, an out-of-range value adjusted — never reached you; and of the five ways an account can sign in, it spoke two, so an account on ed25519, PARSEC, PAM, LDAP or Aurora IAM could not get in, with an error that looked like a wrong password. My client asks for all of that from the first packet: a CALL shows its rows, LOAD DATA LOCAL INFILE opens a file picker and sends only the file you choose, the affected rows and the warnings come in the server’s own reply, and it speaks all five, the plain-text password only over TLS.

On PostgreSQL the driver was well kept, and I replaced it anyway. Every query in the editor started with a probe to learn the server process id, and stopping one meant pg_cancel_backend from another connection, which takes the same user or a superuser; whether a transaction was still open was found out by asking the server for the time twice. Results arrived in binary, hard-wired in the driver, and a type table of my own had to read them, with intervals, arrays, ranges, composite types and domains left uncovered. And what I used of it was its old API, marked deprecated in 75 places. The protocol hands all of that over for free: the process id and a cancel key when the connection opens, the transaction state with every reply, and each value as the same text psql shows. My client signs in with SCRAM-SHA-256, md5 or a plain-text password, the last only over TLS; Kerberos and SSPI are not supported. postgres-nio is actively developed, and that was never the problem: what I gained by writing my own is control, not less maintenance.

What each of the seven clients was written from, what it speaks and what it leaves out is on the drivers page.

SSH without ssh

The SSH tunnel is written inside the app, on SwiftNIO SSH 0.15.0 and swift-crypto 4.5.2. No dependency on system ssh. Decrypted keys never written to disk.

Calíope parses OpenSSH private keys itself, accepts ed25519 and ECDSA P-256, P-384 and P-521, verifies the host key before anything else is sent, and opens a direct-tcpip channel to the database. On your side it listens on 127.0.0.1, on a port chosen when the tunnel opens, and the database connection goes through that local proxy. A passphrase-protected key is decrypted in memory, and it stays there.

TLS is verified by default. Turning SSL on means the server certificate is checked against the system trust roots; a CA you supply switches to certificate-only verification (no hostname check); and skipping verification is an explicit opt-in with a visible warning. The same policy (TLSBuilder) applies to MySQL, MariaDB, PostgreSQL, SQL Server, Oracle Database, MongoDB and Redis. ClickHouse, over HTTPS, offers the same three choices with its own policy on URLSession, and with a CA of yours it still checks the name against the profile’s host, also through an SSH tunnel. Oracle Database, over TCPS, takes the same policy, except that with a CA of yours it still checks the name against the profile’s host, also through an SSH tunnel; and a profile carrying a client certificate is refused.

Inside the sandbox

Calíope runs with the App Sandbox on, as the Mac App Store requires, and Caliope.entitlements asks for exactly this:

  • com.apple.security.app-sandbox
  • com.apple.security.network.client — the outgoing connections to your servers
  • com.apple.security.network.server — the local listeners: the tunnel’s proxy and the MCP server
  • com.apple.security.device.audio-input — dictation, which runs on the device
  • com.apple.security.files.user-selected.read-write — the .sql files you open and the folders you choose for backups
  • com.apple.security.application-groups — the shared container the widgets read from
  • com.apple.security.print — printing the SQL editor, the query log, an ER diagram or a report
  • iCloud: CloudDocuments, CloudKit and the key-value store, for the sync you can opt into

And what is not there: no access to the whole file system, no library-validation opt-out, no JIT.

AI and MCP on your terms

The AI assistant has three kinds of provider. Apple Intelligence runs through Apple’s FoundationModels framework, on the device, with no key and no account. Ollama runs on your own machine, against the models you installed there. The cloud providers work with your own key. The app decides how to talk to a provider by the capabilities it declares, not by its name. It is an assistant, not an agent.

The MCP server, on macOS, turns that around and lets an external AI client — Cursor, Claude Desktop, anything that speaks MCP — ask Calíope about your databases, within boundaries. It is off by default. It listens on 127.0.0.1 only, on port 41500 unless you change it, and that address is not configurable. Every request needs a Bearer token that Calíope generates (32 random bytes) and keeps in the Keychain. The client sees only the connections you explicitly expose, and that list starts empty. It only accepts reads: one statement per call, checked by a statement allow-list — not a read-only transaction — that also rejects FOR UPDATE and LOCK IN SHARE MODE, and results are capped at 200 rows; the MCP page recommends exposing a database user with no write privileges. Five tools, JSON-RPC 2.0 over a single POST /mcp route, protocol version 2025-06-18. Neither consent — the switch nor the exposed list — syncs to another device or travels in a settings backup. The MCP server page lists the five tools and explains how to point a client at it.

What it deliberately doesn’t do

  • No Windows, no Linux. Calíope is written for Apple platforms and stays there.
  • No Oracle Database beyond a client: AWR, SQL Trace and editing PL/SQL stay out, and only 26ai is measured. The engines today are MySQL, MariaDB, Amazon Aurora, PostgreSQL, SQL Server, Oracle Database, ClickHouse, Redis, SQLite, DuckDB and MongoDB.
  • No team workspaces. There is no server of mine and no accounts; your profiles and history are yours, and iCloud sync is an opt-in between your own devices.

If one of those is what you need, the honest comparison names the tools that do it well.