Persistence and Tenancy
B4.run's Node runtime stores its state in local SQLite files by default. That's fine for one process, but tenancy is up to your application. B4.run never infers a tenant from a thread id.
What B4.run persistsCopy link to section: What B4.run persists
The runtime writes six kinds of state, each with its own store, key and lifecycle.
CheckpointsCopy link to section: Checkpoints
LangGraph checkpoints and pending writes.
- Default:
.b4/checkpoints.sqlite - Shared option:
postgresCheckpointerthroughcheckpointer - Keyed by:
thread_id, checkpoint namespace and checkpoint id, with no tenant key. - On thread deletion: the runtime awaits
deleteThreadafter deleting the metadata, when the saver supports it. - Responsibility: your app picks the saver, and the runtime writes route state.
Thread metadataCopy link to section: Thread metadata
- Default:
.b4/threads.sqlite - Shared option:
createPostgresThreadsStorethroughthreadsStore - Keyed by:
thread_id. Metadata can record application fields, but the id itself proves no owner. - On thread deletion: the thread row is removed first.
- Responsibility: your app authorizes threads. The runtime maintains status and route metadata.
Permission decisionsCopy link to section: Permission decisions
- Default:
.b4/permissions.json - Shared option:
createPostgresPermissionsStorethroughpermissions.store - Keyed by: tool or gate key and pattern, scoped to neither a thread nor a tenant by default.
- On thread deletion: they stay.
- Responsibility: your app sets the policy and decides how stores are separated.
Long-term memoryCopy link to section: Long-term memory
- Default:
.b4/memory.sqlite - Shared option: a separate
MemoryStore, such aspgvectorMemoryStore, throughmemory.store - Keyed by: the route-declared memory scope:
workspace,route, and anytenant,useroragentdimensions your app supplies. - On thread deletion: they stay. Memory APIs and your retention policy manage them.
- Responsibility: your app derives namespaces and manages the lifecycle.
Workspace filesCopy link to section: Workspace files
- Default:
<appRoot>/workspace/through the local filesystem backend - Shared option: an application-supplied
backends.filesystem - Keyed by: the workspace path, with no thread or tenant partition.
- On thread deletion: arbitrary workspace files stay.
- Responsibility: your app and its tools.
Sandbox volumesCopy link to section: Sandbox volumes
- Default: none until
sandboxis configured. The location is provider-specific. - Shared option: a durable provider volume, such as a Docker named volume or a Kubernetes PVC.
- Keyed by:
threadId, which is the provider lookup key. - On thread deletion: sandbox destruction removes the thread volume. Idle reap and shutdown release compute but keep the volume.
- Responsibility: the configured sandbox provider.
Choose local or shared storesCopy link to section: Choose local or shared stores
Local SQLite and file stores suit development, or one long-lived Node process with durable local disk. When compute becomes ephemeral, or several processes need the same records, move the three durable runtime stores. Each one moves on its own:
checkpointerfor checkpointsthreadsStorefor thread metadatapermissions.storefor runtime permission grants
@b4run/postgres-storage implements all three over Postgres. Long-term memory is a separate memory.store with its own schema, retrieval and retention needs. @b4run/memory-pgvector is one shared option.
Generated schema and table prefixCopy link to section: Generated schema and table prefix
The generated hono and vercel request stores put checkpoints, threads and permissions in the public schema under the b4 table prefix unless the deployment says otherwise. Those public.b4_* tables have no application namespace. A tenant field in application input doesn't partition them, and two apps pointed at one database with the defaults share them.
Two runtime settings namespace a deployment for you. B4_PG_SCHEMA selects the schema and B4_PG_TABLE_PREFIX selects the prefix. Each takes a lowercase identifier or a $NAME reference to another environment variable. For example, B4_PG_SCHEMA=$VERCEL_ENV puts a Vercel project's preview and production deployments in the preview and production schemas of one shared database. See Namespace preview and production.
If several applications or trust zones share a database, give each application its own schema or prefix. Set it through those bindings, or through store wiring that passes schema or tablePrefix consistently to all three Postgres stores. Hand-composed wiring must preserve the generated edge lifecycle and bundling constraints:
- per-request pools and disposal
- migration coordination
- environment binding from the original
Request - static provider imports
- serialized config
- explicit permission policy and hydration
Note, the generated stores.mjs is replaced on every rebuild.
Permission cachingCopy link to section: Permission caching
A running process caches permission decisions, because PermissionsStore.match() is synchronous. On the configuration-resolved Node path, serveRuntime resolves and loads its configured permissions store once at Node boot. The Postgres implementation fills its in-memory map with load(), and that cache does not auto-refresh across replicas.
So a shared permissions table does not automatically give you instant invalidation. When grants or revocations added elsewhere must reach running replicas, the application owns the refresh. That could be a reload schedule, rebuilding the handler, or another mechanism you control.
Tenant ownershipCopy link to section: Tenant ownership
Derive tenant and user scope from identity your service has already verified. Keep a server-side record that ties that identity to each thread and to every other resource that carries a namespace. Check it before you accept an id from a caller.
Route parameters, request bodies, thread ids and memory scope strings are addressing inputs, not authentication. Keep four things separate: the verified principal, the tenant ownership record, the storage namespace and the authorization decision.
memory.resolveScope receives { routePath, appRoot, principal }, where principal is the caller src/auth.ts verified for this request. Derive the user or tenant dimension from it. A declared dimension left without a value makes memory unavailable for the request instead of using a shared namespace. Test the isolation yourself.
What deleting a thread removesCopy link to section: What deleting a thread removes
DELETE /threads/:thread_id runs in order, and it's not transactional across stores:
- The runtime deletes thread metadata first.
- If the configured checkpoint saver exposes
deleteThread, the runtime awaits that call. - Once the checkpoint step succeeds (or is skipped as unsupported), the runtime destroys the thread's sandbox state and volume, if a sandbox manager is configured.
A saver without deleteThread is skipped. Errors from a supported saver propagate. By then the metadata is already gone, and the error can prevent sandbox cleanup. An HTTP 204 means every attempted step completed. It doesn't mean a cross-store transaction committed.
Global permission decisions, long-term memory, arbitrary workspace files, application database rows and data held by other services all stay. Deleting an account takes an application-level inventory and a workflow that covers every store involved.
Plan for reconciliation of partial deletions, an idempotent retry path for checkpoint and sandbox cleanup, and audit records that tell a missing metadata row apart from a completed cleanup.
Backup, restore, encryption, and retentionCopy link to section: Backup, restore, encryption, and retention
Pick backup and restore procedures for each backend, and rehearse restoring thread metadata and checkpoints together. A restored thread row without its checkpoint history can't reproduce the earlier route state. Workspace files, sandbox volumes and long-term memory each need their own backup plan.
B4.run doesn't encrypt stored values at the application level, and it has no account-erasure transaction across these stores. Postgres rows are plaintext application data unless your app or infrastructure adds encryption. Protect database credentials, storage volumes, backups and access logs to match.
Set retention per data class. Checkpoint history, thread records, permission grants, long-term memory, workspace output, sandbox volumes and backups rarely need the same window. Write down which component expires or deletes each one, and how failed cleanup is retried and audited.
Migration checklistCopy link to section: Migration checklist
- Inventory every store listed above, including local workspace and sandbox data.
- Establish tenant ownership and namespace rules before copying data.
- Provision the destination stores and exercise their native schema initialization and backup path.
- Quiesce or otherwise account for writes while you copy local data with backend-native tooling or an application-specific migration. B4.run has no local-to-Postgres migration command.
- Configure
checkpointer,threadsStore,permissions.store, andmemory.storeexplicitly, then verify representative reads and writes. - Decide how workspace files and sandbox volumes survive replacement compute.
- Rehearse rollback and restoration before removing the local copies.
- Add routing and coordination separately. Shared durable stores don't distribute the active-run gate or the cancellation registry.