Skip to content
runlot

Share a database between projects

Two projects in one organization read and write the same database, with no API in between. Bind the database by name in each runlot.json.

A database belongs to your organization, not to one project. Any project in the organization can bind it by name in runlot.json, and the worker reads it under the binding name you choose. This is how an admin app and a service app, deployed separately, work on the same tables without the service exposing an API for the admin.

Sharing needs the Pro plan: binding a database into a second project, and creating a database that no project is named after, are Pro features.

1. Create the database, or use one you have

A database for both apps, with a name of its own:

runlot db create shop

Or reuse the database a project already has. The service app's own database is named after it, so the admin app can bind service directly; nothing has to be created.

runlot db list shows every database in the organization and which projects bind it.

2. Bind it in both runlot.json files

service/runlot.json
{
  "name": "service",
  "main": "src/index.ts",
  "database": false,
  "databases": [{ "binding": "shop", "database": "shop" }]
}
admin/runlot.json
{
  "name": "admin",
  "main": "src/index.ts",
  "database": false,
  "databases": [{ "binding": "shop", "database": "shop" }]
}

Deploy each project. The deploy applies the bindings: both workers now read the same database as env.shop, with the same API as env.db.

const orders = await env.shop.exec("select id, total from orders order by id desc limit 50");
  • binding is the name the worker reads: up to 32 lowercase letters, digits and _, starting with a letter. db is reserved for the database named after the project.
  • database is the database's name in your organization. It must exist when you deploy.
  • "database": false says the project needs no database of its own. Leave it out if the project also uses env.db.

A project can bind up to two databases besides its own, and a database can be bound by up to four projects besides the one named after it. Both projects must be in the same organization.

3. Read-only for the projects that only read

Add "mode": "read" to an entry, and that project's binding can read but not write:

"databases": [{ "binding": "shop", "database": "shop", "mode": "read" }]

A read binding sees the tables, views and sequences in the public schema, and nothing in login's runlot_auth schema. A write through it fails with PostgreSQL's read-only error. write is the default. To change the mode, edit the entry and deploy again.

4. Migrate the shared schema once

runlot deploy applies a project's migrations/ only to its own env.db, never to a database it binds. Apply the shared database's migrations with runlot db migrate, from any directory:

runlot db migrate shop --from ./shop-migrations

It lists the projects that bind the database before it applies anything, so you know who reads a table you are about to change. Keep the migrations in one place — one repository, or one of the two projects — and change the schema so that both apps keep working during the deploy of each.

Unbinding

Remove the entry from runlot.json and deploy. The database and its data stay.

What deleting does

  • Deleting a project removes its bindings. It never deletes a database: the project's own database stays in your organization, listed on the Databases page.
  • Deleting a database is refused while any project binds it, and the error names each one. runlot db delete shop --unbind-all removes the bindings first; the projects that bound it then get DatabaseNotAttached until their runlot.json drops the entry.

Local development

runlot dev gives each binding an empty local database. Add "migrations": "<dir>" to an entry to have runlot dev apply that directory to the local copy; a deploy ignores the key.

On this page