>
Software

Self-hosted Datasette: a 0.5a0 release that fits CI


If you self-host Datasette (a Python tool that publishes SQLite databases as a queryable web service) and you have ever wanted to push a freshly built database to a production instance without opening an SSH session, the new 0.5a0 release of datasette-upload-dbs is the answer. The plugin has been around for a couple of years, but the 0.5a0 release adds a formal API that turns the upload from a manual click into a curl call. That is a small change on paper and a meaningful change in practice if you build databases in CI (continuous integration, meaning an automated pipeline that runs whenever you push code, like GitHub Actions).

The original plugin let you upload a database file through a form in the Datasette web UI. The uploaded file was validated, written to disk, and swapped into place. That worked, but it was a UI-driven operation. The new release extracts the same flow into a JSON endpoint, which means you can do it from any environment that can curl, including a CI job, a server-side script, or another service.

What the API actually does

The endpoint is a POST to the /-/upload-dbs path on your Datasette instance. The body is a multipart form with two fields: db (the database file itself) and db_name (the name you want the database to be served under). The plugin handles the upload, validates the file, and atomically swaps it into the running instance. After the call returns, https://your-instance.example.com/db_name starts serving the new database.

The canonical curl call from the release notes looks like this:

curl -X POST \\
  -H "Authorization: Bearer *** \\
  -H "Accept: application/json" \\
  -F "db=@content.db" \\
  -F "db_name=content" \\
  https://your-instance.example.com/-/upload-dbs

The token you put in the Authorization header is the Datasette API token you already use for any other plugin. There is no special scope for upload-dbs; if you can hit any Datasette API endpoint, you can hit this one. The Accept: application/json header tells the plugin to return a JSON response instead of an HTML page; the JSON response includes the URL the new database is now served at and any warnings from the swap (a missing index, a read-only mode flag if you configured one).

A second use of the API is atomic replacement. If you already have a database named content running, calling the endpoint with db_name=content replaces the live one with the new upload. The swap is atomic, meaning there is no window where a query hits an empty database or a half-written file: the plugin writes the new file, validates it, then renames it into place. Queries that arrive during the swap are held until the rename completes.

Why this matters for CI

The release notes frame the new API as a CI feature, and that is the right framing. Before this release, the typical Datasette CI workflow looked like one of two patterns, and both were painful:

  • Build the database locally, rsync it to the production server, log in, and restart Datasette. This is the “production deploy” pattern, and it works, but it requires a server with SSH access and a restart script.
  • Build the database in CI, push the artifact to S3 or R2, and have a downstream consumer pull it. This works but breaks the “Datasette is the canonical interface” model.

The new upload-dbs API collapses both patterns into one call. Your CI job runs sqlite3 feed.db < build.sql to build the database, then runs the curl above to push it to production. The CI job does not need SSH access to the Datasette server. The Datasette server does not need to restart. The atomic swap means queries in flight at the moment of the swap complete against the old database; queries after the swap see the new one.

This pattern is most useful for the “rebuild every day” use case. If you have a database that is regenerated from a CSV, a JSON feed, or a scheduled query, you can build the new database in CI on a schedule and push it to Datasette when the build finishes. The user-facing URL never changes. The database underneath it is rebuilt every day.

The “swap” model is the actual feature

What matters most for production users is the “atomic swap” framing. Database-as-a-service tools generally have two modes: live queries (your queries hit the database that is currently running) and snapshots (your queries hit a read-only copy that is rebuilt periodically). Upload-dbs implements the live-queries model with an atomic swap under the hood.

Atomic swap is implemented as a write-then-rename operation, which is how Linux filesystem atomicity works in practice. The plugin writes the new database to a temporary file, verifies the file is a valid SQLite database, then renames it into place. The rename is atomic at the filesystem level, and Datasette’s database-aware plugin layer does not need to coordinate a lock to make the swap visible to running queries.

Practical consequence is that you can run a database-rebuild job on a tight schedule (every five minutes, every hour) without coordinating with the Datasette server, and without users ever seeing an error during the swap. Anyone who has tried to do a “rolling restart” of a service that holds a database open knows how hard this is to get right. Upload-dbs makes it a one-line operation.

Limits and things to be aware of

The plugin has been “around for a while,” which means the implementation has had time to settle, but there are still a few things to watch for.

First, the upload is a full file transfer, not a delta. If your database is 10 GB, every upload-dbs call moves 10 GB. The build pipeline has to regenerate the full database, and the upload has to transfer the full file. This is fine for hundreds of MB. For tens of GB, you will want to make sure the upload path is on a fast network and the build pipeline is not rebuilding the database unnecessarily.

Second, the API returns JSON, but the JSON is not a full machine-readable schema. The release notes show one example response shape; the actual fields are documented in the plugin’s plugin.json. If you are building tooling on top of the API, read the plugin source, not just the release notes.

Third, the “0.5a0” version is an alpha (the “a” in the version number). The API is stable, but the plugin is still in alpha and may have minor breaking changes before 0.5.0 final. For production use, plan around the alpha-status possibility: pin the version, watch the release notes, and have a rollback plan if the version you pin gets a fix that breaks your integration.

Fourth, the swap is atomic, but the validation is not free. The plugin validates the SQLite header and runs a PRAGMA integrity_check on the new file before swapping. For a small database, this is milliseconds. For a large database, it can be seconds. If you are uploading a database with a tight build-and-deploy window, account for the validation time.

How to install it

The plugin is installed with datasette install datasette-upload-dbs. If you are running Datasette in a virtualenv or a container, that command installs the plugin into the same Python environment. After installing, you need to add the plugin to your Datasette configuration file (typically datasette.yaml or metadata.yaml) and enable the API token for the user who will be uploading.

The configuration is a single line: plugins.datasette-upload-dbs: {}. The plugin does not require any extra configuration; it uses the same Datasette API token you already have. If you do not have a Datasette API token yet, create one through the /-/api page in the Datasette UI.

What I would tell past me

Users running a self-hosted Datasette instance with a “rsync + restart” deploy pattern should treat the new API as the upgrade. The setup is one config line. The build pipeline is one curl call. The deploy is atomic.

Users running Datasette as a side project who rebuild the database manually will not see any change in workflow; you can still upload through the web UI. The API is an additional option, not a replacement.

Operators running Datasette at scale with multi-GB databases will find the API works but want to be careful about network bandwidth and validation time. A streaming or delta-based upload would be a more useful feature for this case, but the plugin does not implement that today.

For everyone else, the install is one command and the upgrade is mostly invisible:

  • datasette install datasette-upload-dbs to install the plugin
  • Add plugins.datasette-upload-dbs: {} to your Datasette config
  • Create a Datasette API token if you do not have one already
  • Run the curl call from the release notes to push a database
  • Set up a CI step to run the curl on every successful build

Trade-offs

Atomic swap is the headline feature, but it is also the main constraint. You can swap a database in place, but you cannot run multiple versions of the same database simultaneously. Users who want a staging database alongside a production database must upload them under different names and pin the Datasette URL routing to one or the other. The plugin does not have a “blue/green” mode where you can route a fraction of traffic to a new database and roll back if it fails.

CI usage assumes you trust the CI environment with the database contents. The plugin authenticates the upload with a bearer token, but the token must be present in the CI environment. For private databases, this is fine. For public databases that need to be uploaded by an external service, the token management becomes a separate problem.

Alpha status is real. The API is stable, but the alpha tag means the maintainer reserves the right to make breaking changes before 0.5.0 final. For a production deployment, pin the version, watch the release notes, and have a rollback plan if the next alpha breaks your integration. For a side project, the alpha status is fine; the underlying mechanism (write, validate, rename) is not going to change.

Leave a comment