Step 4: Data Model Design
Status: Data Model Complete
The PRD does not authorize an Android-side memory database. The authoritative durable model below is the contract the separately implemented application backend should enforce behind Mem0. In this Android repository, only the device-scoped enablement preference is persisted; retrieved text, candidates, run state, and provider prompts remain in memory for the current operation only.
| Entity |
Fields |
Primary Key |
Notes |
| AuthenticatedPrincipal |
id: UUID required; subject_hash: string required; created_at: datetime required; disabled_at: datetime? optional |
Surrogate UUID; natural alternate key subject_hash unique |
Backend-only pseudonymous identity derived from the authenticated application session. Android cannot select or override it. Store a one-way/opaque subject binding rather than account details. |
| AssistantAgent |
id: string required; display_name: string required; created_at: datetime required |
Natural key id; recommended value android_assistant |
Stable product identity shared across installs/devices; not a secret and not a randomly persisted device ID. |
| DurableMemory |
id: UUID required; principal_id: UUID required; agent_id: string required; text: string required; schema_version: string required; source: string required (android_assistant); task_outcome_type: string? optional; locale: string? optional; app_package: string? optional; created_at: datetime required; updated_at: datetime required; deleted_at: datetime? optional; provider_memory_id: string? optional |
Surrogate UUID; provider_memory_id is an optional unique external key within the backend tenancy |
Authoritative durable record/index entry. text is an approved/inferred durable fact or concise outcome, never a raw transcript or observed screen text. Metadata is columnar/allowlisted rather than an arbitrary JSON map. Soft-delete is recommended until remote deletion is confirmed. |
| MemoryWrite |
id: UUID required; principal_id: UUID required; agent_id: string required; run_id: UUID required; idempotency_key: string required; status: string required; memory_id: UUID? optional; provider_write_id: string? optional; failure_code: string? optional; created_at: datetime required; updated_at: datetime required; confirmed_at: datetime? optional |
Surrogate UUID; natural alternate key (principal_id, agent_id, run_id, idempotency_key) unique |
Backend idempotency/async-confirmation ledger required by R13-R14. It stores no candidate text or backend response body. Recommended statuses are an implementation extension: PENDING, CONFIRMED, FAILED. |
| MemoryEnablementSetting |
enabled: boolean required, default false; updated_at_epoch_ms: long? optional |
Single Preferences DataStore key memory_enabled |
The only Android-persisted entity. Device-scoped, off by default; disabling stops future task search/add calls but does not delete remote memories. No user ID, token, memory content, backend response, or Mem0 credential is stored with it. |
| Mem0ProviderReference |
durable_memory_id: UUID required; provider_memory_id: string required; provider_revision: string? optional; last_synced_at: datetime? optional |
durable_memory_id; provider_memory_id unique within backend tenancy |
Optional backend adapter record if the implementation needs provider mapping separate from DurableMemory. It contains identifiers only; Mem0 credentials remain in server secret storage, outside this model. |
Not persistent by design:
- MemoryScope(userId, agentId, runId) is an operation value. userId comes from the authenticated identity provider, agentId is stable, and a fresh UUID runId is generated per task or management operation.
- MemoryCandidate, RetrievedMemory, and MemoryContext are bounded transient domain values. They must not be written to Android storage.
- Retrieval cache and completion guards live only for the active service/run. Backend MemoryWrite provides durable cross-retry idempotency.
- Screenshots, bitmaps, accessibility node trees, visible/focused text, notifications, clipboard content, gesture coordinates/traces, conversationHistory, raw model reasoning, prompts, credentials, headers, and raw response bodies have no table, collection, blob, file, log, or preference representation.
| From |
To |
Cardinality |
Foreign Key |
Cascade |
| AuthenticatedPrincipal |
DurableMemory |
1:N |
DurableMemory.principal_id -> AuthenticatedPrincipal.id |
Restrict hard deletion; delete-all should soft-delete only rows bound to the authenticated principal, then reconcile provider deletion. Never cascade across principals. |
| AssistantAgent |
DurableMemory |
1:N |
DurableMemory.agent_id -> AssistantAgent.id |
Restrict agent deletion/update while memories exist; stable natural key should not change. |
| AuthenticatedPrincipal |
MemoryWrite |
1:N |
MemoryWrite.principal_id -> AuthenticatedPrincipal.id |
Restrict principal hard deletion until writes are finalized; privacy erasure deletes/anonymizes ledger rows after provider reconciliation. |
| AssistantAgent |
MemoryWrite |
1:N |
MemoryWrite.agent_id -> AssistantAgent.id |
Restrict deletion/update while write records exist. |
| DurableMemory |
MemoryWrite |
1:N over time; each write references 0..1 memory |
MemoryWrite.memory_id -> DurableMemory.id, nullable while pending/failed |
Set null only if a finalized ledger must outlive a purged memory; otherwise delete ledger under the same authenticated erasure transaction. |
| DurableMemory |
Mem0ProviderReference |
1:0..1 |
Mem0ProviderReference.durable_memory_id -> DurableMemory.id |
Cascade provider-reference deletion when the authoritative memory row is purged; remote provider deletion must be confirmed first or recorded for retry. |
There is no N:M relationship and therefore no join table. Tenant ownership is always AuthenticatedPrincipal -> DurableMemory/MemoryWrite; agent_id is a namespace/filter, never an authorization substitute. run_id is correlation/idempotency, not ownership. Queries, unique constraints, updates, list operations, and deletes must include the session-derived principal_id; foreign identifiers returned by a provider are rejected rather than displayed or injected.
- Primary store: Backend-managed PostgreSQL — recommended for authoritative user isolation, unique idempotency constraints, transactional write confirmation, soft deletion, and auditable scoped list/delete operations. The backend may use Mem0 for inference/vector retrieval, but Mem0 is behind the backend contract and is not the Android security boundary.
- Vector/semantic store: Mem0-managed backend storage/index — used only server-side for semantic add/search. PostgreSQL retains authoritative tenancy, idempotency, lifecycle, and opaque provider mappings if Mem0 cannot enforce all application invariants directly.
- Android local store: Preferences DataStore — one off-by-default memory_enabled boolean. This follows the researched Android/Compose coroutine conventions. No Room/SQLite database is justified because offline memory, local memory browsing, and local durable text retention are explicitly not required and would expand privacy risk.
- Cache layer: Process-memory immutable MemoryContext, keyed by runId, for the active run only; maximum five results, score at least 0.70, maximum 2,000 UTF-8 bytes, descending score, deduplicated by ID then normalized text. Clear it on every terminal path/service destruction. Redis is unnecessary on Android; a backend may add a short-lived cache only if it preserves tenant keys and deletion consistency.
- File storage: None. No JSON files, blobs, screenshots, transcripts, or response dumps.
Schema/ORM Recommendation
- Android schema access: Preferences DataStore with typed wrapper/domain mapping. Use Kotlin serialization only for network DTOs; it is not a persistence ORM. Do not add Room or another ORM for this feature.
- Backend ORM/query builder: The backend stack is outside this repository and remains unspecified, so do not select a language-specific ORM prematurely. For the recommended PostgreSQL design, use the backend project's established typed ORM/query builder and express tenant-scoped composite indexes, foreign keys, check constraints, soft-delete filters, and the unique idempotency constraint in database migrations. If no backend stack exists yet, choose it first and then use its conventional migration-backed ORM rather than letting Mem0 DTOs become the application schema.
- Migration tool: Use the migration system native to the chosen backend stack (for example, Prisma Migrate for TypeScript/Prisma or Alembic for Python/SQLAlchemy). Migrations must be reviewed SQL artifacts and must create the composite ownership/index constraints before traffic is enabled. DataStore needs no relational migration for the initial boolean key; future preference changes use DataStore version/default handling.
- Schema organization: Backend: one schema model per cohesive entity plus explicit migration definitions and separate Mem0 adapter DTOs. Android: grouped pure domain values, separate transport DTOs, and a narrow settings-store schema. Domain models must not import Ktor, JSON, Compose, Android accessibility/graphics, or provider SDK types.
- Note: Exact file paths are determined in Step 5 (Module Design), not here.
Enums (Verbatim from PRD)
| Enum Name |
Values (ALL from PRD) |
Source |
| MemoryResult<T> variants |
Success<T>, Disabled, Unavailable, Rejected |
PRD <pdd-interface> |
| Successful terminal action |
done |
PRD vocabulary / R10-R11 |
| Compose memory-management presentation states |
loading, disabled, empty, unavailable |
PRD verification |
The PRD also requires an error presentation state, but does not declare it as part of a named enum; retain error verbatim when defining the UI state algebra. Likewise, transport cases offline, timed out, rate-limited, unauthorized, and malformed are required failure categories, not declared enum values. No values are renamed or collapsed.
R14 exposes a deliberate contract gap: asynchronous acceptance cannot be represented by the four declared MemoryResult<T> variants. Recommendation: extend the result algebra with Pending(writeId: String) while retaining all four declared variants exactly. Backend MemoryWrite.status should use PENDING, CONFIRMED, and FAILED; these are recommended persistence states, not substitutions for PRD enum values.
- MemoryScope(userId: String, agentId: String, runId: String): shared by identity/scope construction, coordinator, gateway, management operations, and tests. It is transient; backend authorization always derives the principal from the authenticated session.
- MemoryCandidate(userText: String, outcome: String?, explicitRequest: Boolean, metadata: Map<String, String>): shared by orchestration, policy, coordinator, gateway, and tests. Only metadata keys schema_version, source, task_outcome_type, locale, and app_package survive validation.
- RetrievedMemory(id: String, text: String, score: Double, metadata: Map<String, String>): shared by gateway, retrieval policy, settings presentation, model-context rendering, and tests.
- MemoryContext(memories: List<RetrievedMemory>): shared by coordinator, provider-neutral agent request, all three model adapters, and tests; immutable and run-bounded.
- MemoryResult<T>: shared exhaustive operation result across gateway, coordinator, settings state holder, observability mapping, and tests; add Pending to reconcile R14.
- MemoryGateway and MemoryCoordinator: shared domain ports consumed by orchestration, transport/no-op implementations, management state, and test fakes.
- MemoryMetadata: recommended typed shared value replacing free-form maps after boundary parsing; exact keys are the five allowlisted names above, with source = android_assistant.
- AuthenticatedIdentity / backend-session authentication abstraction: shared by scope creation, gateway composition, and management; it exposes pseudonymous identity/session material without logging or persistence.
- MemoryPolicyConfig: shared immutable limits for count, score, byte budget, timeouts, retries, metadata schema version, and enabled default.
- Search/add/list/delete transport request and response DTOs, pagination cursors, redacted error DTO, and DTO mappers: internal to backend transport.
- MemoryWrite transport status DTO and polling response: internal to transport/coordinator; the persisted backend ledger is not exposed as an Android domain entity.
- Retrieval normalization/deduplication keys, byte-budget accumulator, candidate rejection reasons, and forbidden-source markers: internal to deterministic policy implementation.
- Per-run cache entry, begin-once/completion guard, mutex/state-machine representation, and cleanup token: internal to coordinator orchestration and never persisted.
- DataStore preference keys and serialized preference representation: internal to the settings store.
- Compose state algebra and confirmation-dialog state: internal to the memory-management UI; it must include Disabled, Loading, Empty, Content, Unavailable, and Error.
- Ktor timeout/retry configuration DTOs and authentication-header injection details: internal to transport/composition.
- Redacted metric event implementation and elapsed-time buckets: internal to observability; the public reporter accepts allowlisted scalars only.
Proceeding to Step 5: Design