ribo.toml Reference
Every Cell has a ribo.toml configuration file. ribo deploy reads it from the current directory by default, or from the path given by --config.
[cell]
The [cell] table is required.
[cell]
name = "my-cell" # required
js = "./cell.js" # required (or wasm, or static)
build = "npm run build" # optional; runs before uploadingFields
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Human-readable name. Used to derive the stable address. |
js |
string | one of | Path to the JavaScript entry point (relative to ribo.toml) |
wasm |
string | one of | Path to the compiled .wasm file |
static |
string | one of | Path to a local directory; deploys a static site with no JS required |
build |
string | no | Shell command to run before deploying. Runs in the directory containing ribo.toml. |
bindgen_glue |
string | no | Path to the wasm-bindgen JS glue file. Required when wasm is set and the binary uses wasm-bindgen. |
account_id |
string | no | Pin this project to an account (acct_...). See Account pin. |
account |
string | no | Human-readable label for the pinned account. Display-only, never enforced. |
compatibility_date |
string | no | Runtime behaviour date, YYYY-MM-DD. See Runtime compatibility. |
compatibility_flags |
array | no | Runtime features to enable, e.g. ["nodejs_als"]. |
Exactly one of js, wasm, or static must be present.
Account pin
account_id pins the Cell to a specific account so it can't be deployed into the wrong one. When set, ribo deploy compares it against your active account (from your auth token) and refuses to deploy on a mismatch, before uploading anything:
[cell]
name = "my-cell"
js = "./cell.js"
account_id = "acct_3f8c1a2b4d6e8f00" # only deployable from this account
account = "Acme Corp" # optional label, shown in outputThe pin is an assertion, not a selector: it never changes which account is used, it only guards against deploying this project somewhere unintended. The optional account label is display-only and is never used for enforcement (account names are mutable; the id is the identifier). Generate the pin without hand-copying the id with ribo account pin.
Don't pin shared/example templates that others deploy into their own accounts; the pin would refuse their deploys.
The pin catches a wrong account; it doesn't choose the right one. If you regularly work across several accounts (or run agents that deploy for you), bind each project tree to its own credential with profiles, and keep the pin as the backstop.
Runtime compatibility
Your Cell runs in its own V8 isolate. Some runtime behaviour is dated, so that fixes and changes can land without altering how already-deployed code behaves. compatibility_date selects which set of behaviours your Cell sees, and compatibility_flags turns on individual features by name.
[cell]
name = "my-cell"
js = "./cell.js"
compatibility_date = "2026-06-16"
compatibility_flags = ["nodejs_als"]Both keys are optional. Omit them and your Cell gets the platform default, which does not move on its own — a Cell's runtime behaviour changes only when you change these keys and redeploy.
compatibility_date must be a real calendar date written as YYYY-MM-DD, zero-padded (2026-06-16, not 2026-6-16), and no later than the date the platform supports. A date the platform can't serve is rejected at deploy time with the newest one it accepts.
Available flags:
| Flag | Effect |
|---|---|
nodejs_als |
Enables AsyncLocalStorage from node:async_hooks. |
nodejs_compat |
Enables the Node.js compatibility layer (node:* built-ins). |
nodejs_compat_v2 |
The v2 Node.js compatibility layer. |
A flag outside this list is rejected at deploy time rather than accepted and ignored.
Two things to know:
- Changes take effect on redeploy. Editing these keys without running
ribo deploychanges nothing; the settings are read when your Cell's isolate is loaded, and that is keyed to the deployed version. - Older
riboversions ignore these keys silently.ribo2026.08.2 and later send them. An earlier version parses the keys, drops them, and reports a successful deploy — with your Cell still on the defaults. Runribo --versionif a flag doesn't appear to take effect.
[[bindings]]
Each [[bindings]] block declares a resource that the Cell can access at runtime. The binding name becomes a property on the env object passed to fetch.
c3: SQLite database
[[bindings]]
type = "c3"
binding = "DB" # env.DB in your cell
database = "myapp" # database name (created with ribo db create)Multiple databases:
[[bindings]]
type = "c3"
binding = "USERS"
database = "users"
[[bindings]]
type = "c3"
binding = "LOGS"
database = "request-logs"g7: Object storage bucket
[[bindings]]
type = "g7"
binding = "IMAGES" # env.IMAGES in your cell
bucket = "avatars" # bucket name (created with ribo bucket create)files: Static file tree
[[bindings]]
type = "files"
binding = "ASSETS" # env.ASSETS in your cell
dir = "./public" # local directory to uploadFiles are uploaded at deploy time and served from object storage. The binding has a fetch(request) method for URL-based serving and a get(key) method for direct access.
dir is the whole boundary, and it has no default. Every regular file beneath it
is uploaded, with no exclusion list, and the bucket serves each one under its own
name. Point it at a directory that holds only what the site serves — a binding
written dir = "." publishes this ribo.toml, any .env file beside it and the
whole .git directory, which is why the convention is a public/ subdirectory
with ribo.toml and cell.js one level above it. A files block with no dir
key parses and uploads nothing.
ribo deploy warns when it finds such an object and prints the URL it answers
on; ribo deploy --unpublish-config deletes
those keys from the bucket. Removing a file locally does not unpublish it — a
deploy never deletes.
text: Plain string value
[[bindings]]
type = "text"
binding = "API_KEY" # env.API_KEY in your cell
value = "sk-..." # literal string valueUse text bindings for configuration, feature flags, or non-secret string values.
vault: Encrypted secret
[[bindings]]
type = "vault"
binding = "JWT_SECRET" # env.JWT_SECRET in your cellA vault binding injects a secret value at dispatch time. Unlike text, the value is not stored in ribo.toml; only the key name is. The value itself is set separately with ribo vault set and stored encrypted server-side. Use vault bindings for API keys, signing secrets, database credentials, and any other sensitive value you don't want committed to source control.
ribo vault set my-cell JWT_SECRET # set the value (prompts for hidden input)
ribo deploy # then deployImportant: set the vault value with ribo vault set before deploying a cell that declares a vault binding. If the value is missing at deploy time the binding is stored as a placeholder, and the cell will receive that placeholder instead of the real secret, causing silent failures. See ribo vault for the full command reference.
For how vault values are protected (encryption at rest, when decryption happens, scopes, auditing, and rotation), see the Vault Security Model.
[[pulse]]
Each [[pulse]] block declares a cron schedule. The platform calls the Cell's pulse(event, env) handler at each scheduled time.
[[pulse]]
schedule = "0 * * * *" # every hour at :00
[[pulse]]
schedule = "*/15 * * * *" # every 15 minutesMultiple [[pulse]] blocks are allowed. Each fires independently.
Fields
| Field | Type | Required | Description |
|---|---|---|---|
schedule |
string | yes | 5-field POSIX cron expression (UTC). Validated at deploy time. |
Cron syntax: min hour dom month dow. All times are UTC. Minimum interval is 1 minute. Day-of-week runs 1–7 with Sunday as 1, so Monday is 2. A Cell may declare at most 25 schedules; an expression the scheduler cannot parse fails the deploy with a 400 naming it.
See Pulse Overview for the handler interface and examples.
[gate]
Restrict who can reach the Cell's public URL. The policy deploys with the Cell and is enforced at the edge before your code runs — see Access Control (Gate) for the full model.
[gate]
audience = "domain"
domains = ["yourco.com"]
allow = ["contractor@example.com"]
[gate.session]
ttl = "7d"Fields
| Field | Type | Required | Description |
|---|---|---|---|
audience |
string | yes | One of public, link, private, account, emails, domain, sso. |
allow |
string[] | for emails |
Allowed email addresses. Optional extra allowance for other audiences. |
domains |
string[] | for domain |
Allowed email domains, e.g. "yourco.com". |
links |
bool | no | Accept capability links (ribo gate link). Default true. |
[gate.session] ttl |
duration | no | Session lifetime, "1h"–"90d" (default "30d"). |
[gate.source.<name>] |
table | for sso |
An identity provider for federated sign-in — one table per provider, up to 8. See below. |
The config is validated at deploy time; an invalid gate fails the deploy rather than deploying the Cell open. Removing the [gate] block removes the gate on the next deploy.
[gate.source]
For audience = "sso", each [gate.source.<name>] table declares one identity provider. The table name (google, okta, …) becomes the URL path segment of the provider's callback and the default label on its sign-in button. Names are 1–32 characters of lowercase letters, digits, - or _; a policy holds at most 8 sources, and sso requires at least one.
[gate]
audience = "sso"
domains = ["acme.com"]
[gate.source.okta]
type = "oidc"
issuer = "https://acme.okta.com"
client_id = "0oa..."
scopes = "openid email profile groups"
groups_claim = "groups"
label = "Acme Okta"| Field | Type | Required | Description |
|---|---|---|---|
type |
string | yes | oidc (any compliant provider), or the presets google / github. |
issuer |
string | for oidc |
The provider's https issuer URL, exactly as the provider states it (a trailing slash is kept). Forbidden for the presets — their issuer is fixed. |
client_id |
string | yes | The OAuth client id issued by the provider. |
scopes |
string | no | Scopes requested from the provider, default "openid email profile". oidc only. |
groups_claim |
string | no | ID-token claim read for group names, default "groups". oidc only. |
label |
string | no | Sign-in button text. Defaults to the source name. |
The client secret is never a ribo.toml key. It lives in the Cell's vault under GATE_OIDC_<NAME> — the source name uppercased with - mapped to _, so [gate.source.acme-sso] reads GATE_OIDC_ACME_SSO — and must be set with ribo vault set before deploying; a deploy with a missing secret fails and names the command to run. A google or github source additionally requires a non-empty domains or allow on the [gate] table. See federated sign-in for the model and Connect an Identity Provider for per-provider setup.
[server]
Override the default management API URL. Useful for local development.
[server]
url = "http://localhost:8082"When omitted, ribo uses https://api.tissue.systems.
Complete example
[cell]
name = "notes-app"
js = "./dist/cell.js"
build = "esbuild src/cell.ts --bundle --outfile=dist/cell.js --format=esm"
[[pulse]]
schedule = "0 * * * *" # hourly cleanup
[[bindings]]
type = "c3"
binding = "DB"
database = "notes"
[[bindings]]
type = "g7"
binding = "UPLOADS"
bucket = "note-attachments"
[[bindings]]
type = "text"
binding = "APP_VERSION"
value = "1.0.0"Static site shorthand
For a Cell that only serves static files, the static field eliminates the need for a JS entry point entirely:
[cell]
name = "my-site"
static = "./public"ribo generates the pass-through worker automatically and deploys the directory contents. See Static Sites for routing behaviour and the hybrid (static + dynamic) pattern.