← All posts
Remote databasesSQLiteHTTPweb_vfsSQLCipherEncryption

Browse a SQLite database (even an encrypted one) straight from a URL

A SQLite database is increasingly just a URL behind a CDN, but peeking inside one has meant a multi-gigabyte download or a command-line session. Here's what makes opening a database over HTTP hard, and how Chwilio turns it into a paste-and-Return.

Chwilio9 min read

More and more, a SQLite database isn’t a file on your own machine. It’s a URL. Teams publish immutable .sqlite files behind a CDN or object store (S3, GCS, R2, raw.githubusercontent.com), because a single self-contained file is about the easiest thing in the world to host. Open-data portals do it, data-journalism projects do it, and plenty of apps ship a reference database as a downloadable artifact.

So you have a link to a 2 GB catalog.sqlite and one question: what’s actually in it? You want to browse a few tables and run a query, ideally without waiting on a 2 GB download you’ll delete five minutes later. That’s harder than it sounds, and it’s exactly what File ▸ Open from URL in Chwilio handles for you.

A URL isn’t a file

SQLite was built to read a database as a local file through the operating system: open(), seek(), read(). A URL supports none of that. So historically you had two options, and both were bad.

The first is to download the whole thing first. For a multi-gigabyte database that’s minutes of transfer, gigabytes of disk, and a copy that goes stale the moment the source changes. To read three rows, you paid for the entire file.

The second is to stand up a server in front of it: wrap the database in an API or a query service so the client never touches the file directly. Now it’s infrastructure, not a link you can paste.

There’s a third path that sounds obvious and turns out to be the hard one. Read just the bytes you need, over HTTP, on demand.

Why “just read the bytes you need” is harder than it sounds

HTTP has had the raw capability for this for years. A server that advertises Accept-Ranges: bytes will honor a request like Range: bytes=4096-8191 and hand back exactly that slice of the file. A SQLite database is neatly organized into fixed-size pages, so in principle “read page 12” is just “fetch bytes 49152 to 53247.” In principle.

Making it real runs into a stack of problems:

  • SQLite doesn’t speak HTTP. It reaches storage through a layer called the VFS (virtual file system), the thing that turns “read page N” into an actual read(). Nothing in the box translates a page read into an HTTP Range request. Someone has to write a VFS that does, then wire it in so SQLite uses it instead of the local-file one.
  • The header dance. Before SQLite can read any page, it has to read the 100-byte file header to learn the page size and page count. So the client fetches just those bytes first, parses the geometry, and only then starts pulling pages. Get that choreography slightly wrong and the file reads as “corrupt.”
  • Immutability and read-only. Range reads only make sense if the file isn’t changing underneath you. A page that shifts mid-query gives you garbage. So a remote open has to be read-only and immutable by construction, which also means the app’s editing, backup, and journal machinery all have to stand down for that connection.
  • The presigned-URL footgun. The most common real-world remote SQLite URL is an S3 or GCS presigned URL, and those carry their own query string full of &, =, and ?. You hand your URL to SQLite inside another URI (...&web_url=<your link>), and SQLite parses that outer URI itself, splitting on & and =. Encode the inner URL naively and it gets truncated at its first &: the signature falls off, and every request 403s. This one bites everybody who builds it themselves.
  • Keeping the UI alive. Every one of those page fetches is network latency. A schema read or a COUNT(*) that would be instant on a local file is now dozens of round-trips. Do it on the main thread and the app just beachballs.

You can assemble all of this yourself, out of a custom VFS, the right open flags, careful URL encoding, and a lot of background-thread plumbing. That’s precisely why browsing a remote SQLite file has stayed a command-line-and-library job, rather than something a GUI hands you.

In Chwilio: paste a URL, press Return

Chwilio does all of the above and puts it behind one menu item:

  1. File ▸ Open from URL… — ⇧⌘U on macOS, Ctrl+Shift+U on Windows.
  2. Paste an http(s) link to a SQLite file.
  3. Press Return.

A window opens on the remote database, read-only, and you browse it like any other: sidebar, data grid, SQL editor, filters. Only the pages your queries actually touch come over the network.

File ▸ Open from URL…   (⇧⌘U · Ctrl+Shift+U)

  https://data.example.com/catalog.sqlite

  → opens read-only · immutable · streamed over HTTP
-- Runs against the remote file. An indexed lookup like this
-- fetches a handful of pages, not the whole 2 GB database.
SELECT name, price, in_stock
FROM   products
WHERE  sku = 'A-1042';

A few things worth knowing:

  • No setup, on either platform. The HTTP VFS is built into the app — both the macOS and the Windows build — so there’s no extension to install or configure first. Nothing to download, nothing to point Chwilio at, and no PATH to set up on Windows.
  • The same feature, not a port of it. Remote open runs through the same shared engine on both platforms, so the behaviour below — range fetches, immutability, read-only, cancelable counts — is identical. Only the shortcut differs.
  • Presigned URLs just work. Chwilio percent-encodes your URL correctly, so an S3 or GCS link that carries its own ?...&... query string survives intact instead of getting cut off.
  • Responsive over the network. The open runs off the main thread with a progress spinner, and row counts run on a separate, cancelable connection. Switch tables or close the window and an in-flight count stops rather than hanging.
  • Grab a local copy anytime. If you decide you want the file on disk, Database ▸ Backup Database… downloads it in one step.

What the server needs to support

Remote open works when the file behaves like a static, addressable blob:

  • The host must honor HTTP range requests (Accept-Ranges: bytes). Most CDNs and object stores (S3, GCS, R2, Cloudflare, static hosting) do this out of the box.
  • The file must be immutable while you browse it. Don’t point at a database that’s being written to.
  • Use the raw file URL, not an HTML page wrapping it. On GitHub that means a raw.githubusercontent.com/... link, not the github.com/.../blob/... page (that page is HTML, not the database).

Open from URL is a Pro feature: it’s enabled on a licensed or active-beta build, and disabled in the free trial.

Encrypted databases open the same way

An encrypted file makes the remote open harder in one specific spot. SQLCipher scrambles the whole database with AES-256, and the first 16 bytes are a random salt rather than the usual SQLite format 3 magic. So the header dance from earlier has nothing to read in the clear: the page size and page count stay ciphertext until a passphrase unlocks them.

ChwilioCipher, the encrypted edition, opens these over the same File ▸ Open from URL…. Paste the link and it prompts for the passphrase before the first page comes over the wire. It derives the key from the salt in the header and your passphrase, then decrypts each range-fetched page locally as it lands. The server only ever hands back ciphertext, and the plaintext never leaves your machine.

Everything from the plain case still holds. The connection is read-only and immutable, the query planner reads only the pages along an index’s path, and the decryption runs off the main thread with the rest of the open. Browsing an encrypted 2 GB database on a CDN feels the same as browsing a plain one. The host requirements don’t change either — range requests, an immutable file, a raw URL — because SQLCipher sits below all of it. There’s more on the encryption itself in Encrypt your SQLite databases with SQLCipher.

Why fetching only what you read matters

Once opening a remote database is a paste away, a lot of everyday tasks get easier:

  • Peek inside a published dataset instantly. Open-data portals, data-journalism releases, and Datasette-style published SQLite are all just URLs, so you can inspect a multi-GB one in a native window without the multi-GB download.
  • Query a CDN-hosted reference database. Keep a lookup or catalog database on your CDN and browse it ad hoc, instead of shipping a copy into every environment that needs a peek.
  • Inspect a release or build artifact. If a database is a downloadable asset, open it straight from its download URL to sanity-check a release before anyone pulls it.
  • Share a link, not a file. Everyone opens the same immutable snapshot, read-only. There’s no “which copy is the right one?”, and no chance of clobbering a shared file, because the connection can’t write.
  • Pay only for what you read. An indexed query touches a few pages; a targeted SELECT over a huge remote table transfers kilobytes, not gigabytes.

Under the hood, briefly

SQLite talks to storage through a VFS. Chwilio bundles sqlite_web_vfs, a VFS that turns each page read into an HTTP Range request, and opens the database with SQLite’s URI filename mode:

file:/__web__?mode=ro&immutable=1&vfs=web_chwilio&web_url=<your percent-encoded URL>

Because the file is opened immutable and read-only, fetched pages can be cached, and the query planner reads only the pages along an index’s path. That’s what makes a lookup over a 2 GB remote file feel local. Chwilio runs the whole open (VFS load plus schema read) off the main thread. The fiddly parts, the header dance, the read-only guards, and that presigned-URL encoding, are all handled for you, so you never see them.


Good news: you can try this right now, for free. Chwilio is in private beta, and the beta build unlocks every feature, including Open from URL, for the whole beta period at no cost. Grab a beta invite, then try File ▸ Open from URL… against any range-request-capable .sqlite link today. Encrypted databases over HTTP work in ChwilioCipher too. When the beta period ends, access reverts to the free trial, and keeping these features needs a license. Questions about a specific host or dataset? Get in touch.