managarten/apps/mana/CLAUDE.md
Till JS 142a65a22f docs: Phase 9 documentation roundup — close encryption-shaped doc gaps
Five documentation surfaces gained encryption awareness in this
sweep. Before this commit, the only place anyone could learn about
the at-rest encryption layer or the zero-knowledge opt-in was the
internal DATA_LAYER_AUDIT.md. New contributors and self-hosters
would never discover one of the most important features of the
product just by reading the standard onboarding docs.

apps/docs/src/content/docs/architecture/security.mdx (NEW)
----------------------------------------------------------
First-class user-facing security page in the Starlight site,
slotted into the Architecture sidebar between Authentication and
Backend.

Sections:
  - What's encrypted (overview table of 27 modules + the
    intentional plaintext carve-outs)
  - Standard mode flow with ASCII diagram
  - "What Mana CAN see" trust statements per mode
  - Zero-knowledge mode setup walkthrough (Steps component)
  - Unlock flow on a new device
  - Recovery code rotation
  - Deployment requirements (the loud MANA_AUTH_KEK warning)
  - Audit trail action vocabulary
  - Threat model summary table
  - Implementation file references with paths

services/mana-auth/CLAUDE.md
----------------------------
New "Encryption Vault" section under Key Endpoints, listing all 7
routes (status, init, key, rotate, recovery-wrap GET+DELETE,
zero-knowledge) with their HTTP method, path, error codes, and a
description. Mentions the three CHECK constraints + RLS + audit
table. Points readers at DATA_LAYER_AUDIT.md and the new
security.mdx for the deep dive.

Environment Variables block gains MANA_AUTH_KEK with a multi-line
comment explaining the openssl rand command + dev fallback warning.

apps/mana/CLAUDE.md
-------------------
Full rewrite. The existing file was from the Supabase era and
described things like @supabase/ssr, safeGetSession(), and a
five-table schema with users + organizations + teams that doesn't
exist any more. Replaced with the unified-app architecture:

  - Module system layout (collections.ts / queries.ts / stores/)
  - Mana Auth (Better Auth + EdDSA JWT) instead of Supabase
  - Local-first data layer with the full pipeline diagram
  - At-rest encryption section with the "when writing module code
    that touches sensitive fields" 4-step guide
  - Updated routing structure (no more separate /organizations,
    /teams routes)
  - Module store pattern code example
  - Reference document table at the bottom pointing at the audit,
    the new security.mdx, and the auth doc

Root CLAUDE.md
--------------
New "At-Rest Encryption (Phase 1–9)" subsection under the
Local-First Architecture section. Two-mode trust summary table,
production requirement for MANA_AUTH_KEK with the openssl command,
the "when writing module code" 4-step guide, and a reference
table. New contributors reading the root CLAUDE.md from top to
bottom now hit encryption naturally as part of the data layer
discussion.

.env.macmini.example
--------------------
MANA_AUTH_KEK was missing from the production env example
entirely — the macmini deployment would silently boot on the
32-zero-byte dev fallback if you copied this file. Added with a
multi-paragraph comment covering: how to generate, why it's
required, how to store securely (Docker secrets / KMS / Vault),
and the rotation caveat.

apps/docs/src/content/docs/deployment/self-hosting.mdx
------------------------------------------------------
Two changes:

  1. Added MANA_AUTH_KEK to the mana-auth service block in the
     Compose example with an inline comment pointing at the new
     section below.

  2. New "Encryption Vault Setup" H2 section with subsections:
     - Generating a KEK (with a fake example value labelled DO NOT
       USE — generate your own)
     - Securing the KEK (Docker secrets, KMS, systemd
       LoadCredential, anti-patterns)
     - "What if I lose the KEK?" — explains the data is
       unrecoverable by design and mitigation via zero-knowledge
       mode opt-in
     - KEK rotation — calls out the missing background re-wrap
       job as a known limitation

apps/docs/astro.config.mjs
--------------------------
Added "Security & Encryption" entry to the Architecture sidebar
between Authentication and Backend so the new page is reachable
from the docs nav.

Astro check: 0 errors, 0 warnings, 0 hints across 4 .astro files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:47:59 +02:00

9.9 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

Mana is the unified web application serving 27+ product modules (todo, calendar, contacts, chat, notes, dreams, memoro, cards, picture, presi, music, storage, …) under one SvelteKit app, one IndexedDB, one auth session, and one deployment at mana.how.

  • Web App (apps/web): SvelteKit 2 + Svelte 5 unified app — the main surface
  • Mobile App (apps/mobile): Expo / React Native (work-in-progress, separate codebase)
  • Landing (apps/landing): Astro static landing page deployed to Cloudflare Pages

Architecture

Unified Module System

All modules share one SvelteKit build. Each module lives in apps/web/src/lib/modules/{name}/ and registers itself via a module.config.ts file. The data layer uses a single Dexie IndexedDB (mana) containing all 120+ collections from every module — table names that collide across modules are prefixed (e.g. todoProjects, cardDecks, presiDecks).

Module state lives in three files per module:

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, no reads (those go through queries.ts)

Authentication

Mana Auth is the central authentication service (services/mana-auth/, port 3001), built on Better Auth + Hono + Bun. The web app talks to it via the shared @mana/shared-auth client.

  • Token format: EdDSA JWT with minimal claims (sub, email, role, sid, tier)
  • Session storage: Cookies (*.mana.how domain) + JWT in memory
  • Route protection: (app) group is auth-gated via the AuthGate component in the layout
  • Cross-app SSO: Same Mana Auth session works across all *.mana.how apps
  • Access tiers: guest < public < beta < alpha < founder — apps can require a minimum tier via mana-apps.ts

The legacy Supabase integration was removed. Anything mentioning @supabase/ssr, safeGetSession(), or event.locals.supabase is a leftover from a much earlier iteration and should be deleted on sight.

Data Layer (Local-First)

The app reads and writes to IndexedDB first, then syncs to the server in the background via the mana-sync Go service (port 3050).

Architecture diagram:

User action (e.g. tasksStore.createTask)
        │
        ▼
Module store builds the LocalRecord
        │
        ▼
encryptRecord(tableName, record)        ← Phase 49 encryption layer
        │
        ▼
table.add(encryptedRecord)              ← Dexie write
        │
        ▼
Dexie hooks (database.ts):
  - stamp userId
  - stamp __fieldTimestamps per field
  - record into _pendingChanges
  - 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

For the deep dive — sync engine, retry/backoff, quota recovery, telemetry, RLS, encryption rollout — read apps/web/src/lib/data/DATA_LAYER_AUDIT.md. This is the single most important file for understanding how the app works under the hood.

At-Rest Encryption

User-typed content in 27 tables is encrypted with AES-GCM-256 before it touches IndexedDB. The master key lives in mana-auth (KEK-wrapped) and is fetched on login.

Two trust modes:

Mode Default What Mana can decrypt
Standard Yes The user's master key, via the server-side KEK
Zero-Knowledge Opt-in (Settings → Sicherheit) Nothing — recovery code lives only with the user

When writing module code that touches sensitive fields:

  1. Add the table to apps/web/src/lib/data/crypto/registry.ts with the field allowlist
  2. Wrap writes: await encryptRecord(tableName, record) before table.add() / table.update()
  3. Wrap reads: decryptRecords(tableName, visible) after the Dexie query, before the type converter
  4. The Dexie hook in database.ts does NOT auto-encrypt — every store does it explicitly. This is by design (Web Crypto is async, hooks are sync).

For new sensitive fields, default to encrypt. For new structural fields (IDs, timestamps, enums, sort/filter keys), default to plaintext.

User-facing docs at apps/docs/src/content/docs/architecture/security.mdx.

Routing Structure

apps/web/src/routes/
├── (auth)/              # Public auth pages (login, register, recovery)
├── (app)/               # Auth-gated app surface — 27+ module routes
│   ├── dashboard/       # Customizable widget grid
│   ├── settings/
│   │   └── security/    # Vault status + recovery code + ZK opt-in
│   ├── todo/            # …and many more module routes
│   └── …
├── workbench/           # Multi-pane interface (ListView + DetailView per module)
└── api/                 # SvelteKit API endpoints (rare; most data is local-first)

Path Aliases

Defined in apps/web/svelte.config.js:

  • $libsrc/lib
  • $componentssrc/lib/components
  • $storessrc/lib/stores
  • $utilssrc/lib/utils
  • $typessrc/lib/types
  • $serversrc/lib/server

Development Commands

Web App (apps/web)

cd apps/web

# Development
pnpm dev                # Start dev server on port 5173

# Building
pnpm build              # Build for production
pnpm preview            # Preview production build

# Code Quality
pnpm check              # Type-check with svelte-check
pnpm lint               # Format check + ESLint
pnpm format             # Prettier write

# Testing
pnpm test               # Run Vitest unit tests
pnpm test:ui            # Run Vitest with UI
pnpm test:e2e           # Run Playwright E2E tests

For full local-dev setup (Mana Auth + mana-sync + web together), use the root-level dev:*:full commands. See docs/LOCAL_DEVELOPMENT.md and the root CLAUDE.md.

Mobile App (apps/mobile)

The mobile app is currently lower-priority than the web app and may lag behind in features. Standard Expo commands apply (pnpm start, pnpm ios, pnpm android, EAS builds for production).

Environment Configuration

Generated automatically from the root .env.development via pnpm setup:env. The relevant variables for the web app:

PUBLIC_MANA_AUTH_URL=http://localhost:3001
PUBLIC_MANA_SYNC_URL=http://localhost:3050

The web app does NOT read or store any database credentials directly — all server-side data goes through Mana Auth + mana-sync. See the root CLAUDE.md for the full env-var rundown.

Technology Stack

Web App

  • Framework: SvelteKit 2 + Svelte 5 (runes mode throughout)
  • Styling: TailwindCSS
  • Auth: Mana Auth (Better Auth + EdDSA JWT) via @mana/shared-auth
  • Data: Local-first with Dexie.js + mana-sync (Go) backend
  • Encryption: AES-GCM-256 via Web Crypto, server-wrapped MK with optional zero-knowledge mode
  • Testing: Vitest (unit + integration with fake-indexeddb), Playwright (E2E)
  • Build: Vite

Mobile App

  • Framework: Expo + React Native
  • Routing: Expo Router (file-based)
  • Styling: NativeWind
  • Build: EAS Build

Important Patterns

Module Store Pattern

Every module has a mutation-only store that handles writes + a queries file that handles reads. The store NEVER reads from Dexie via await table.toArray() for UI rendering — that's the queries file's job (via liveQuery hooks). The store only reads when it needs to mutate based on existing state (e.g. toggle, increment).

// modules/todo/stores/tasks.svelte.ts
import { taskTable } from '../collections';
import { encryptRecord } from '$lib/data/crypto';
import { toTask } from '../queries';

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;
  },
  // ...
};
// modules/todo/queries.ts
export function useAllTasks() {
  return useLiveQueryWithDefault(async () => {
    const locals = await db.table<LocalTask>('tasks').orderBy('order').toArray();
    const visible = locals.filter((t) => !t.deletedAt);
    const decrypted = await decryptRecords('tasks', visible);
    return decrypted.map(toTask);
  }, [] as Task[]);
}

Svelte 5 Runes Mode

All routes and components use Svelte 5 runes:

// CORRECT
let count = $state(0);
let doubled = $derived(count * 2);
$effect(() => {
  console.log(count);
});

// WRONG (legacy Svelte 3/4)
let count = 0;
$: doubled = count * 2;

Auth Access

Auth state lives in $lib/stores/auth.svelte.ts. The current user id is also pushed into the data layer's current-user.ts so the Dexie creating-hook can auto-stamp userId on every record. Module stores never need to know who the current user is.

Route Protection

The (app) group is wrapped by an AuthGate component that redirects unauthenticated users to /login and reads the access tier from the JWT to gate beta/alpha/founder-only modules.

Reference Documents

Path Purpose
apps/web/src/lib/data/DATA_LAYER_AUDIT.md Complete data-layer architecture, sync engine, encryption rollout, threat model, backlog
apps/docs/src/content/docs/architecture/security.mdx User-facing security & encryption walkthrough
apps/docs/src/content/docs/architecture/authentication.mdx Auth flow + JWT structure
Root CLAUDE.md Monorepo overview, dev commands, sibling services