--- id: TASK-15 title: 'EncryptID personal subdomains: .r*.online with local-first data' status: Done assignee: [] created_date: '2026-02-25 03:01' updated_date: '2026-02-25 04:48' labels: - architecture - auth - encryptid - infrastructure - cross-app dependencies: [] priority: high --- ## Description When a user logs in via EncryptID, they should operate out of `.r*.online` (e.g., `alice.rnotes.online`, `alice.rspace.online`), with all their data saved securely to their local space. ## Scope This is a cross-cutting feature affecting all rStack apps. Key areas: ### 1. DNS & Routing - Wildcard DNS for each `r*.online` domain (`*.rnotes.online`, `*.rspace.online`, etc.) - Cloudflare wildcard CNAME records pointing to the tunnel - Traefik wildcard Host rules to route `*.r*.online` to the correct app container ### 2. Auth / EncryptID Integration - On login, redirect to `.r*.online` - Middleware to extract subdomain, validate it matches the authenticated EncryptID - Reject requests where subdomain doesn't match session identity ### 3. Data Isolation & Local-First Storage - Each user's data is scoped to their EncryptID - Explore local-first / CRDTs (e.g., Yjs, Automerge) for offline-capable storage - Sync strategy: local device ↔ user's personal encrypted space on server - Encryption at rest using keys derived from EncryptID ### 4. Multi-App Consistency - Shared auth session across `*.r*.online` subdomains (cross-subdomain cookies or token-based) - AppSwitcher links should resolve to `.rspace.online`, `.rnotes.online`, etc. when logged in - Consistent UX: user always sees their subdomain in the URL bar ### 5. Migration - Existing data needs a migration path to the new per-user scoped storage - Backward compat for users accessing the root domain (redirect to subdomain after login) ## Open Questions - What is the EncryptID identifier format? (alphanumeric, length constraints, case sensitivity) - Should unauthenticated users see public content at the root domain? - How does this interact with rSpace's existing space/room model? - Storage backend: SQLite per user? Postgres row-level security? Encrypted blob store? ## Acceptance Criteria - [ ] #1 Wildcard DNS configured for all r*.online domains - [ ] #2 Middleware extracts and validates EncryptID from subdomain - [ ] #3 User data is scoped and encrypted per EncryptID - [ ] #4 AppSwitcher links resolve to user's personal subdomain when logged in - [ ] #5 Cross-subdomain auth session works across rStack apps - [ ] #6 Migration path defined for existing data - [ ] #7 Local-first storage strategy documented and prototyped ## Implementation Notes Research complete - EncryptID uses DID format (did:key:z, 50+ chars). DIDs too long for DNS labels (63 char limit). Username-based subdomains recommended but usernames are currently optional. ## Final Summary Implemented username-based personal subdomains (`.rnotes.online`) across 8 phases: **Phase 1 - Infrastructure**: DNS wildcard and Traefik routing already configured. Cloudflared needs manual `*.rnotes.online` entry. **Phase 2 - EncryptID**: Server already enforces username UNIQUE NOT NULL + includes in JWT. SDK updated: made `username` required in `EncryptIDClaims` type, updated `createSession()`. **Phase 3 - Schema**: Added `workspaceSlug` field to Notebook model with index + migration SQL. **Phase 4 - Middleware**: Sets `x-workspace-slug` header from subdomain extraction. New `workspace.ts` helper. **Phase 5 - API Filtering**: All GET endpoints filter by workspace on subdomains (notebooks, notes, search, notebook detail, notebook notes). **Phase 6 - AppSwitcher**: Fetches `/api/me` for username, generates `.r*.online` links when logged in. **Phase 7 - Sessions**: `SubdomainSession` component syncs localStorage ↔ `.rnotes.online` domain-wide cookie. `authFetch` falls back to domain cookie. **Phase 8 - Migration**: Auto-assigns unscoped notebooks to user's workspace on auth. **Manual steps remaining**: Remove stale container on netcup, run migration, add cloudflared wildcard entry.