managarten/CLAUDE.md
Till JS 8e8b6ac65f fix(mana-auth) + chore: rewrite /api/v1/auth/login JWT mint, remove Matrix stack
This commit bundles two unrelated changes that were swept together by an
accidental `git add -A` in another working session. Documented here so the
history reflects what's actually inside.

═══════════════════════════════════════════════════════════════════════
1. fix(mana-auth): /api/v1/auth/login mints JWT via auth.handler instead
   of api.signInEmail
═══════════════════════════════════════════════════════════════════════

Previous attempt (commit 55cc75e7d) tried to fix the broken JWT mint in
/api/v1/auth/login by switching the cookie name from `mana.session_token`
to `__Secure-mana.session_token` for production. That was necessary but
not sufficient: Better Auth's session cookie value isn't just the raw
session token, it's `<token>.<HMAC>` where the HMAC is derived from the
better-auth secret. Reconstructing the cookie from auth.api.signInEmail's
JSON response only gave us the raw token, so /api/auth/token's
get-session middleware still couldn't validate it and the JWT mint kept
silently failing.

Real fix: do the sign-in via auth.handler (the HTTP path) rather than
auth.api.signInEmail (the SDK path). The handler returns a real fetch
Response with a Set-Cookie header containing the fully signed cookie
envelope. We capture that header verbatim and forward it as the cookie
on the /api/auth/token request, which now passes validation and mints
the JWT correctly.

Verified end-to-end on auth.mana.how:

  $ curl -X POST https://auth.mana.how/api/v1/auth/login \
      -d '{"email":"...","password":"..."}'
  {
    "user": {...},
    "token": "<session token>",
    "accessToken": "eyJhbGciOiJFZERTQSI...",   ← real JWT now
    "refreshToken": "<session token>"
  }

Side benefits:
- Email-not-verified path is now handled by checking
  signInResponse.status === 403 directly, no more catching APIError
  with the comment-noted async-stream footgun.
- X-Forwarded-For is forwarded explicitly so Better Auth's rate limiter
  and our security log see the real client IP.
- The leftover catch block now only handles unexpected exceptions
  (network errors etc); the FORBIDDEN-checking logic in it is dead but
  harmless and left in for defense in depth.

═══════════════════════════════════════════════════════════════════════
2. chore: remove the entire self-hosted Matrix stack (Synapse, Element,
   Manalink, mana-matrix-bot)
═══════════════════════════════════════════════════════════════════════

The Matrix subsystem ran parallel to the main Mana product without any
load-bearing integration: the unified web app never imported matrix-js-sdk,
the chat module uses mana-sync (local-first), and mana-matrix-bot's
plugins duplicated features the unified app already ships natively.
Keeping it alive cost a Synapse + Element + matrix-web + bot container
quartet, three Cloudflare routes, an OIDC provider plugin in mana-auth,
and a steady drip of devlog/dependency churn.

Removed:
- apps/matrix (Manalink web + mobile, ~150 files)
- services/mana-matrix-bot (Go bot with ~20 plugins)
- docker/matrix configs (Synapse + Element)
- synapse/element-web/matrix-web/mana-matrix-bot services in
  docker-compose.macmini.yml
- matrix.mana.how/element.mana.how/link.mana.how Cloudflare tunnel routes
- OIDC provider plugin + matrix-synapse trustedClient + matrixUserLinks
  table from mana-auth (oauth_* schema definitions also removed)
- MatrixService import path in mana-media (importFromMatrix endpoint)
- Matrix notification channel in mana-notify (worker, metrics, config,
  channel_type enum, MatrixOptions handler)
- Matrix entries from shared-branding (mana-apps + app-icons),
  notify-client, the i18n bundle, the observatory map, the credits
  app-label list, the landing footer/apps page, the prometheus + alerts
  + promtail tier mappings, and the matrix-related deploy paths in
  cd-macmini.yml + ci.yml

Devlog/manascore/blueprint entries that mention Matrix are left intact
as historical record. The oauth_* + matrix_user_links Postgres tables
stay on existing prod databases — code can no longer write to them, drop
them in a follow-up migration if you want them gone for real.

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

9.5 KiB

CLAUDE.md

Guidance for Claude Code when working in this repo.

Monorepo Overview

pnpm workspace monorepo with two consolidated tops:

  • apps/mana/apps/web — unified SvelteKit frontend serving 27+ product modules under mana.how. One build, one IndexedDB, one auth session, one deployment.
  • apps/api (@mana/api) — unified Hono/Bun backend API server. Consolidates per-module compute servers; routes registered under /api/v1/{module}/*.

Per-product directories under apps/{product}/ still exist for landing pages, mobile apps, and product-specific packages, but the active web frontend and API both live in the two consolidated apps above.

  • Package Manager: pnpm 9.15.0
  • Build System: Turborepo
  • Node: 20+
  • Primary doc: apps/mana/CLAUDE.md — module structure, data layer, encryption, routing.

Repo layout

apps/
├── mana/            # Unified frontend (SvelteKit web + Expo mobile + Astro landing)
├── api/             # Unified backend API (Hono/Bun) — @mana/api
├── {product}/       # Per-product landing pages, mobile apps, packages
│                    # Standalone (own container, not unified): manavoxel
games/               # arcade, voxelava, whopixels, worldream
services/            # Backend services (Hono/Bun, Go, Python) — see list below
packages/            # Shared workspace packages (@mana/*)
docs/                # Long-form docs (deployment, hardware, postmortems, etc.)
.claude/guidelines/  # Coding conventions — read before changing code

Active services (services/)

mana-auth (3001), mana-sync (3050), mana-credits, mana-user, mana-subscriptions, mana-analytics, mana-search (3021), mana-crawler, mana-api-gateway, mana-notify, mana-media, mana-llm, mana-image-gen, mana-video-gen, mana-stt, mana-tts, mana-voice-bot, mana-events, mana-landing-builder. Each non-trivial service has its own CLAUDE.md.

Coding Guidelines

Always consult before changing code:

Document Purpose
.claude/GUIDELINES.md Overview
.claude/guidelines/code-style.md Formatting, naming, linting
.claude/guidelines/sveltekit-web.md Svelte 5 runes, stores
.claude/guidelines/expo-mobile.md React Native, NativeWind
.claude/guidelines/hono-server.md Hono/Bun servers
.claude/guidelines/database.md Drizzle ORM, pgSchema
.claude/guidelines/authentication.md Mana Auth integration
.claude/guidelines/error-handling.md Result types, error codes
.claude/guidelines/testing.md Vitest, mock factories
.claude/guidelines/design-ux.md UI patterns, a11y

Development Quick Start

See docs/LOCAL_DEVELOPMENT.md for the full setup.

pnpm docker:up           # PostgreSQL, Redis, MinIO
pnpm setup:env           # Generate per-app .env files from .env.development
pnpm setup:db            # Create databases + push schemas

# Start the unified Mana app (most common)
pnpm run mana:dev

# Project-specific full stack (auth + backend + web with auto DB setup)
pnpm dev:chat:full
pnpm dev:todo:full
pnpm dev:picture:full
# … one per project

# Service-only
pnpm dev:auth            # mana-auth (3001)
pnpm dev:sync            # mana-sync Go server (3050)

Quality:

pnpm run build
pnpm run type-check
pnpm run format

Key Architecture Notes

These are the patterns that span the repo. Service-/app-specific details live in their own CLAUDE.md.

Local-first data layer

The unified Mana app uses one IndexedDB (mana) with all 120+ collections. Module stores write directly to Dexie tables; hooks in database.ts track changes into _pendingChanges tagged by appId. The unified sync engine (sync.ts) groups by appId and pushes to mana-sync (Go, port 3050), which persists field-level LWW into PostgreSQL with RLS.

Full architecture, sprint history, threat model:

At-rest encryption

Sensitive user content in 27 tables is AES-GCM-256 encrypted before hitting IndexedDB. Master key lives in mana-auth, KEK-wrapped (MANA_AUTH_KEK env, must be set in prod). Optional zero-knowledge mode via Settings → Sicherheit.

When touching sensitive fields:

  1. Add the table to apps/mana/apps/web/src/lib/data/crypto/registry.ts with the field allowlist
  2. await encryptRecord(tableName, record) before writes
  3. await decryptRecords(tableName, visible) after Dexie reads, before the type converter

Default new user-typed fields to encrypt; default new IDs/timestamps/sort-keys to plaintext.

Authentication

All servers use @mana/shared-hono with authMiddleware(). Tokens are EdDSA JWTs issued by mana-auth with claims {sub, email, role, sid, tier, exp, iss, aud}. Cross-app SSO works across *.mana.how. See .claude/guidelines/authentication.md and services/mana-auth/.

Adding an app to SSO requires updating all three:

  1. trustedOrigins in services/mana-auth/src/auth/better-auth.config.ts
  2. CORS_ORIGINS for mana-auth in docker-compose.macmini.yml
  3. Run pnpm test -- src/auth/sso-config.spec.ts from services/mana-auth/

Access tiers

guest < public < beta < alpha < founder. Apps gate themselves via requiredTier in packages/shared-branding/src/mana-apps.ts; the JWT carries a tier claim; AuthGate enforces it client-side. Admin API at PUT /api/v1/admin/users/:id/tier.

Database (PostgreSQL)

Two databases: mana_platform (all services + app server-side data, schema-isolated via pgSchema) and mana_sync (sync engine, write-heavy). Always use pgSchema('name').table(...), never plain pgTable(). Adding a new schema: see .claude/guidelines/database.md.

Object storage

MinIO (Docker, S3-compatible) in both local and prod. Console: http://localhost:9001 (minioadmin/minioadmin). Use @mana/shared-storage helpers. Pre-configured per-project buckets (picture-storage, chat-storage, cards-storage, …).

Turborepo: avoid recursive turbo calls

CRITICAL: Parent workspace packages (e.g. apps/chat/package.json) must NEVER define type-check, build, or lint scripts that call turbo run <task>. Root turbo already orchestrates those — defining them in children causes infinite recursion (10+ minute hangs, thousands of duplicate tasks). Only dev is OK to delegate to turbo from a parent package, since it's persistent and typically scoped.

Shared Packages (packages/)

Package Purpose
@mana/shared-auth Client-side auth for web/mobile
@mana/shared-hono Hono middleware (auth, health, errors)
@mana/shared-storage S3/MinIO helpers
@mana/shared-branding App registry, tiers, branding
@mana/shared-types Common TS types
@mana/shared-utils Utility functions
@mana/shared-ui React Native UI components
@mana/shared-theme Theme config
@mana/shared-i18n i18n
@mana/local-store Local-first store primitives — used by unified Mana, manavoxel, arcade, and shared-uload/-stores/-links

Adding Dependencies

pnpm add -D <pkg> -w                       # Workspace root (dev tools)
pnpm add <pkg> --filter @mana/web          # A specific app
pnpm add <pkg> --filter @mana/shared-utils # A shared package

Environment Variables

Single source of truth: .env.development (committed). After editing, run pnpm setup:env to regenerate per-app .env files with the right prefixes (EXPO_PUBLIC_* for mobile, PUBLIC_* for SvelteKit, no prefix for Hono/Bun servers). Mapping logic in scripts/generate-env.mjs. Full guide: docs/ENVIRONMENT_VARIABLES.md.

Server Access

  • Production (Mac Mini): ssh mana-server (Cloudflare Tunnel). See docs/MAC_MINI_SERVER.md. Useful: ./scripts/mac-mini/status.sh, ./scripts/mac-mini/deploy.sh, ./scripts/mac-mini/build-app.sh <app>.
  • GPU server (Windows, RTX 3090): ssh mana-gpu (192.168.178.11, LAN only). Hosts STT/TTS/image-gen/video-gen/Ollama. See docs/WINDOWS_GPU_SERVER_SETUP.md.

Reference Docs

Path When you need it
apps/mana/CLAUDE.md Default — module pattern, routing, encryption usage
apps/mana/apps/web/src/lib/data/DATA_LAYER_AUDIT.md Sync engine deep-dive, encryption rollout, threat model
docs/LOCAL_DEVELOPMENT.md First-time setup, dev:*:full commands
docs/ENVIRONMENT_VARIABLES.md All env vars
docs/DATABASE_MIGRATIONS.md Migration workflow + rollback
docs/DEPLOYMENT.md Production deployment
docs/PORT_SCHEMA.md Which service runs on which port
Service-specific CLAUDE.md files Service internals