on this page

Cellsribo.toml Reference

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 uploading

Fields

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 output

The 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 deploy changes nothing; the settings are read when your Cell's isolate is loaded, and that is keyed to the deployed version.
  • Older ribo versions ignore these keys silently. ribo 2026.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. Run ribo --version if 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 upload

Files 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 value

Use 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 cell

A 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 deploy

Important: 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 minutes

Multiple [[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.