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.
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
| Concept | The takeaway |
|---|---|
| Scope before backend | Identify tenant/project, app, user, conversation, turn, and execution scope before choosing a physical store. |
| Authority is not cache | PostgreSQL, an app artifact store, or an external provider owns durable facts; Redis and hot filesystem projections accelerate or deliver them. |
| Movement is not storage | Event 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 files | Bytes under app storage become user-visible only after an explicit hosting or public-content publication step. |
| Trusted app, fenced generated code | App 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 choice | A 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:
| Surface | Logical boundary | Possible physical topology |
|---|---|---|
| PostgreSQL | tenant/project schema, app-owned tables and row scopes | dedicated database or shared instance with separate schemas |
| Redis | tenant/project-prefixed keys, streams, locks, sessions, and caches | dedicated Redis or shared keyspace |
| Object/artifact storage | tenant/project/app key prefix, with deeper user/turn scope where required | dedicated bucket or shared bucket/prefix |
| App filesystem | tenant/project/app root | local mounted storage or shared filesystem such as EFS |
| Conversation storage | tenant/project/user/conversation/turn | configured 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.
02The data homes are deliberately different
The following table is the app author's primary routing map.
| Data | Authority or configured home | Normal app access | What it is for |
|---|---|---|---|
| Deployment app properties | configured app descriptor authority; Redis is the runtime cache | self.bundle_prop(...), await set_bundle_prop(...) | non-secret deployment choices, surface policy, feature flags, model roles, UI and integration config |
| Deployment app secrets | configured secrets provider | await get_secret("b:..."), await set_bundle_secret(...) | credentials and secret material shared by this app deployment |
| User app properties | PostgreSQL user_bundle_props | async user-prop helpers | durable per-user choices owned by this app |
| User app secrets | configured secrets provider under user scope | await get_secret("u:..."), async user-secret helpers | personal provider credentials or other user-owned secret material |
| App relational/domain data | app-prefixed tables in the tenant/project PostgreSQL schema | app-owned async repository/service | authoritative entities, revisions, idempotency records, relationships |
| App filesystem state | runtime-provided bundle_storage_root() | app-owned subdirectory below the root | prepared indexes, local mirrors, large reusable assets, app work state |
| App artifacts | BundleArtifactStorage | backend-neutral storage API | app-owned files/records whose backend may be localfs or object storage |
| Lightweight cache | Redis KV cache with deliberate namespace/TTL | async KV API | rebuildable acceleration, small flags, leases, derived state |
| Conversation history and files | platform conversation, event, artifact, and file stores | conversation/file SDK surfaces | messages, timeline events, user uploads, assistant-produced files, turn artifacts |
| Public content | durable app artifact records plus a derived hot filesystem tier | public-content registry | crawlable pages, indexes, sitemaps, social assets, retractions |
| External provider data | the external provider | guarded connected-account or named-service adapter | Gmail, 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.
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.
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:
- **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. - 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.
| Path | Durable admission? | What it carries | Durable authority after handling |
|---|---|---|---|
| Conversation event bus | yes | user prompt, followup, attachment, selected context, agent-visible event | conversation/event stores and the resulting turn record |
| Data Bus | yes | app-domain messages and requested mutations, optionally partitioned by object | the app's database or storage after the handler commits |
| Background jobs | yes | ready work already made durable or reconstructable by the producer | the app-owned work/result record |
| Communicator | transient delivery | progress, peer/session/project UI updates, handler replies | none unless another owner records the event |
| Telemetry/recording sink | observation | approved facts about work that happened | configured 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.
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:
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:
- What is the smallest correct scope: deployment, app, user, conversation, turn, execution, or external provider?
- Which store is authoritative after the operation succeeds?
- Is any Redis/filesystem copy only a cache, queue, index, or projection?
- Which identity selects the row or object, and where is that identity resolved?
- Does the operation need idempotency, revision checks, a distributed lock, or all three?
- How are bytes made user-visible or agent-readable without exposing a host path?
- What is the retention, backup, export, and app-removal procedure?
- 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:
- App storage and cache
- App transports
- Runtime configuration and secrets
- Tenant, user, authority, and execution boundaries
- Conversation Event Bus and Data Bus
- Public content provider
- Public content solution
- Isolated execution runtime
Related public articles:
- Data in Motion: The Data Bus and the Live Relay
- The Conversation Is a Lane
- Connection Hub Storage: Tokens, Edges, and the Line Between Them
- Public Content to CDN
- Isolated Code Execution
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.