KDCube
← Engineering
KDCube Engineering · Concept

Application Data in KDCube: Where State Lives and How It Moves

Configuration, domain rows, user secrets, conversation files, caches, and public content have different owners and movement paths.

18 July 2026Engineering16 minConceptPunch Card
app storagedata flowsscoped statedurable recordsAI application data architectureKDCube storagedata isolation

An application does not become isolated because every byte is put behind one database abstraction. It becomes understandable when each kind of state has an owner, an address, an authority, a retention rule, and a deliberate path across runtime boundaries. KDCube gives an app several storage and transport surfaces because configuration, domain rows, user secrets, conversation files, caches, and public content are not the same data.

AI application data architecture is often drawn as one cylinder beneath one agent. That picture stops being useful as soon as the application has several users, several workers, connected accounts, generated files, browser widgets, background work, or public content.

KDCube makes the split explicit. A running deployment is bound to one effective tenant/project. Many users and operator-approved apps can share its proc workers, pools, Redis, PostgreSQL, object storage, and mounted filesystem. The runtime therefore carries scope in addresses and request context instead of pretending every user has a separate process.

This article maps those data homes and the paths between them. It does not repeat how to package an app or declare its surfaces; that builder map is in Build an AI App with KDCube.

One terminology note: KDCube now uses app in builder-facing prose. Current source, descriptors, routes, and CLI commands still use bundle in literal identifiers such as bundle_id, bundles.yaml, and bundle_storage_root(). App and bundle name the same deployable unit.

00Implementation highlights

ConceptThe takeaway
Scope before backendIdentify tenant/project, app, user, conversation, turn, and execution scope before choosing a physical store.
Authority is not cachePostgreSQL, an app artifact store, or an external provider owns durable facts; Redis and hot filesystem projections accelerate or deliver them.
Movement is not storageEvent lanes, Data Bus, background jobs, and the communicator move work or observations; they do not become the app's business database.
App files are not chat filesBytes under app storage become user-visible only after an explicit hosting or public-content publication step.
Trusted app, fenced generated codeApp backend code is trusted deployment code. Generated code has a profile-dependent boundary; the reference split executor receives narrow materialized inputs without platform credentials or normal network access.
Physical sharing is a deployment choiceA database, Redis instance, bucket, or EFS can be dedicated or shared; KDCube's logical addresses remain tenant/project scoped either way.

01Start with the address

The useful hierarchy is not "database, cache, filesystem." It is:

tenant / project deployment
  app
    actor and user scope
      session
        conversation
          turn
            execution

Not every record needs every segment. An app-wide search index needs tenant/project/app. A user preference adds user. A conversation attachment adds conversation and turn. An execution snapshot adds execution.

The platform derives these facts from authenticated request and runtime context. Model output may propose an object reference or operation argument, but it does not select an arbitrary tenant, project, or storage root.

The same logical contract can sit on different physical topologies:

SurfaceLogical boundaryPossible physical topology
PostgreSQLtenant/project schema, app-owned tables and row scopesdedicated database or shared instance with separate schemas
Redistenant/project-prefixed keys, streams, locks, sessions, and cachesdedicated Redis or shared keyspace
Object/artifact storagetenant/project/app key prefix, with deeper user/turn scope where requireddedicated bucket or shared bucket/prefix
App filesystemtenant/project/app rootlocal mounted storage or shared filesystem such as EFS
Conversation storagetenant/project/user/conversation/turnconfigured local or object-storage backend

That is the precise isolation claim: the deployment and SDK contracts address the current tenant/project and request scope. It is not a claim that every deployment automatically receives dedicated physical infrastructure.

Logical scope drawers and replaceable physical backends Nested logical drawers identify tenant and project, application, user, conversation, turn, and execution. Separate backend plates show PostgreSQL, Redis, object storage, and filesystem as replaceable deployment choices. ARCHIVE REGISTER A · SCOPED ADDRESS The address is stable. The physical topology can change. Use only the scope segments a record needs; derive them from authenticated runtime context. 01 · TENANT / PROJECT DEPLOYMENT effective runtime boundary 02 · APPLICATION domain owner and namespace 03 · ACTOR / USER 04 · CONVERSATION 05 · TURN 06 · EXECUTION BACKEND PLATES dedicated or shared by deployment choice PostgreSQL schema + app-owned rows Redis prefixed keys · streams · locks · cache Object / artifact storage tenant · project · app key prefixes Filesystem / EFS runtime-selected application root SCOPE IS THE CONTRACT · TOPOLOGY IS A DEPLOYMENT CHOICE
Punch Card archive register. A cabinet starts with one large drawer "tenant / project deployment". Inside it, separate drawers for app, user, conversation, turn, and execution. Beside it, four interchangeable backend plates: PostgreSQL, Redis, object storage, filesystem. The drawer labels stay fixed while the physical backend plates can be dedicated or shared. Caption: "Scope is the contract; topology is a deployment choice."

02The data homes are deliberately different

The following table is the app author's primary routing map.

DataAuthority or configured homeNormal app accessWhat it is for
Deployment app propertiesconfigured app descriptor authority; Redis is the runtime cacheself.bundle_prop(...), await set_bundle_prop(...)non-secret deployment choices, surface policy, feature flags, model roles, UI and integration config
Deployment app secretsconfigured secrets providerawait get_secret("b:..."), await set_bundle_secret(...)credentials and secret material shared by this app deployment
User app propertiesPostgreSQL user_bundle_propsasync user-prop helpersdurable per-user choices owned by this app
User app secretsconfigured secrets provider under user scopeawait get_secret("u:..."), async user-secret helperspersonal provider credentials or other user-owned secret material
App relational/domain dataapp-prefixed tables in the tenant/project PostgreSQL schemaapp-owned async repository/serviceauthoritative entities, revisions, idempotency records, relationships
App filesystem stateruntime-provided bundle_storage_root()app-owned subdirectory below the rootprepared indexes, local mirrors, large reusable assets, app work state
App artifactsBundleArtifactStoragebackend-neutral storage APIapp-owned files/records whose backend may be localfs or object storage
Lightweight cacheRedis KV cache with deliberate namespace/TTLasync KV APIrebuildable acceleration, small flags, leases, derived state
Conversation history and filesplatform conversation, event, artifact, and file storesconversation/file SDK surfacesmessages, timeline events, user uploads, assistant-produced files, turn artifacts
Public contentdurable app artifact records plus a derived hot filesystem tierpublic-content registrycrawlable pages, indexes, sitemaps, social assets, retractions
External provider datathe external providerguarded connected-account or named-service adapterGmail, Slack, and other remote objects that should remain provider-owned

The table also says what not to do. A Slack token does not belong in bundles.yaml. A user preference does not belong in an app-wide property. A large search index does not belong in Redis simply because Redis is reachable. A Data Bus message does not become the record of a completed mutation.

The data homes cabinet A cabinet of eleven labeled drawers, each naming its authority and access path: deployment props under the descriptor authority with Redis as cache; deployment secrets under the secrets provider, locked; user props in PostgreSQL user_bundle_props; user secrets under the secrets provider, locked; domain data in app tables of the tenant/project schema; app filesystem under bundle_storage_root; app artifacts through backend-neutral BundleArtifactStorage; the KV cache in a Redis namespace with TTL, rebuildable; conversation data in platform stores via SDK surfaces; public content in the registry with durable records plus a hot tier; and external provider data, provider-owned behind guarded adapters, locked. The warm plate marks domain data as the authority drawer — caches and transports never replace it. ARCHIVE REGISTER C · THE DATA HOMES Eleven homes, one cabinet. Every home names its authority and its access helper; locks mark the secret drawers. ONE CABINET · EVERY HOME NAMES ITS AUTHORITY DEPLOYMENT PROPSdescriptor authority · Redis is cache DEPLOYMENT SECRETSsecrets provider · get_secret(“b:…”) USER PROPSPostgreSQL user_bundle_props USER SECRETSsecrets provider · get_secret(“u:…”) DOMAIN DATAapp tables in the tenant/project schema APP FILESYSTEMbundle_storage_root() subtree APP ARTIFACTSBundleArtifactStorage · backend-neutral KV CACHERedis namespace + TTL · rebuildable CONVERSATION DATAplatform stores · SDK surfaces PUBLIC CONTENTregistry: durable records + hot tier EXTERNAL PROVIDERprovider-owned · guarded adapters DOMAIN DATA IS THE AUTHORITY DRAWER caches and transports never replace it AUTHORITY OR CONFIGURED HOME FIRST · ACCESS HELPER SECOND
Eleven homes, one cabinet — authority first, access helper second.

03App-owned state: rows, files, artifacts, and cache

Relational state

An app may create its own tables in the deployment's tenant/project PostgreSQL schema. Table names are app-prefixed; row columns carry the domain scopes the data actually needs, such as user, agent, conversation, object, or revision.

Schema setup must be idempotent and safe when several proc workers load the app at once. A Postgres advisory lock or another database-level critical section is the correct boundary for shared DDL. An asyncio.Lock protects only one process.

KDCube does not currently promise that deleting an app automatically drops its tables. The app must document schema ownership, migrations, retention, backup, and the operator cleanup path. This limitation is more useful than implying an automatic deprovision lifecycle that does not exist.

Shared app filesystem

bundle_storage_root() gives entrypoint code the stable root selected by the runtime:

<app-storage-root>/<tenant>/<project>/<safe-app-id>/

The app owns named subdirectories below it. The deployment decides whether the root is local mounted storage or a shared filesystem such as EFS. App code does not put host paths, file:// URIs, or mount assumptions in its properties. Persistence and cross-worker visibility therefore follow the selected mount: an instance-local directory is not a fleet-wide durable authority merely because it came from the runtime helper.

Prepared indexes, cloned repositories, generated registries, and other shared filesystem outputs need a source signature plus a shared-storage critical section. Several workers can execute on_bundle_load() for the same app in their own processes.

Backend-neutral app artifacts

BundleArtifactStorage is a separate app-owned artifact API. Its logical layout is tenant/project/app scoped, while deployment configuration chooses a localfs or object-storage backend. Its retention and availability follow that configured backend. This is the right abstraction for artifact-style objects when app code should not know the physical URI.

In proc's async paths, use await storage.write_a(...), await storage.read_a(...), and async for ... in storage.iter_bytes(...). The compatibility methods list(...), exists(...), and delete(...) are synchronous today; keep them out of the shared event loop or move a bounded call behind an explicit thread boundary.

Redis cache

The async KV cache is for small, rebuildable state and coordination. Use a tenant/project/app namespace, a TTL when staleness is acceptable, and a durable authority elsewhere when loss matters.

Redis also carries platform queues, leases, discovery, notifications, and streams. The fact that a value travels through Redis does not make Redis the domain authority for that value.

All application I/O must remain async end to end. A synchronous filesystem, database, Redis, HTTP, subprocess, sleep, or lock call inside an async def still blocks the shared proc event loop. Use async APIs; move an unavoidable bounded legacy call off the loop explicitly.

Application storage catalog A punch-card catalog compares PostgreSQL, the application filesystem, BundleArtifactStorage, and Redis by scope, authority, rebuildability, and lock boundary. ARCHIVE REGISTER B · STORAGE CATALOG Choose a home by ownership, not convenience. HOME SCOPE AUTHORITY REBUILD? SHARED LOCK PostgreSQL domain records app + row durable rows NO DB advisory lock App filesystem prepared shared assets app root selected mount MAYBE shared-storage lock BundleArtifactStorage backend-neutral artifacts tenant/project/app artifact backend POLICY object operation Redis cache + coordination prefixed key another durable home YES lease / atomic op DO NOT PROMOTE A TRANSPORT OR CACHE INTO THE BUSINESS RECORD
Archive card with four rows: "PostgreSQL / durable domain records", "app filesystem / prepared shared assets", "BundleArtifactStorage / backend-neutral artifacts", "Redis / rebuildable cache and coordination". Each row has columns Scope, Authority, Rebuildable?, and Lock. A red catalogue divider reads "Do not promote a transport or cache into the business record."

04Conversation data is a platform record, not an app folder

Conversation messages, external events, turn logs, user attachments, hosted assistant files, and execution snapshots have a different lifecycle from app-owned storage. They follow user/conversation/turn addresses and are reloaded through the conversation SDK and chat component.

Two boundaries matter:

  1. **Storing bytes under the app root does not publish them to a conversation.** A trusted tool must use the file result/hosting contract so the platform records hosted metadata and emits chat.files.
  2. App storage is not automatically agent-readable. An agent receives a logical object/ref or a materialized workspace input only through an explicit tool, named-service provider, MCP adapter, search surface, pull policy, or rehoster.

This prevents two accidental shortcuts: exposing a host path as a user-facing download, and giving generated code ambient access to app or platform storage.

A typical generated-file journey is:

agent or tool proposes work
  -> trusted tool writes bytes in the current output area
  -> file host registers bytes under tenant/project/user/conversation/turn
  -> chat.files carries hosted metadata
  -> browser or Telegram delivery reads hosted bytes through the file service

The user-facing file is therefore an addressed platform object, not a leaked logical path.

05Data Bus, event lanes, jobs, and the communicator move different things

KDCube has several delivery mechanisms because they preserve different semantics.

PathDurable admission?What it carriesDurable authority after handling
Conversation event busyesuser prompt, followup, attachment, selected context, agent-visible eventconversation/event stores and the resulting turn record
Data Busyesapp-domain messages and requested mutations, optionally partitioned by objectthe app's database or storage after the handler commits
Background jobsyesready work already made durable or reconstructable by the producerthe app-owned work/result record
Communicatortransient deliveryprogress, peer/session/project UI updates, handler repliesnone unless another owner records the event
Telemetry/recording sinkobservationapproved facts about work that happenedconfigured telemetry or recording store, not domain state

For Data Bus, an ingress acknowledgement means the message entered the stream. It does not mean the app committed the mutation. The handler still checks idempotency, base revision, actor visibility, and domain invariants before writing authoritative state. A later communicator reply can report success or conflict to the browser.

Conversation events are different. They enter the ordered tenant/project/user/conversation/agent lane because they should influence an agent turn. A board patch should stay on Data Bus unless the app deliberately bridges the committed change into conversation context.

Background jobs are different again. The queue transports retryable work to proc; it should not be the only copy of an invoice, import, or report request whose loss matters.

Data delivery routes and their authorities Five tracks for conversation events, Data Bus, background jobs, communicator delivery, and telemetry enter trusted application code and terminate in distinct authoritative destinations. ARCHIVE REGISTER C · DELIVERY ROUTES A route describes delivery, not ownership. Admission and transport facts do not replace the authoritative record created after handling. Conversation event lane durable ordered admission Data Bus durable app-domain message Background job retryable queued work Communicator transient UI delivery Telemetry sink approved observations TRUSTED APPLICATION validate + authorize idempotency · revision · scope perform async work commit or emit bounded result OBSERVED EFFECT Conversation / turn records messages · events · files · timeline App-owned durable state database · artifact · work/result record handler commit is the authority No durable authority communicator fades after delivery Observation store reviewable evidence, not domain state DELIVERY FACT → HANDLER EFFECT → EXPLICIT AUTHORITY
Five horizontal punched tracks enter an app: conversation event lane, Data Bus, background jobs, communicator, and telemetry. Conversation ends in conversation records; Data Bus and jobs end in an app-owned database/artifact drawer; communicator fades after delivery; telemetry ends in an observation drawer. Caption: "A route describes delivery, not ownership."

06Public content uses a durable tier and a hot serving tier

Public content is a useful example because it makes source-of-truth and projection explicit.

The app decides which items are published, updated, or retracted. The public-content registry stores full item records and a generation counter in BundleArtifactStorage. A derived hot tier under the app filesystem stores the serving index and mirrored item records.

write time
  app publish/update/retract
    -> durable item + generation
    -> atomic hot-tier update + signature

read time
  crawler/catalog request
    -> hot index/item only
    -> rendered HTML, metadata, JSON-LD, sitemap, or 410

Crawler traffic does not invoke app business operations and does not read the durable backend on every request. A missing or stale hot tier is rebuilt under a shared lock by comparing generation markers, not timestamps.

This is not a generic recommendation to maintain two copies of everything. It is a deliberate read model for a public, anonymous, high-read surface.

07Trusted app code and generated code have different boundaries

App backend code is trusted deployment code. It can use the SDK's scoped database, cache, storage, secret, network, and integration surfaces. KDCube does not sandbox a deployed app from the platform that its operator chose to run.

Generated code is a separate case. An agent harness can use the reusable workspace and execution runtime:

logical ref or requested input
  -> trusted resolver under bound user/authority
  -> approved bytes materialized into this execution workspace
  -> generated code runs under the selected execution profile
  -> trusted supervisor mediates privileged tools
  -> declared outputs are hosted back into the conversation

Isolation is profile-dependent. Reference production descriptors select split Docker: generated code runs in a restricted executor with a narrow mount/environment surface and no platform credential or normal network path; privileged services remain in the trusted supervisor. A local subprocess profile inherits the host environment and network, is useful for controlled development, and is not the same security boundary.

In the split profile, the executor does not discover a platform storage root and browse it. A trusted, user-bound component resolves and materializes the specific bytes approved for that execution.

08A complete mutation journey

Consider a user editing one card in a collaborative board:

A complete mutation journey Six numbered steps down one punched track. One: the widget publishes a Data Bus message with tenant, project, app, subject, object_ref, idempotency key, and base revision. Two: proc admits it to the app stream with an authenticated actor and configured subject visibility. Three: the handler claims the object partition — one serial mutation path per object. Four, the warm authority step: the domain repository checks authority and revision, then commits the new row and revision in PostgreSQL. Five: a transient success or conflict reply returns — the database remains the record. Six, optional: a conversation external event, only when an agent should react to the committed change. ARCHIVE REGISTER D · ONE MUTATION, END TO END A user edits one card on a shared board. Six stamped steps; the browser never chooses a schema, the stream never becomes the record. 01 Widget publishes a Data Bus message tenant/project/app · subject · object_ref · idempotency_key · base_revision 02 Proc admits it to the app stream authenticated actor · configured subject visibility 03 The handler claims the object partition one serial mutation path for this object 04 The repository checks authority and revision then commits the new row/revision in PostgreSQL — the authority 05 A transient success / conflict reply returns the database remains the record; the reply is delivery 06 Optionally: a conversation external event only when an agent should react to the committed change THE BROWSER NEVER CHOOSES A TRUST BOUNDARY · THE AGENT SEES ONLY WHAT POLICY ADMITS
One mutation, end to end — the stream never becomes the record.

At no point does the browser choose a database schema or trust boundary. At no point does the Redis stream become the board record. And the agent sees the change only when product policy says it belongs in conversation context.

09The practical review

For every new state or data flow, answer these questions:

  1. What is the smallest correct scope: deployment, app, user, conversation, turn, execution, or external provider?
  2. Which store is authoritative after the operation succeeds?
  3. Is any Redis/filesystem copy only a cache, queue, index, or projection?
  4. Which identity selects the row or object, and where is that identity resolved?
  5. Does the operation need idempotency, revision checks, a distributed lock, or all three?
  6. How are bytes made user-visible or agent-readable without exposing a host path?
  7. What is the retention, backup, export, and app-removal procedure?
  8. Which execution profile handles generated code, and what inputs are actually materialized?

If those answers are explicit, the data model remains inspectable even when several users, apps, workers, and transports share one deployment.

Read the contracts

Platform documentation:

Related public articles:

The Markdown file beside this page is the approved indexing source. Literal platform contracts retain names such as bundle_id, bundles.yaml, and SDK bundle decorators.

KDCube Engineering
18 · 07 · 2026