# CLAUDE.md — Mana Unified App Project-level guidance for `apps/mana/`. For monorepo-wide patterns (auth, services, dev commands, env vars), see the **[root `CLAUDE.md`](../../CLAUDE.md)**. ## Project Overview **Mana** is the unified web app at **mana.how**, serving 27+ product modules (todo, calendar, contacts, chat, notes, dreams, memoro, cards, picture, presi, music, storage, …) under one SvelteKit build, one IndexedDB, one auth session, one deployment. ``` apps/mana/apps/ ├── web/ # SvelteKit 2 + Svelte 5 unified app — the main surface └── landing/ # Astro static landing → Cloudflare Pages ``` Note: `apps/mana/apps/mobile/` was removed on 2026-04-20 along with five other product mobile apps (cards, chat, context, picture, traces). The only remaining Expo/React Native surface in the repo is `apps/memoro/ apps/mobile/`. ## Module System Each module lives in `apps/web/src/lib/modules/{name}/` and registers itself via `module.config.ts`. Module state is split into three files: | File | Role | |------|------| | `collections.ts` | Dexie table references + (sometimes) seed data | | `queries.ts` | **Read-side** — Dexie liveQuery hooks, type converters, pure helpers for `$derived` | | `stores/*.svelte.ts` | **Write-side** — mutation methods. Never reads for UI rendering (queries.ts does that). Only reads when a mutation needs existing state (toggle, increment). | ### Module store pattern ```typescript // modules/todo/stores/tasks.svelte.ts export const tasksStore = { async createTask(input: {...}) { const newLocal: LocalTask = { ...input, id: crypto.randomUUID() }; const plaintextSnapshot = toTask({ ...newLocal }); await encryptRecord('tasks', newLocal); await taskTable.add(newLocal); return plaintextSnapshot; }, }; ``` ```typescript // modules/todo/queries.ts export function useAllTasks() { return useLiveQueryWithDefault(async () => { const locals = await db.table('tasks').orderBy('order').toArray(); const visible = locals.filter((t) => !t.deletedAt); const decrypted = await decryptRecords('tasks', visible); return decrypted.map(toTask); }, [] as Task[]); } ``` ## Data Layer (Local-First) The app reads and writes IndexedDB **first**, then syncs to **mana-sync** (Go, port 3050) in the background. One Dexie database (`mana`) holds 120+ collections from every module — colliding table names get a module prefix (e.g. `todoProjects`, `cardDecks`, `presiDecks`). ``` User action (e.g. tasksStore.createTask) │ ▼ Module store builds the LocalRecord │ ▼ encryptRecord(tableName, record) │ ▼ table.add(encryptedRecord) ← Dexie write │ ▼ Dexie hooks (database.ts): - stamp userId (user-level tables only) - stamp __fieldMeta[k] = { at, actor, origin } per field - stamp _updatedAtIndex (local-only shadow for indexed sorts) - record into _pendingChanges (tagged with appId + actor + origin) - record into _activity │ ▼ Sync engine (sync.ts) — debounced 1s - groups changes by appId - POSTs to mana-sync │ ▼ mana-sync → PostgreSQL with field-level LWW + RLS │ ▼ Other clients pull via SSE / polling │ ▼ applyServerChanges → Dexie hooks (suppressed) → liveQuery → decryptRecord → UI ``` **Deep dive**: [`apps/web/src/lib/data/DATA_LAYER_AUDIT.md`](apps/web/src/lib/data/DATA_LAYER_AUDIT.md) — sync engine, retry/backoff, quota recovery, telemetry, RLS, encryption rollout, threat model. **Single most important file for understanding how the app works under the hood.** ### Conflict-Detection (post 2026-04-26 sync-field-meta-overhaul) The four bug-roots that made the conflict-toast fire spuriously have all been closed. Architecture today: - **`__fieldMeta`** (single hidden field per record, replaces the older `__fieldTimestamps` / `__fieldActors` / `__lastActor` triple). Shape: `{ [field]: { at, actor, origin } }`. The Dexie creating/updating hook stamps it on every write; consumers read it via `readFieldMeta()` and `deriveUpdatedAt()` from `$lib/data/sync`. - **Origin-tracking**: `originFromActor(actor)` in `@mana/shared-ai` maps `actor.kind` onto `'user' | 'agent' | 'system' | 'migration' | 'server-replay'`. The conflict surface fires only when `localFieldMeta.origin === 'user'` — replay-deltas from server pulls, agent writes, migration helpers, and bootstrap inserts never surface as toasts. - **`updatedAt` is no longer a synced data field.** Type-converters compute `updatedAt` on read as `max(__fieldMeta[*].at)` via `deriveUpdatedAt(local)`. For Dexie-indexed sort, every record carries a non-synced `_updatedAtIndex` shadow column that the hook stamps automatically — `orderBy('_updatedAtIndex')` instead of `orderBy('updatedAt')`. - **Server-side singleton bootstrap**: mana-auth writes per-user and per-Space singletons straight into `mana_sync.sync_changes` with `origin: 'system'`. `userContext` (per-user) is bootstrapped from the `/register` flow; `kontextDoc` (per-Space) is bootstrapped from the personal-space hook in `databaseHooks.user.create.after` and from `organizationHooks.afterCreateOrganization` for every non-personal Space. The webapp's `getOrCreateLocalDoc()` survives in both stores only as a fallback for the rare race where the first pull hasn't landed yet. - **Stable `client_id`**: Dexie table `_clientIdentity` (single row keyed by `id='self'`) is the canonical source of the per-device sync identity. `restoreClientIdFromDexie()` runs once at boot and reconciles localStorage ↔ Dexie — a localStorage wipe gets restored from Dexie, the server keeps seeing the same client. When writing new code: | Pattern | Use this | | --- | --- | | Read "last modified" for UI | `deriveUpdatedAt(local)` (returns ISO string) | | Sort a Dexie query by recency | `.orderBy('_updatedAtIndex')` | | Stamp a system/migration write | wrap in `runAsAsync(makeSystemActor(SYSTEM_MIGRATION, '