Bug 1: NotifyUser() early-returned when no WebSocket clients existed,
skipping SSE subscriber notifications entirely. Fixed by restructuring
to check WS clients and SSE subscribers independently.
Bug 2: SSE stream cursor defaulted to client's `since` parameter when
no initial data existed. If `since` was in the future (or very recent),
live updates had created_at < cursor and were silently filtered out.
Fixed by defaulting cursor to now() when no initial data is returned.
Bug 3: NotifyUser used original sseSubs slice instead of sseSubsCopy
after releasing the read lock (race condition).
Verified E2E: Push from client A → SSE stream on client B receives
live change event with correct data within ~1 second.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New endpoint GET /sync/{appId}/stream sends Server-Sent Events with
change data directly, replacing the WebSocket notification + HTTP pull
round-trip pattern.
Server (Go):
- HandleStream() in handler.go: SSE endpoint with initial sync + live streaming
- Hub.Subscribe()/Unsubscribe() in hub.go: channel-based SSE subscriber system
- Notification type for type-safe SSE events
- convertChanges() helper extracted from duplicated code
- WriteTimeout set to 0 for SSE long-lived connections
Protocol: Client connects to /sync/{appId}/stream?collections=a,b&since=...
Server sends initial changes, then streams live changes as other clients sync.
Heartbeat every 30s keeps connection alive. Push still uses POST /sync/{appId}.
WebSocket remains available as fallback (not removed).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server now returns hasMore: true when there are more than 1000 changes
pending for a collection. Client continues pulling in a loop until
hasMore is false, using the last row's timestamp as cursor.
Prevents data loss after long offline periods where >1000 changes
accumulated for a single collection.
Server changes (Go):
- GetChangesSince() accepts limit parameter
- HandlePull() fetches limit+1, trims, sets hasMore
- SyncedUntil uses last row's timestamp when paginating
Client changes (TypeScript):
- Pull loop: while (hasMore) { fetch → apply → advance cursor }
- Cursor only persisted after all pages fetched
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mirrors the frontend unification (single IndexedDB) on the backend.
All services now use pgSchema() for isolation within one shared database,
enabling cross-schema JOINs, simplified ops, and zero DB setup for new apps.
- Migrate 7 services from pgTable() to pgSchema(): mana-user (usr),
mana-media (media), todo, traces, presi, uload, cards
- Update all DATABASE_URLs in .env.development, docker-compose, configs
- Rewrite init-db scripts for 2 databases + 12 schemas
- Rewrite setup-databases.sh for consolidated architecture
- Update shared-drizzle-config default to mana_platform
- Update CLAUDE.md with new database architecture docs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Create packages/shared-python/manacore_auth/ with:
- auth.py: API key validation, rate limiting, local + external auth
- external_auth.py: mana-core-auth remote validation with caching
- create_auth_dependency(scope): factory for per-service auth deps
Migrated services:
- mana-stt: auth.py now wraps shared auth with scope="stt" (272→42 LOC)
- mana-tts: auth.py now wraps shared auth with scope="tts" (272→42 LOC)
The only difference between services was the scope parameter ("stt" vs "tts").
Both external_auth.py files were 100% identical and are now thin re-exports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- shared-auth-stores: delete createSupabaseAuthStore (zero usage across monorepo,
all apps use createManaAuthStore). Remove export + types from index.ts.
- services: move ollama-metrics-proxy (stub — just a Grafana dashboard JSON) and
it-landing (Astro landing page, not a service) to services-archived/
- lint-staged: add services-archived/ to eslint ignore pattern
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add unified /ws endpoint that serves all app notifications over a single connection.
The server now includes appId in the sync-available message payload so the client
knows which app to pull. Legacy /ws/{appId} endpoint remains for backward compatibility.
Backend (Go):
- hub.go: Message struct gains AppId field, NotifyUser sends to all user clients
(unified clients receive everything, legacy clients filtered by appId)
- main.go: new GET /ws route (empty appId = unified mode)
Frontend (sync.ts):
- Single connectUnifiedWs() replaces 27 per-app connectWs() calls
- Parses msg.appId from server to pull only the affected app
- Reconnect/offline logic simplified to one WS
This reduces WebSocket connections from 27 per user to 1, cutting server
connection overhead by ~96%.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- mana-image-gen: change default port from 3025 to 3026 to avoid conflict with mana-llm
- Dashboard widgets (12): replace APP_URLS.{app}.dev/prod with internal route paths (/todo, /calendar, etc.)
and remove target="_blank" since all apps are now internal routes in the unified app
- Home page: use goto() for internal apps, keep window.open() only for external apps (matrix, arcade)
- AppRow: remove unused APP_URLS import
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New GPU service for fast text-to-video generation using LTX-Video (~2B params)
on the RTX 3090. Generates 480p clips in 10-30 seconds, uses ~10GB VRAM.
Includes Cloudflare Tunnel route, Prometheus monitoring, and health checks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
mana-stt: add WhisperX service with CUDA GPU support, speaker diarization, and auto-fallback chain.
mana-notify: add locale fallback and default templates for task reminders.
CD: update deployment pipeline and docker-compose configuration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract ~120 hardcoded German strings from 14 Svelte components into i18n locale
files using svelte-i18n $t() calls. Add new translation sections (taskForm, filters,
tags, subtasks, durationPicker, kanban, toolbar) across all 5 languages (de/en/fr/es/it).
Also add missing shared common translations for Spanish, French, and Italian
(150+ keys each) in packages/shared-i18n.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Delete apps/memoro/apps/backend/ (NestJS) and apps/memoro/apps/audio-backend/
(NestJS) — all functionality has been ported to the new Hono/Bun servers
(apps/server/ and apps/audio-server/).
Also clean up root and memoro package.json scripts to remove references
to the old @memoro/backend and @memoro/audio-backend packages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Accessing (error as any)?.body?.code on a Better Auth APIError triggers an internal
async stream read. When the request body contains special chars like '!', the deferred
JSON parse fails as an unhandled rejection that races with the response, causing 500.
Use only error.status === 'FORBIDDEN' which is a simple string property.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Better Auth uses callbackURL to determine the post-verification redirect target.
Setting only redirectTo left callbackURL=/ which resolved to auth.mana.how/ (404).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without this, Better Auth's definePayload receives a user object
without the custom accessTier column, causing the JWT tier claim
to always default to 'public'.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The client (shared-auth) calls /api/v1/auth/session-to-token for SSO and
2FA flows, but this endpoint was never implemented. Also, the login endpoint
returned raw Better Auth session data instead of the expected
{ accessToken, refreshToken } format.
- Add POST /api/v1/auth/session-to-token endpoint
- Fix login to generate JWT via Better Auth's /api/auth/token
- Fix refresh to return JWT instead of raw session data
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces a tiered access control system so apps can be released
gradually (founder → alpha → beta → public) without extra infrastructure.
Users are gated at the AuthGate level based on their tier vs the app's
requiredTier. All apps remain deployed and reachable, but only users
with sufficient tier can enter.
- Add accessTier enum + column to users schema (default: 'public')
- Add tier claim to JWT payload in better-auth config
- Add requiredTier field to ManaApp interface + all 25 apps
- Add hasAppAccess(), getAccessibleManaApps(), ACCESS_TIER_LABELS
- Update AuthGate with tier check + access denied screen
- Update getPillAppItems + Home page to filter by user tier
- Update all 22 app layouts to pass user tier to PillNav
- Add admin API: GET/PUT /api/v1/admin/users/:id/tier
- Document access tier system in CLAUDE.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- mana-media: strip workspace devDep before bun install (shared-drizzle-config
is only needed for drizzle-kit, not at runtime)
- inventar-web: replace nested <button> with <div role="button"> to fix
Svelte 5 HTML validation error during build
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sync integration:
- Redirect service reads links from mana-sync's sync_changes table
- Analytics service queries clicks from sync_changes
- Click tracking writes to sync_changes (visible to all clients)
- Public profile reads from sync_changes
- Server DB points to mana_sync database (not separate uload DB)
- Removed uload-database dependency from server
Stripe:
- Real Stripe checkout session creation (monthly/yearly)
- Webhook handler with signature verification
- Webhook route bypasses JWT auth
Documentation:
- Root CLAUDE.md: added uload to project table, dev commands, local-first list
- mana-sync CLAUDE.md: added uLoad, Taktik, Calc to connected apps
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- docker-compose.macmini.yml: calc-web service on port 5026
- cloudflared-config.yml: calc.mana.how → localhost:5026
- mana-auth trusted origins: add https://calc.mana.how
- mana-credits & mana-user CORS: add https://calc.mana.how
- Dockerfile port updated from 5018 to 5026 (5018 used by zitare)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Route all AI workloads (Ollama, STT, TTS, Image Gen) to GPU server
(192.168.178.11) via LAN instead of host.docker.internal
- Upgrade default model to gemma3:12b and max concurrent to 5
- Add daily signup limit service (MAX_DAILY_SIGNUPS env var)
- Add GET /api/v1/auth/signup-status public endpoint
- Add k6 load test suite (web-apps, auth, sync-websocket, ollama)
- Add capacity planning documentation
- Fix: add eslint-config to sveltekit-base and calendar Dockerfiles
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace NestJS framework with Hono + Bun, eliminating the last
NestJS service from the stack. All business logic preserved:
- CAS upload with SHA-256 dedup
- BullMQ image processing (Sharp thumbnails/variants)
- Matrix MXC URL import
- EXIF extraction
- File streaming/transforms
- Prometheus metrics
23 NestJS files → 12 Hono files. Zero NestJS in the monorepo.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bash-based integration test that verifies the full sync cycle:
1. Health check
2. Client A pushes insert
3. Client B pulls and sees the change
4. Client B pushes update (field-level)
5. Client A pulls and sees the update
6. Client A pushes delete
7. Unauthorized request rejected (401)
Requires running mana-sync + mana-auth. Run with:
./services/mana-sync/test/e2e-sync-flow.sh [TOKEN]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All 4 Go services using shared-go now:
1. COPY packages/shared-go/ to /shared-go/ in builder
2. go mod edit -replace before go mod download (dep caching)
3. go mod edit -replace before go build (source rebuild)
Docker builds verified locally for mana-search and mana-notify.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- mana-crawler: config → envutil, handler → httputil.WriteJSON
- mana-api-gateway: config → envutil, handlers → httputil.WriteJSON
- Fix Dockerfile COPY paths (remove stale -go suffix in all 4 services)
- All services now use packages/shared-go via replace directive
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add lightweight Hono + Bun servers for server-only compute endpoints.
CRUD is handled by mana-sync, these handle AI + file upload only.
Traces: AI guide generation, location sync (Port 3026)
Planta: Photo upload (S3), AI plant analysis (Port 3022)
NutriPhi: AI meal analysis (photo+text), recommendations (Port 3023)
Each uses @manacore/shared-hono for auth/health/errors. ~100-200 LOC.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove the entire NestJS-based mana-core-auth service (~36,000 lines
including tests, config, and package files). The new mana-auth service
(Hono + Bun, ~1,900 LOC) is the complete replacement on the same port.
Deleted:
- services/mana-core-auth/ — 169 files, 36,123 lines
(NestJS 10, Express, class-validator, all NestJS infrastructure)
Updated:
- docker-compose.macmini.yml: mana-auth now builds from services/mana-auth
with Bun healthcheck, simplified env vars (no Redis, no DuckDB needed)
- CLAUDE.md: mana-core-auth → mana-auth in services list
- Overview plan: marked Phase 4+5 as DONE, updated next steps
The ManaCore auth ecosystem is now:
- mana-auth (3001) — Auth, JWT, SSO, OIDC, Guilds, API Keys, GDPR
- mana-credits (3061) — Credits, Gifts, Guild Pools, Stripe
- mana-user (3062) — Settings, Tags, Storage
- mana-subscriptions (3063) — Plans, Billing, Invoices
- mana-analytics (3064) — Feedback, Voting
Total: ~6,600 LOC across 5 Hono+Bun services
Replaces: ~20,000 LOC in 1 NestJS service (67% reduction)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Complete the mana-auth Hono service with all remaining endpoints
from mana-core-auth.
Added:
- routes/auth.ts: Full auth flow (register, login, logout, validate,
password reset, profile, change-password, account deletion,
security events) with lockout + security event logging
- routes/guilds.ts: Guild CRUD, member management, invitations
(delegates to Better Auth org plugin + mana-credits for pools)
- routes/api-keys.ts: API key generation, listing, revocation,
validation (sk_live_* format, SHA-256 hashed)
- routes/me.ts: GDPR data export/delete (Articles 17 & 20)
- services/security.ts: SecurityEventsService (fire-and-forget audit)
+ AccountLockoutService (5 failures/15min → 30min lockout)
- services/api-keys.ts: Key generation, validation, scope checks
Updated:
- index.ts: Wire all routes with proper middleware (JWT, service auth)
Service now has ~1,900 LOC covering all functionality from the
original ~11,500 LOC NestJS mana-core-auth (83% reduction).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewrite the central authentication service from NestJS to Hono + Bun.
Uses Better Auth's native fetch-based handler — no Express conversion.
Key architecture changes:
- Better Auth handler mounted directly on Hono (app.all('/api/auth/*'))
- No NestJS DI, modules, guards, decorators — plain TypeScript
- JWT validation via jose (same as extracted services)
- Email via nodemailer (simplified, German templates)
- ~1,400 LOC vs ~11,500 LOC in NestJS (88% reduction)
Service structure:
- auth/better-auth.config.ts — copied from mana-core-auth (framework-agnostic)
- auth/stores.ts — in-memory stores for email redirect URLs
- email/send.ts — nodemailer email functions
- middleware/ — JWT auth, service auth, error handler (shared pattern)
- db/schema/ — copied from mana-core-auth (Drizzle, framework-agnostic)
Port: 3001 (same as mana-core-auth — drop-in replacement)
Database: mana_auth (same DB, same schemas)
Better Auth plugins: Organization, JWT (EdDSA), OIDC Provider,
Two-Factor (TOTP), Magic Link
Note: This is the initial version. Guilds, API keys, Me (GDPR),
security (lockout/audit), and admin endpoints will be added
incrementally. The old mana-core-auth remains until fully replaced.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract feedback, analytics, and AI modules from mana-core-auth into
standalone mana-analytics service (Hono + Bun, Port 3064).
New service (services/mana-analytics/):
- User feedback CRUD with voting
- AI-powered feedback title generation via mana-llm
- Simplified from DuckDB analytics to pure PostgreSQL
- ~550 LOC
Removed from mana-core-auth:
- feedback/ module (6 files)
- analytics/ module (4 files)
- ai/ module (3 files)
- db/schema/feedback.schema.ts
mana-core-auth now contains ONLY pure auth:
- Better Auth (JWT, Sessions, 2FA, Passkeys, OIDC, Magic Links)
- Organizations/Guilds (membership management)
- API Keys, Security, Me (GDPR), Health, Metrics
- Ready for Phase 5: Hono rewrite
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract subscription billing into standalone mana-subscriptions service
(Hono + Bun, Port 3063). Also removes Stripe module from mana-core-auth
since subscription webhooks are the last consumer.
New service (services/mana-subscriptions/):
- Plans CRUD, subscription management, Stripe Checkout + Portal
- Invoice tracking, webhook handlers for sub/invoice events
- Internal API for plan limits (consumed by guilds service)
- ~990 LOC (vs ~1,700 in NestJS incl. Stripe module)
Removed from mana-core-auth:
- subscriptions/ module (6 files)
- stripe/ module (4 files) — no longer needed in auth
- db/schema/subscriptions.schema.ts
- guilds.service.ts: replaced direct DB plan limit query with
HTTP call to mana-subscriptions internal API
mana-core-auth now contains only:
- Auth (Better Auth, JWT, Sessions, 2FA, Passkeys, OIDC)
- Organizations/Guilds (membership only, no credits/plans)
- API Keys, Security, Me (GDPR), Health, Metrics
- Feedback + Analytics (next extraction target)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- mana-sync on port 3051 (Go sync server for local-first apps)
- mana-notify-go on port 3040 (Go notification service)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Required for Docker Desktop Mac (TLS proxy) and sites with
self-signed/expired certificates. Crawlers routinely need this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>