Storage map
Source: docs/architecture/storage.md at origin/platform (c2ce9cf), synced 2026-09-26. Do not edit this page here; change the source and run yarn sync:studio.
Every kind of state Studio and its runtime keep, where it lives, who writes and reads it, how durable it is, and what a
restart loses. Verified against Studio API a78212f, data-app-runtime b21a421, studio-snapshot-worker 2241304,
agent-service 95c607eb4 and the preview ConfigMaps.
Backends
| Backend | Where | Durability | Notes |
|---|---|---|---|
| blob-store documents | blob-store.plat-svc:80; Studio metadata namespace bicycle-studio-meta (BSA_METADATA_NAMESPACE), data namespace data-apps (BSA_BLOB_NAMESPACE), runtime namespace rt | Durable (object storage) | blob-store prefixes every key with the tenant derived from the caller's credential, so a document path below means {tenant}/{namespace}/{path}. BSA_METADATA_STORE=blob on preview and prod; the sql option (SQLite / Postgres table workflow_docs and app tables) exists for laptops. create is read-then-write-then-read-back, not atomic; record writes that must not race use a CAS token (workflow.json) or create-only keys |
| Redis | plat-svc-redis.plat-svc:6379, Redis 7.4.8, one replica, AOF on, allkeys-lru, no auth | Testing-grade (#18): keys can be evicted under memory pressure | All Studio broker keys are prefixed bsa-rt:, claim keys bsa-claim:, agent runs / analysis / app cache studio:. When Redis is unreachable the broker, agent runs and analysis are switched off at startup |
| Postgres (Cloud SQL, private IP) | database workflow-runner (workflow-runner; app role workflow_runner_app, RLS by app.tenant_id), database agent-service (usage ledger) | Durable | Studio has no Postgres yet. The runtime's rt schema (rt.claims, rt.invocations, rt.kv) is DDL only; the Postgres store backend is a stub in bicycle-exec and interface-only in Studio |
| Temporal | <temporal host>, namespace default | Durable workflow histories | Task queues wf-control[-preview], wf-io[-preview] (runner), data-app-snapshot[-preview] (snapshot worker) |
| Pod-local disk / memory | runtime-server invocation store (memory backend on preview and prod), agent-service run files under its scratch dir, Studio in-memory tasks and caches | Lost on restart | Listed per row below |
Functions
| State | Where | Writer → readers | Durability / retention | Restart loses |
|---|---|---|---|---|
Function record (name, kind, manifest incl. an agent function's manifest.agent.spec, disabled, enabled_state) | functions/{tenant}/{name}/function.json (tenant library); app-scoped: functions/{tenant}/_app/{app_id}/{name}/… and …/sha/{sha12}.json | Studio FunctionService → Studio, MCP tools/list | Durable, no TTL; disable is a soft state on the record (#44) | — |
| Draft (version 0) and draft revisions | …/draft.json, …/draft_revs/{revision:08d}.json (create-only) | Studio (editor saves, heartbeats) → Studio | Durable; 409 draft_conflict on a stale revision | editing presence is not stored beyond the revision |
| Versions | …/versions/{n}.json (immutable) | Studio publish → broker resolve, MCP pins | Durable | — |
| Packages (code zips) | data ns functions/{tenant}/packages/{sha256}.bin; runtime-server fetches {namespace}/functions/{tenant}/{key} with the caller's credential | Studio upload → runtime-server (local digest-keyed cache, ≤ 256 entries) | Durable; ≤ 8 MiB (BSA_FUNCTIONS_MAX_PACKAGE_BYTES) | runtime-server's local package cache |
| Publish reviews | …/reviews/{diff_sha256}.json {revision, shown_to, shown_at} | review GET → publish POST | Durable; valid BSA_FUNCTIONS_REVIEW_TTL_SECONDS (900 s) for that person | — |
| Tenant / app index | functions/{tenant}/_index/tenant.json, _index/app_{app_id}.json | Studio on every save/publish → list routes | Durable; rebuilt from records if lost | — |
| Badges (tests / evals) | evals/{tenant}/[_app/{app_id}/]_badges.json | eval and test runs → function list and header | Durable | — |
| Evals | evals/{tenant}/{fn}/{datasets/{evs_id},policy,runs/{evr_id},proposals/{evp_id}}.json, evals/{tenant}/_runs/{evr_id}.json, _quota/{yyyy-mm-dd}.json | Studio evals service | Durable; a running eval is an in-process task: runner_lost after BSA_EVALS_RUN_STALE_SECONDS (900) | in-flight eval runs |
There is no separate agents store: an "agent" is a function of kind agent; agent limits per tenant are the document
settings/{tenant}/agent_limits.json (staff-written).
Workflows (Studio side, BlobDocBackend(root="wf"))
| State | Where | Writer → readers | Durability / retention | Restart loses |
|---|---|---|---|---|
Record (title, slug, state, published_revision, draft, notify, expose, schedules, schedule_overrides, disabled) | wf/{tenant}/{wf}/workflow.json (CAS field cas); slug index wf/{tenant}/_slugs/{slug}.json; tenant index wf/{tenant}/_index/workflows.json | Studio WorkflowService → Studio, MCP | Durable, no TTL | — |
| Revisions (document, files list, lock, validation) | wf/{tenant}/{wf}/revisions/{n}.json (immutable, create-only); files by content at data ns wf/{tenant}/{wf}/files/{sha256}.bin | Studio patch/put_file/publish → Studio; the runner reads bundle files at {tenant}/data-apps/wf/{tenant}/{wf}/files/{sha256}.bin | Durable | — |
| Draft | The head revision (revision.kind: draft); editing[] presence on the record for 120 s after a heartbeat | Studio | Durable | — |
| Runs (Studio's projection) | wf/{tenant}/{wf}/runs/{run_id}.json, run-head index wf/{tenant}/{wf}/_runs.json; grant → run index wf/{tenant}/{wf}/grants/{jti}.json | RemoteRunner start, the events relay, reads that refresh status from the runner | Durable, no TTL. Runs before WP2 have invocation_id: null | in-process (deprecated path) runs: a dead pod leaves running, the next read marks interrupted |
| Schedules | On the record (schedules[]: wft_{wf}_{trigger}_{seq}, sync_state, schedule_seq) and in scheduling-service; tick dedupe wf/{tenant}/{wf}/ticks/{trigger}/{fire_key}.json (create-only) | publish / pause / resume / disable → scheduling-service; the tick route | Durable. An id is never reused (seq bumps on every create) | — |
| Artifact versions, pointers, fingerprints (in-process path only) | wf/{tenant}/{wf}/versions/{artifact}/{pkey}/{av_id}.json, pointers/{artifact}/{pkey}.json, fp/{artifact}/{pkey}/{fp}.json, bytes at data ns `data/{artifact}/{pkey}/{av_id}.json | .png` | the deprecated in-process runner; Studio also keeps the runner's snapshot PNGs here (rs_{run}_{node}_{attempt}.png) | Durable |
| Approvals (Studio copy) | wf/{tenant}/_approvals/{apr_id}.json (tenant inbox in one read); on the remote path the runner's approvals table is the truth and Studio merges | Studio decide → inbox | Durable; expiry applied on read (≤ P14D) | — |
| Ledger (in-process path) | wf/{tenant}/{wf}/ledger/{action}/{key_hash}.json (create-only claim) | deprecated in-process runner | Durable | — |
| Mail policy | wf/{tenant}/_policy/mail.json `{allowed_domains, external: approve | deny | allow, updated_by, updated_at}` | tenant Admin PUT → validate/plan facts, sends |
Action policy (landed a78212f) | wf/{tenant}/_policy/actions.json {auto_allowed: [action ids], updated_by, updated_at}; deployment side BSA_WORKFLOW_ACTIONS_ALLOWED (default []) | tenant Admin PUT → validate/plan facts, :send re-check | Durable | — |
| Destinations (deprecated alias lists) | wf/{tenant}/_destinations/{dst_id}.json | Admin PUT (Deprecation: true) → alias resolution for old revisions; destinations_migration moves them onto send steps | Durable | — |
| Publish reviews | wf/{tenant}/{wf}/reviews/{diff_sha256}.json | review GET → publish POST within REVIEW_TTL_S | Durable | — |
| Workflow blobs (function-node files, e.g. ML models) | blob-store {tenant}/workflows/{wf}/store/{name} (declared in the document's blobs; the function asks for blob.read/blob.write on those names) | a code function via the fn/1 blob capability (runtime-server gateway, scoped by the rt-invoke/1 grant) → later runs; the guide's pattern is model-<date>.skops + model-latest.json | Durable, unversioned (put the version in the name) | — |
Workflow runs (workflow-runner Postgres, database workflow-runner)
Migrations 0001–0004. All tenant tables carry tenant_id under row-level security. No retention or cleanup job
exists: rows accumulate.
| Table | Holds | Writer → readers |
|---|---|---|
revisions | every revision Studio pushes (document, lock, files, doc_sha256, form, app_version, overlay_rev, label, timezone) | Studio PUT …/revisions/{n} → the runner |
workflow_seq | plan_seq and state per workflow | runner |
run_triggers | one row per trigger (trigger_id, env, deploy_env, logical_date, idempotency_key, `type manual | cron |
runs | the run (status, `deploy_env prod | preview, plan, triggerincl.policy.on_success, cost_usd, cancel_requested, error, temporal_workflow_id, **run_grant`** in clear) — unique active run per (tenant, workflow, env, logical_date) |
node_runs | one per node partition (status, fingerprint, cache_hit, attempts, outputs, checks, llm, cost_usd, error, log) | activities |
attempts | executor attempts (executor, job_name, pod_uid, grant_scope, staging_prefix, exit_reason) | activities |
artifact_versions | version metadata (data_key, rows, bytes, columns, checks, complete, missing, stale, withdrawn, training fields); bytes at blob {tenant}/rt/wf/{wf}/att/{attempt_id}/{artifact}.json (≤ 64 MiB, written once per attempt) | run_partition → Studio artifact routes |
pointers, pointer_moves, cache_index, leases | what serves per artifact and partition, the move audit, fingerprint cache, locks | runner |
approvals, approval_decisions | send approvals (state, payload_hash, payload_data_key, preview, review, gaps, approvers, expires_at, context since 0004, receipt) and each person's decision | open_action / decisions from Studio → the wf-action child workflow, Studio inbox |
action_ledger, ledger_attempts, ledger_recipients | the per-action ledger keyed by key_hash = sha256(rendered idempotency_key)[:32]; one open attempt per key (ledger_one_open); per-recipient state `approved | sending |
run_index | one row per run for the federated run list (kind, parent_run_id, principal, deploy_env, status, cost_usd, links) | runner → GET /v1/tenants/{t}/run-index |
events_outbox | the run's events (type, payload, relayed_at) relayed at least once, ≤ 200 per run every 2 s, to Studio POST /api/internal/workflows/{wf}/runs/{run}/events | activities → relay task |
Temporal holds the control flow only: workflow data-app-dag (id wfr:{tenant}:{run_id}, reject-duplicate) and the
wf-action child per send (abandon-on-parent-close, waits for the decision signal or expiry). A runner restart loses only
the in-flight activity's progress since its last DB write; the DB is the truth and activities are idempotent by it.
Invocations (Studio, root invocations)
| State | Where | Writer → readers | Durability / retention | Restart loses |
|---|---|---|---|---|
| Invocation record (every kind: code, llm, agent, classify, lookup, workflow root and node children) | Durable copy invocations/{tenant}/by-id/{inv}.json, written with the tenant service identity; live routing record Redis bsa-rt:inv:{id} | broker submit / poll / relay → GET /invocations/{id}, /events, /trace | Redis BSA_FUNCTIONS_RECORD_TTL_SECONDS (7 d); blob copy no TTL, best effort. After the Redis record expires, reads answer from the copy (terminal) | — |
| Day index | invocations/{tenant}/idx/{yyyymmdd}/{created}~{inv}~{kind}~{caller8}.json (empty bodies; a list walks the last 14 days) | broker on write → GET /invocations, GET /runs | Durable | — |
| Events / trace | Part of the record (events[], trace_count); code invocations pass runtime-server's events through; agent invocations read agent_events from the agent run | broker, relay | As the record | code events on runtime-server: memory store (7 d TTL nominal; lost on its restart), 410 once drained |
| Result reuse (app calls) | Redis bsa-rt:reuse:{tenant}:{key} (25 h) and bsa-rt:reuse-lock:… (20 s); durable invocations/{tenant}/reuse/{key}.json {latest, succeeded} | broker invoke_runtime | 24 h window max | — |
| Idempotency (service tokens) | Redis claim `bsa-claim:rt-idem:{tenant}:{rt | ref}:{sha(key)}`, 24 h | broker | best effort |
| Quotas and leases | Redis bsa-rt:conc:t:{tenant}, conc:r:{tenant}:{rt}, conc:f:{tenant}:{scope}:{name} (zsets, TTL timeout + 300 s); CU counters `bsa-rt:cu:t | r | f:…:{yyyymmdd}(2 d);settled:{inv}, desc:{root}, fanout:{inv}, jti:{jti}:{seq}, llm-n | llm-usd:{inv}; reaper zset reap:remote` |
| Agent queue and slots | Redis zset bsa-rt:aq:{tenant} (queued agent invocations), aq:alive:{inv} (45 s), aq:rr, aq:last:{tenant} (7 d), aq:gbusy (20 s), aq:try:{inv}:{attempt} (60 s); the tenant slot is the agent-runs zset studio:agent:{tenant}:slots (lease 900 s) | broker _start_or_queue, dispatched by the caller's own polls | BSA_AGENT_RUNS_QUEUE_TTL_SECONDS 600 then failed: queue_timeout; depth ≤ 50 | queued records survive in the durable store and are re-indexed |
| Draft runs | llm: inv_draft_{hex} recorded inline; agent: inv_agent_{run_id}; code/classify/lookup: ordinary inv_… | DraftRunner | As records | in-flight :submit llm/lookup/classify tasks (RemoteKinds._tasks) fail deadline_exceeded via the reaper |
| MCP pins and cancel | Redis `bsa-rt:mcp-pin:{tenant}:{subject}:{name | wf:{slug}}(3600 s),mcp-call:{tenant}:{subject}:{request_id}` | /mcp/functions | best effort |
| runtime-server's own invocation store | inv:{id} (7 d), inv:by-tenant:{tenant} in the stores factory; memory backend on preview and prod (no BICYCLE_STORES_* set) | runtime-server exec → GET /v1/invocations/{id}, /v1/tenants/{t}/run-index | not durable | all live code invocations on that replica |
Agent runs (Studio agent_runs + agent-service)
| State | Where | Writer → readers | Durability / retention | Restart loses |
|---|---|---|---|---|
| Studio run record | Redis studio:agent:{tenant}:run:{run_id} (24 h); indexes `…:by_finding:{app | -}:{key}, …:by_subject:…(zsets, 30 d); idempotency lock…🔒{app | -}:{idem}(15 min); daily budget…:budget:{yyyy-mm-dd}; slots …:slots` (lease 900 s; nested runs take none) | AgentRunsService.submit → /agent-runs/{id}, invocations |
| Studio durable copy | agent-runs/{tenant}/{app_id or -}/{run_id}.json in the metadata namespace, on submit and on terminal | Studio | Durable | — |
agent-service run files (run.json, events.jsonl, trace.jsonl, output.json, feedback, evidence, _idem, _specs, _quota) | Pod-local under the scratch dir; mirrored to blob-store namespace agent_runs (≤ 500 events / trace rows), which is the read fallback | agent runner → /agents/runs/{id}/trace, …/events | blob copy durable; the local index is not (the StatefulSet has no PVC) | the local run index and any run in flight |
| agent-service slots | AGENT_RUNS_SLOTS=redis → agent-runs:{env}:t:{tenant} zset, :run:{run_id}, daily :d:{tenant}:{day} (2 d); preview does not set it, so slots and daily counts are in-process per pod | agent runner | per pod | all slot state |
| Usage ledger | Postgres database agent-service, table consumption_event (event_id unique, tenant_id, user_id, resource_urn, tokens_in/out, cost, flow, surface, ref, run_id, parent_run_id, studio_invocation_id, model, tool_calls, steps, duration_ms, status, limits_source, details) | agent-service metering → GET /api/v1/usage | Durable, no retention | — |
Analysis jobs (Detect / Explain)
Redis only, no blob copy: studio:analysis:{tenant}:job:{id} (24 h), result:{id} (24 h), fp:{fingerprint} (6 h),
latest:{app}:{analysis}:{scope} (24 h), lock studio:de:lock:{tenant}:{fp} (35 min), slots
studio:de:slots:global:{cls} / studio:de:slots:t:{tenant}:{cls}, queue studio:de:queue:{tenant}, budget
studio:de:budget:{tenant}:{day}. agent-service persists the detect result CSVs behind GET /analysis/results/{id};
Studio's job is the only index of them. Analysis jobs are not invocations today (see "changing").
Data apps, versions, permissions, snapshots
| State | Where | Writer → readers | Durability / retention | Restart loses |
|---|---|---|---|---|
| App record with grants and version pointers | apps/{appId}/app.json (metadata ns) | DataAppService → UI, MCP, the frame's query route | Durable | — |
| Versions (manifest, validated bundle path, state) | apps/{appId}/versions/{n}.json; bundle zip in the data ns at the version's blob_path | upload/complete/publish | Durable. A private draft reports expiresAt = created + BSA_DRAFT_TTL_DAYS (30) but nothing deletes it | — |
| Permissions | rows on the app record (`principal type user | tenant, read | write | publish |
| Tenant profile, model profiles | tenants/{tenant}.json (incl. semanticRouting), profiles/{model}.json | settings PUT, design tools | Durable | — |
| App cache (declared persistence) | Redis studio:{tenant}:app:{app}:cache:{key} + key set …:cache-keys; 1 000 keys / app, 64 KiB / value | the app via /api/data-apps/{id}/cache/* and the fn/1 cache capability | TTL = min(requested, declared); best effort | — |
| App blobs (declared persistence) | data ns {appId}/store/{name} → blob-store {tenant}/data-apps/{appId}/store/ | the app, design tools, the fn/1 blob capability | Durable | — |
| Links and schedules (Schedule Mode) | links/{tenant}/{appId}/{linkId}.json, schedules/{tenant}/{appId}/{id}.json; the cron itself in scheduling-service (payload carries the HMAC cron grant) | schedule routes → the worker's mint_access, deliver | Durable | — |
| Snapshot runs | data ns {appId}/snapshots/{runId}/{started.json, run.json, uploads.json, summary.json, delivery.json, page.png, page.pdf, tab-N.png} | Studio writes started.json; the worker writes artifacts by signed URL and run.json; Studio writes the delivery receipt | Durable, no TTL | — |
| Send claims (Schedule Mode dedupe) | `bsa-claim:sched-send:{tenant}:{app}:{sid | -}:{fire_key}:{recipient_hash}, TTL BSA_SEND_CLAIM_TTL_DAYS(35 d); backendredison preview, off in prod;postgresraisesClaimStoreUnavailable` → the deliver route fails open | internal_deliver_run | best effort |
| Chat thread index | chat_threads/{tenant}/{appId}/{userId}/… (transcripts live only in agent-service) | chat routes | Durable | — |
| Notebooks | notebooks/{id}/{notebook,versions/{n},results/{n}}.json, files at data ns notebooks/{id}/files/… | notebooks service | Durable | — |
| Audit events | audit/{YYYY-MM-DD}/{HHMMSS}-{evt_id}.json (append-only; SQL: audit_events) | every mutating service, best effort | Durable | — |
Studio in-memory state (lost on a Studio pod restart)
In-process workflow runs on the deprecated path; :submit llm/lookup/classify tasks (reaped to deadline_exceeded);
viewer-mode capability channels (the long-poll a sandbox's query/agent/llm.call rides); in-process eval runs;
the MCP tools/list cache (30 s); TTL caches (token validate 300 s, session 30 s, OAuth introspection ≤ 30 s,
support-login sessions 240 s, agents/connections 60 s, actions catalog 300 s, pricing book, semantic catalog,
query-path manifests); the snapshot-session rate limiters.
Changing (in flight on platform, check git log before relying on this page)
- #48, Studio's copies go. At
a78212fStudio still shipsworkflows/{runner,validate,plan,registry,cel,llmrun, sqlrun,classify,graph,partitions,templates,lock}.pyandworkflows/spec/*.json, marked deprecated/frozen, andfactories.pystill constructsInProcessRunnerwhenBSA_WORKFLOW_RUNNER=inprocess(the code default; preview setsremoteandBSA_WORKFLOW_ENGINE=runtime). The runtime side landed inb21a421("runtime-server answers what Studio kept a copy of: registry build facts, gated changes, CEL, llm.call output"). When the Studio deletion lands, the in-process-only rows above (Studio-side artifact versions, pointers, fingerprints, ledger claims) become read-only history, andinprocess/shadowmodes disappear; rollback isgit revertof that commit (see operations.md (in the bicycle-studio-api repository, not published)). - #39, one run list.
GET /api/studio/v1/runsfederates onlyfunctionandworkflowtoday (runtime_broker/runs.pySOURCES). The frozen v1.5 design mirrors agent runs, analysis jobs and Schedule Mode runs into the invocation store with deterministic ids, addsrun_status(partial), makes llm/agent draft runs ordinaryinv_records, and replaces the per-invocation index keys with one day documentinvocations/{tenant}/idx/{yyyymmdd}.jsonmaintained read-modify-write. None of that is onplatformata78212f; the per-invocation key index in this page is what runs. - Action policy landed in
a78212f(row above); the runtime'ssends.auto_refusal(contract 1.6.0) is the one implementation, Studio passesfacts.action_policyandfacts.actions_allowed. - Snapshot identity (P0.2): the app-scoped render token is built but parked; the worker still renders as the tenant support session and refuses apps not shared with the whole tenant.
- Postgres claim store: the
postgresbackend is a stub on both sides; the Redis bridge claim is the only send dedupe outside the runner's ledger.