Skip to article
PlanToCodeDocsGet the app

HandbookArchitecture

Storage ownership

Who owns which database, why PlanToCode never opens a Codex file itself, how a transcript source is identified, and the atomic identity claim behind the outbox.

Checked against the source on 17 September 2026

On this page

Separate the stores by owner

Codex’s files sit inside PlanToCode’s folder, yet only Codex opens them
Codex’s files sit inside PlanToCode’s folder, yet only Codex opens themPlanToCode’s app data folder holds its own database and files and, one level down, a home folder for each Codex profile. The desktop runtime opens its own files directly, creates and deletes profile folders, and reaches everything inside a home only through JSON-RPC to the Codex child. An attached rollout can live at any path, and Codex opens it only when a request names it. Off the computer, the server keeps accounts and billing, and the relay forwards desktop data to phones without storing it.
Your computer
PlanToCode desktopRust runtime
Codex app-serverone child process per profile pair
JSON-RPC over stdio
com.plantocode · app data
appdata.dbsession links, outbox, live overlay
command output and attachmentscodex-command-outputs/ · pasted-images/
codex/profiles/<uuid>/ · created and deleted by PlanToCode
home/ · opened only by Codex
sessions/ · archived_sessions/ · .jsonl · .jsonl.zstprivate SQLite · auth.json
attached rolloutany path · Codex opens it when a request names it
~/.codexa separate Codex home, never searched
Regional serveraccounts and billing in PostgreSQL and Redis
relay · not stored
Phoneunsent messages, caches, view state
  • PlanToCode datacodex-command-outputs/ · pasted-images/ · appdata.db

    The desktop runtime opens these itself: SQLx for the database, plain file reads and writes for command output and chat attachments.

  • Codex homecodex/profiles/<uuid>/home/

    PlanToCode creates the folder, launches Codex with it as CODEX_HOME, and deletes the whole profile folder when you remove the profile, unless a session still links a rollout inside it. It never opens a file inside. History comes back through thread/items/list.

  • Startup guard--allowed-storage-root = CODEX_HOME = sqlite_home

    The bundled app-server refuses to start unless all three resolve to the same folder, so Codex can’t keep its databases anywhere else.

  • Off this computerPostgreSQL · Redis

    The server keeps accounts, devices, credits, and usage in PostgreSQL, and login handoffs, rate limits, and charge reservations in Redis. What the relay forwards isn’t stored.

StoreOwner and contentAccess rule
PlanToCode appdata.dbDesktop product metadata, session links, outbox operations, remote idempotency, and live overlay state.PlanToCode Rust repositories use SQLx and SQLite.
App-owned Codex profile homeThe Codex child owns its private databases, auth/config, and normal owned session storage.Use the app-server protocol. PlanToCode must not query the child’s private SQLite schema.
An explicitly selected conversation sourceThe exact linked transcript path and thread identity define the source.Access the selected source through path-aware app-server methods. Do not search other homes for a replacement.
Server PostgreSQLAccount and service data controlled by server repositories and database roles.Separate migration, system-runtime, and tenant-runtime principals.
Phone stateView state, local copies of unsent messages, and preview and output caches.Treat the desktop as authority for projects, execution, outbox, and files.

The WebView asset protocol is scoped to media previews under the app cache only, so no private runtime file is reachable from the page.

Storage identity and execution identity differ

An app-owned Codex profile has a stable opaque UUID that must be canonical lowercase text; an email address and a filesystem path are not profile identity. codex_session_links keys the link by app session and carries profile_id, codex_thread_id, a nullable rollout_path, and a status of available, profile_required, or unavailable, with a partial unique index that allows exactly one available link per thread and profile. Indexed and Exact are design names; in code one predicate decides: a link whose rollout path lies outside the runtime home is an external exact source, reads pass path and historySources only when a rollout path is supplied, and the wire storageFormat stays indexed either way. Both routes call thread/items/list on the Codex app-server, and an exact read adds path and historySources. An operation resolves its route once and keeps it.

Exact source access has deliberate limits

An exact read stitches history from byte ranges of the files it is given
An exact read stitches history from byte ranges of the files it is givenFor an exact source the desktop sends the rollout path and the session’s other rollouts as historySources. The Codex app-server replays the parent’s bytes up to the offset the child recorded, then the child’s own bytes, and binds the cursor to a digest of exactly those bytes. Appending to the child keeps the cursor valid, and rewriting read bytes forces a reload. A parent that isn’t listed fails the read, even when its file sits in the same folder. Sizes are examples.
Codex app-serverthread/items/list · threadId · path · historySources · cursor
parent listed in historySources
parent.jsonl
inherited prefix
not read
history_base.end_byte_offset
child.jsonl · path
own bytes
rewritten or truncated: reloadRequired
appended later: still valid
one replay
parent prefix
child
the cursor’s digest covers these bytes
parent not listed
child.jsonl · path
history_base
parent.jsonlsame folder, not in historySources
missingHistorySource: the read fails. No folder or Codex home is searched.
  • historySources[{ rolloutId, path }]

    Built by the desktop from the absolute paths of the session’s other linked rollouts, which it records as Codex announces each child. It’s the only place the app-server looks for an ancestor.

  • history_basethread_id · end_byte_offset

    Session metadata at the top of the child’s file: the ancestor rollout and the byte where the inherited part ends. The app-server follows up to 256 ancestors and rejects a cycle or an ancestor shorter than that offset.

  • reloadRequirederror data.code

    The cursor carries a digest of every byte it covered. Appending keeps it valid. Changing those bytes fails the next page, which the desktop shows as an ordinary load error. Only sourceUnavailable offers to choose the source file again.

ConditionContract
Required ancestor source is absentThe app-server fails the read with missingHistorySource, even if the file sits next to the child. The desktop shows a generic load error. Only sourceUnavailable becomes CODEX_SOURCE_ERROR, which offers to choose the source file again.
Source is replaced or truncatedThe app-server binds each cursor to a digest of the bytes it read. Appending keeps cursors valid. Truncating or rewriting fails the next page with reloadRequired, shown as a generic load error. “Timeline source changed; reload the timeline” is the desktop’s error when the linked source itself changed.
Current method takes only a thread IDDo not offer that mutation for an exact external source if it cannot address the selected path. This applies to current goal, rollback, and persisted thread-settings operations.
Another bundled sidecar owns the writerRelease thread ownership before a different runtime resumes it.

Before a send or an attach, the desktop releases the thread from every other profile server it runs. That coordinates the bundled sidecars of this process; it does not prevent an unrelated external Codex process from writing the same source, so stop an external writer before continuing that conversation in PlanToCode.

The outbox uses an atomic identity claim

Key relationships in the product database — schematic, not migration SQL
workspace_chat_outbox
  session_id → entries_json, updated_at

workspace_chat_outbox_operation_identity
  (session_id, operation_id) → request_fingerprint

remote_rpc_idempotency
  scope_key → request_fingerprint, state, response_json, lease, expiry

codex_live_timeline_overlays
  timeline target → persisted overlay snapshot

codex_live_timeline_overlay_revisions
  singleton → one global revision sequence

codex_chat_operation_ledger
  (app_session_id, operation_id) → status, result_json

save_workspace_chat_outbox_with_operation_identity opens one transaction. Its INSERT … ON CONFLICT updates the identity row only when the existing fingerprint equals the submitted fingerprint. If that statement does not affect one row, the transaction rolls back. It then upserts the outbox entries JSON and commits. The identity row is keyed by the base session while the outbox JSON is saved under the alias session, so the same operation can be found from any alias in the scope.

The remote_rpc_idempotency table is the second ledger. It keeps the outcome of every listed remote mutation for 48 hours across desktop restarts, so a phone that retries after the desktop came back still gets the stored answer.

Project file access is a separate boundary

Remote file reads run with the desktop user’s filesystem access. resolve_desktop_file_read_path expands a home shortcut, joins a relative path onto the canonical project directory, and canonicalizes the result; it does not enforce project containment, and the design note says so on purpose. allowExternalFile and allowExternalMedia only decide which resolver is tried first, so an authenticated companion can open paths outside the selected project either way. ensure_path_within_project, which rejects parent components and checks the canonical root, exists for desktop-local operations.

App-managed chat attachments live in pasted-images under the app data directory, and an external media read for an attachment must classify as image, video, audio, or document. The phone never uses its own home directory.