on this page

ribo CLILocal Development

Local Development

ribo local runs your Cell on your own machine: the same V8 isolate runtime that serves it in production, plus c3 for SQL, g7 for objects, and a management API — four services on localhost, started and stopped by one command.

cd my-cell
ribo local

Nothing is deployed to the platform, nothing is billed, and after the first run it works with no network at all.


Why it starts a whole stack

A Cell is dispatchable only from the management server that holds it. The router resolves an address against that server's registry, so a Cell deployed to api.tissue.systems is invisible to a runtime on your laptop, and a Cell deployed on your laptop is invisible to the edges.

ribo local brings up a management server here and deploys into it. That is the arrangement in which http://localhost:8080/<address> can answer.


Quick start

Run it in a directory that has a ribo.toml:

$ ribo local
starting  c3 · g7 · api · runtime
ready     runtime 2026.08.014 :8080 · c3 :8081 · g7 :8083 · api :8082

account   Acme Corp (acct_3f8c1a2b4d6e8f00)
deployed  my-cell (js)
url       http://localhost:8080/33ed412f7x2kp

watching  /Users/you/my-cell (3 dirs) — a save redeploys
Ctrl-C to stop.

The Cell is live at that URL:

curl http://localhost:8080/33ed412f7x2kp/

Ctrl-C stops all four services and prints what it stopped.

You do not have to be logged in. With no credential the stack still runs and the deploy line reads account <local/unauthenticated>, which is what CI and offline work get.


What is running

Service Port What it does
runtime 8080 Runs your Cell in its own V8 isolate and dispatches requests to it
c3 8081 SQL databases behind type = "c3" bindings
api 8082 Management API: registry, deploys, buckets, vault
g7 8083 Object storage behind type = "g7" and type = "files" bindings

Three of the four are ribo itself — the runtime is the only separate executable, and ribo fetches it for you.

The ports are fixed: the runtime's service bindings name them literally, so there is no port flag. A busy port is reported rather than worked around.

Error: Port 8081 is already in use, which c3 needs.
A previous stack may still be running — try `ribo local stop`.

The first run downloads the runtime

Fetching the Cell runtime 2026.08.014 for macos-arm64 (28 MB, signed by release@tissue.systems)

The download is checked against a signed manifest before it is unpacked: a signature from outside ribo's release trust list, or a checksum that does not match, refuses to install rather than warning. It is then cached by version and reused:

Platform Cache
macOS ~/Library/Caches/ribo/runtime/<version>/
Linux ~/.cache/ribo/runtime/<version>/

Runtime builds are published for macOS arm64 (Apple silicon) and Linux x86_64. On Windows, run ribo under WSL.

Two escape hatches:

RIBO_NO_DOWNLOAD=1 ribo local          # fail instead of fetching anything
ribo local --runtime /path/to/runtime  # use a runtime you already have

RIBO_RUNTIME_DIR does the same as --runtime for a whole shell.


Create databases and buckets first

The local stack starts empty, and ribo deploy does not create resources — locally or in production. Point the resource commands at the local management API before the first request:

export TISSUE_SERVER=http://localhost:8082   # every ribo command in this shell

ribo db create notes
ribo bucket create uploads
ribo vault set my-cell SECRET

Or per command, without the export:

ribo db create notes --server http://localhost:8082

A type = "files" binding is the exception: its bucket is derived from the address and created during the deploy.

Skip this and the Cell throws on the first request that touches the binding — C3 database "notes" not found, or bucket "uploads" not found. Both print in your terminal.


What your Cell prints

console.log goes straight to the terminal, unprefixed:

cell saw GET /notes

An uncaught exception is answered to the client as a bare Internal Server Error, which says nothing. The terminal gets what actually happened, with the request path and the frames:

error     bucket "uploads" not found  (/upload)
          at async Object.fetch (cell.js:12:11)

Errors are written to stderr and your Cell's own output to stdout, so a redirect separates them.


Saving a file redeploys the Cell

ribo local watches the Cell directory while it runs. Save cell.js and the next request runs the new code:

changed   cell.js
reloaded  my-cell (js)

The address does not change and the services keep running — only the Cell's code is replaced, because the runtime keys each isolate on the address and the deploy timestamp. Static files under a type = "files" binding are re-uploaded the same way, and the line says how many moved:

changed   public/index.html
reloaded  my-cell (js)
files     FILES→33ed412f7x2kp-files (12 files: 1 uploaded, 11 unchanged)

A build command in ribo.toml runs on every save, before the upload. Writes it makes into the directory do not trigger a further reload.

A failed reload — a build that exits non-zero, a bucket that does not exist — prints the error and leaves the previous deploy serving:

changed   cell.js
reload    failed — build failed: npm run build (exit 1)

Four things are left out of the watch: .git, node_modules, target, and the scratch files editors write beside the one you saved (.swp, file~, .#file, .DS_Store). Directories a files binding names in allow_symlinks_to are watched, because a shared asset lives outside the Cell directory by design and a watch on a symlink is a watch on the link, not on what it points at.

To turn it off and deploy once:

ribo local --no-watch

You can also deploy from another terminal, whether or not the watch is on:

ribo deploy --server http://localhost:8082

To bring the stack up without deploying anything — useful when the Cell you are working on lives somewhere else — there is nothing to watch, so the watch does not start:

ribo local --no-deploy

Where local data lives

Under ~/Library/Caches/ribo/local/ on macOS, ~/.cache/ribo/local/ on Linux:

Path Holds
c3-data/ one SQLite file per database
g7/ one directory per bucket
cells/ deployed code, the local registry, vault values
run.json the process IDs of the running stack

It survives restarts, so the rows you wrote yesterday are there today. Delete the directory to start from nothing.

The databases are ordinary SQLite files. ribo db exec notes "select * from notes" --server http://localhost:8082 reads them, and so does any SQLite client.


Stopping a stack you lost

If a terminal was closed on a running stack, its services are still holding the ports:

$ ribo local stop
stopped   c3 (54130)
stopped   g7 (54132)
stopped   api (54133)
stopped   runtime (54134)

With nothing running it prints No local stack recorded as running.


What is different from production

Local Production
Routing path-based: localhost:8080/<address>/… subdomain: <cell>.<subdomain>.tissue.dev
Authentication none — every request reaches the Cell account credentials on the API, Gate on the Cell
Gate a [gate] block fails the deploy: the policy needs storage the local stack does not run policies, capability links, sessions
Pulse no scheduler runs; [[pulse]] schedules are stored but nothing fires them dispatched on schedule, UTC
g7 the binding API; the S3 API, presigned URLs and per-bucket credentials answer 501 all of it
Custom domains, Synapse not part of the local stack

Because routing is path-based, a Cell that reads its own hostname sees localhost:8080 locally. Route on the path, or read the address prefix off it.

To exercise a pulse(event, env) handler, send the invocation the scheduler would send:

curl -X POST http://localhost:8080/33ed412f7x2kp/ \
  -H 'x-tissue-event: pulse' \
  -H 'x-tissue-cron: */5 * * * *' \
  -H 'x-tissue-scheduled-time: 2026-08-13T08:00:00Z'

Flags

Flag Default Description
--config <path> ./ribo.toml Config file to deploy
--runtime <path> the ribo cache Runtime executable, or a directory holding one
--no-deploy off Start the services without deploying the Cell in this directory
--no-watch off Deploy once and stop there, instead of redeploying on every save
--profile <name> Credential profile for the deploy (see Accounts & Profiles)

Going to production

When it works locally, deploy it for real from the same directory:

ribo deploy

Without --server the deploy goes to https://api.tissue.systems, and the databases, buckets and vault values you created locally are not there — create them again against the platform.


See also