Skip to content

chore: dependency upgrades, Better Auth migration, and performance improvements#98

Closed
elasticjava wants to merge 27 commits into
juanfran:mainfrom
elasticjava:main
Closed

chore: dependency upgrades, Better Auth migration, and performance improvements#98
elasticjava wants to merge 27 commits into
juanfran:mainfrom
elasticjava:main

Conversation

@elasticjava

Copy link
Copy Markdown

Summary

This PR brings the dependency stack up to date, replaces the deprecated Lucia auth library with Better Auth, and includes targeted performance improvements for high-concurrency collaborative sessions.


Changes

Security & Stability Fixes (fix(api))

  • CORS hardening: replaced origin: true with origin: process.env['FRONTEND_URL'] — only the configured frontend origin is allowed for tRPC REST endpoints (Socket.IO already had its own restricted CORS)
  • Cookie security: added secure and sameSite: 'lax' to OAuth state/code_verifier cookies in production
  • Memory leak: emptyBoard() now also clears the boardSettings cache to prevent stale read-only settings persisting after a board closes
  • Immutability: userJoin() uses spread instead of in-place array mutation
  • Debug artifact: removed commented-out setTimeout(() => socket.conn.close()) from client.ts

TipTap 3.0.0-beta.163.20.1 (chore(deps))

All 20 @tiptap/* packages updated from beta to stable 3.x.

API fixes:

  • Removed tippyOptions: {} from custom BubbleMenu.addOptions() — the project uses @floating-ui/dom instead of tippy, so this field is not part of the type
  • setContent now passes { emitUpdate: false } matching the new SetContentOptions type (prevents potential update loops)
  • Added @tiptap/suggestion@^3.20.1 as explicit dependency (required peer dep for extension-mention@3.x)
  • Fixed server.connection() cookie type: Record<string, string | undefined> matching cookie@1.x parse return type

Drizzle 0.28.60.45.1 + drizzle-kit 0.19.130.31.9 (chore(deps))

  • Updated drizzle.config.ts: replaced deprecated driver: 'pg' + breakpoints with defineConfig() + dialect: 'postgresql'
  • Ran drizzle-kit up to upgrade migration snapshot format
  • No query-level breaking changes in the codebase

Auth: Lucia v3 → Better Auth 1.5 (feat(auth))

Lucia v3 was officially deprecated in March 2025. This PR replaces it with Better Auth (MIT).

Design: Better Auth is configured with its Drizzle adapter mapped to the existing database tables (accounts as user table, account_session as session table), preserving all foreign key relationships to boards, teams, and invitations without breaking changes.

Schema additions (migration 0008):

  • accounts: email_verified, created_at, updated_at
  • account_session: token (unique), created_at, updated_at, ip_address, user_agent
  • New table oauth_accounts: stores OAuth provider account links (replaces accounts.google_id)
  • New table verification: for future email OTP / magic link flows

Migration safety:

  • Existing Lucia sessions are cleared (users re-authenticate once after upgrade)
  • Existing Google OAuth links are migrated from accounts.google_idoauth_accounts
  • Account linking enabled for smooth Google re-auth of existing users

Other changes:

  • arctic and @lucia-auth/adapter-postgresql removed
  • OAuth callback (/api/auth/callback/google) is now handled automatically by Better Auth via a Fastify wildcard route (/api/auth/*) with a scoped content-type parser
  • Session validation moved to auth.api.getSession() with 5-minute cookie cache — eliminates per-request DB session lookups in tRPC context
  • lucia.invalidateUserSessionsgetAuth().api.revokeUserSessions in logout/account deletion

Breaking change for existing deployments: all active sessions are invalidated by migration 0008. Users must sign in again after applying the migration.


Performance: high-concurrency boards (perf(api))

Targeted optimizations for scenarios with many simultaneous users on a single board:

Fix Impact
DB connection pool postgres({ max: 20, idle_timeout: 30 }) Handles concurrent DB queries without queuing
Validator Map (O(1) lookup) Replaces O(n) Array.find() per incoming board message
Socket.IO ping tuning pingTimeout: 15s / pingInterval: 10s Faster dead-connection detection with manageable overhead
Socket.IO maxHttpBufferSize: 5MB Supports large board state transfers
close() optimization findIndex + targeted splice instead of full map() scan over all board nodes

Note: cursor moves already bypass full validation via the existing broadcast message type — no change needed there.


Test Plan

  • All existing unit tests pass (sync-node-box, web vitest suites)
  • Both api and web production builds succeed with no TypeScript errors
  • Lint clean across all 7 Nx projects (warnings are pre-existing)

For manual verification after deploying migration 0008:

  1. Sign in with Google — should work seamlessly (Better Auth creates session)
  2. Open a board with multiple users — real-time collaboration (cursor, actions) should function as before
  3. Logout → re-login — session invalidation should work
  4. Board admin read-only toggle → reconnect — should not show stale permissions

- CORS: restrict allowed origin to FRONTEND_URL instead of wildcard
- auth: add secure and sameSite=lax flags to OAuth state/code_verifier cookies
- server: clear boardSettings cache in emptyBoard to prevent stale data
- server: replace direct state mutation in userJoin with immutable pattern
- client: remove leftover debug code
- schema: add comment explaining dual roleEnum declaration
- deps: update Angular ecosystem to 21.2.x, Nx to 22.5.4, and minor packages
- chore: add .mcp.json to gitignore
- All @tiptap/* packages updated from 3.0.0-beta.16 to 3.20.1
- Added @tiptap/suggestion@3.20.1 (required peer dep for extension-mention)
- Remove tippyOptions from custom BubbleMenu addOptions (not part of API)
- Fix setContent call: pass { emitUpdate: false } matching new SetContentOptions type
- Fix server.connection() cookies type: Record<string, string | undefined>
  (cookie@1.x parse() now returns string | undefined for missing keys)
… → 0.31.9

- drizzle.config.ts: replace driver/breakpoints with defineConfig + dialect
  (driver: 'pg' and breakpoints removed in drizzle-kit 0.20+)
- Replace lucia, @lucia-auth/adapter-postgresql, arctic with better-auth
- Configure Better Auth with Drizzle adapter mapped to existing DB tables
  (accounts as user table, account_session as session table)
- Add Better Auth required columns: emailVerified, createdAt, updatedAt
  to accounts; token, timestamps, ipAddress, userAgent to account_session
- Add oauth_accounts table for OAuth provider account linking
- Add verification table for future email/OTP flows
- Enable account linking for smooth Google OAuth migration
- Replace Lucia session validation with Better Auth getSession API
- Replace OAuth callback handler with Better Auth wildcard route
  (/api/auth/* mounted in Fastify with scoped content-type parser)
- Replace lucia.invalidateUserSessions with getAuth().api.revokeUserSessions
- Remove parse() from init-server.ts: socket cookie header passed directly
- Migration 0008: clears old Lucia sessions, migrates Google links to oauth_accounts

Breaking change: existing sessions invalidated; users must re-authenticate.
- DB connection pool: postgres max=20, idle_timeout=30s, connect_timeout=10s
- Validator Map: replace O(n) array.find() with O(1) Map.get() per message
- Socket.IO: pingTimeout=15s/pingInterval=10s for faster dead conn detection
- Socket.IO: maxHttpBufferSize=5MB for large board state transfers
- client close(): findIndex short-circuit instead of full map() scan
- StickyNotePadComponent: visueller zettelstapel in der toolbar —
  klick aktiviert platziermodus, drag-to-drop erstellt zettel direkt
  an der abwurfposition (miro-tear-off-feeling)
- tastenkürzel N zum aktivieren des notizmodus hinzugefügt
- ghost-note-vorschau während des ziehens (position:fixed, rotiert)
- drei DDD-templates: event-storming, bounded-context-canvas,
  aggregate-design-canvas (nick tune / virtual DDD)
- cypress E2E-projekt (apps/web-e2e) mit 5 test-suiten:
  note-creation, templates, toolbar, collaboration, ddd-workflows
…board-toolbar

- DOM-Kopplung entfernt: board-boundary-check aus StickyNotePadComponent in
  BoardToolbarComponent verschoben (Trennung der Zuständigkeiten)
- NgZone.runOutsideAngular für mousemove-tracking, re-entry nur für Signal-Updates
- DOCUMENT-Token statt direktem document-Zugriff (SSR-kompatibel)
- totes AppModule-Export-Artefakt entfernt
- togglePinned: !this.pinned() statt ternary
- #darken: unterstützt nun auch 3-stellige Hex-Codes (#abc)
- text-color bug: contrastColor() statt hardcoded #000 (wcag-kontrast)
- focus-indikatoren: :focus-visible mit outline (wcag 2.1 aa)
- links: text-decoration underline (wcag 1.4.1)
- ws-disconnect: reconnect-banner statt auto-redirect zu /
- touch-support: touchstart/move/end in board-move-service
- pinch-zoom: 2-finger-zoom im board-zoom-service
- e2e: 6 spec-dateien, 45 tests, alle gruen
- 5-phasen-plan: canvas-renderer, yjs-crdt, rust-backend, ux-exzellenz, enterprise
- architektur-ziel: canvas 2d + r-tree + yjs + rust/axum + flatbuffers
- wettbewerbsanalyse: miro, figjam, excalidraw, tldraw
- performance-kpis: 60fps bei 500 nodes, 50+ concurrent users, <20ms latenz
…beitet

- Yjs CRDT vor Canvas Renderer priorisiert (Datenverlust = kritischster Bug)
- Canvas Full → Canvas Hybrid (DOM-Overlay fuer TipTap, wie tldraw)
- Rust Backend gestrichen (Node.js + y-websocket reicht fuer 50-100 User)
- FlatBuffers gestrichen (Yjs y-protocols reicht)
- Phase 0 eingefuehrt (Baseline + Quick Wins: DOM-Viewport-Culling, Dark Mode)
- Feature-Flag Cutover statt Dual-Write Migration
- Realistische Ziele (50-100 statt 500+ concurrent Users)
- Fehlende Aspekte ergaenzt (S3, GDPR, WS Rate Limiting, Bundle Size)
- User-Feedback-Checkpoints nach jeder Phase
- NodesComponent filtert Nodes nach Viewport-Sichtbarkeit
- Board-Koordinaten werden aus zoom/position berechnet
- 200px Buffer verhindert Pop-in am Viewport-Rand
- Fokussierte Nodes werden immer gerendert (kein Verschwinden bei Drag)
- Nodes ohne width/height bekommen Default 300px
- Keine externen Dependencies (kein rbush noetig bei <1000 Nodes)
- E2E: zoomToFit() Command fuer Template-Tests hinzugefuegt
- Alle 45 E2E-Tests gruen
- CSS Custom Properties für Dark Mode via prefers-color-scheme und data-theme
- Semantische Variablen: --board-bg, --board-dot, --toolbar-bg, --toolbar-border, --dialog-bg
- Hardcoded Farben durch CSS-Variablen ersetzt (Toolbar, Panel, Text, Header)
- Invertierte Graustufen für Dark Mode (--grey-10 bis --grey-90)
- Performance-Monitor: FPS, DOM-Nodes, Memory (debug.perf(), ?perf=1)
- throttleTime(2000) → debounceTime(2000): erst nach 2s Ruhe persistieren
- Ephemere User-Daten (cursor, connected) vor Persist filtern
- distinctUntilChanged mit JSON-Vergleich: nur bei echten Änderungen schreiben
- emptyBoard: ebenfalls User-Nodes vor letztem Persist entfernen
…oration

- YjsBoardService: Angular-Service mit Y.Doc, WebsocketProvider, IndexedDB-Offline
- y-websocket Server: Fastify WS-Upgrade auf /yjs/:boardId mit Auth + Access-Check
- DB-Migration: board_yjs BYTEA + use_yjs Boolean fuer Feature-Flag Cutover
- Automatische JSON→Yjs Migration beim ersten Board-Zugriff
- Awareness-Protocol fuer Live-Cursors vorbereitet
- 3s Debounce-Persist nach Yjs-Updates
… zoom

3 CSS-Stufen: LOD 0 (>50% Zoom) volle Details, LOD 1 (20-50%) kein Text,
LOD 2 (<20%) nur farbige Rechtecke. content-visibility: auto fuer
browser-natives rendering-culling.
…sserungen

- ? oeffnet keyboard-shortcuts-hilfe-dialog mit allen shortcuts
- png-export-button im header (canvas-basiert, 2x aufloesung)
- node-enter-animation (scale-in) fuer visuelles feedback
- prefers-reduced-motion respektierung global
- responsive toolbar-positionierung fuer mobile (768px breakpoint)
- websocket rate-limiting (60 msg/s pro verbindung) im yjs-server
- /health endpoint fuer container-monitoring
- security-header (x-content-type-options, x-frame-options, referrer-policy)
- docker-compose health-checks fuer db, api mit ordentlichem depends_on
…ache

- Protocol Buffers Schema (board, node, viewport, common) mit buf v2
- Connect-Fastify Integration neben bestehendem tRPC
- Auth-Interceptor mit Cookie-basierter Session-Validierung
- Huffman-Tree-inspirierte Viewport-Klassifizierung (Hot/Warm/Cold)
- Angular Client: Transport, ViewportSync mit Debounce/AbortController
- OPFS-primärer KV-Store mit IndexedDB-Fallback
- @tapiz/proto Pfad-Alias für saubere Imports
…-härtung

- XSS: DOMPurify für innerHTML in NoteHeightCalculator und SafeHtmlPipe
- Validierung: URL-Protokoll-Einschränkung (http/https), Array-Limits für votes/emojis/drawing
- Auth: BETTER_AUTH_SECRET Startup-Validierung
- Security-Headers: CSP, HSTS, Permissions-Policy
- DB: Pool von 20→50 mit max_lifetime, Transactions für createBoard/deleteBoard
- Yjs: LRU-Cache (200 Docs, 10min TTL), Heartbeat für tote WebSocket-Verbindungen
- N+1 Query: getBoardAdmins optimiert (direkte DB-Abfrage statt team-db Hilfsfunktion)
- tRPC: v11-Kompatibilität (getRawInput, Client-Upgrade)
- Docker: Postgres auf 127.0.0.1 gebunden
…ests

- applyAction: O(1) Patch statt O(n) map() — nur geändertes Node wird kopiert
- syncNodeBox: redundanten Array-Spread bei setState entfernt (doppelte Kopie)
- Performance-Test: 100 Patches auf 500 Nodes in <50ms, Referenz-Identität geprüft
- DB-Migration 0009: Indizes für accounts_boards, starreds, team_members, boards, notifications
- Validierungs-Tests: 16 Tests für note.validator (Array-Limits) und image.validator (URL-Protokoll)
- Test-Infrastruktur: vitest für board-commons Library aufgesetzt
- 7 DB-Tests: createBoard/deleteBoard Transactions, getBoardAdmins, haveAccess
- 7 viewport-sync Tests: Hot/Warm/Cold-Klassifizierung, Zoom, Performance (500 Nodes <10ms)
- Gesamtzahl Tests: 19 sync-node-box + 16 Validierung + 14 API = 49 neue Tests
Axum + Tokio + Yrs (Yjs-CRDT) + DashMap für lock-free Concurrency.
- Auth: Session-Validierung direkt gegen Better Auth DB
- Yjs: RoomManager mit LRU-Cache (200 Docs, 10min TTL), periodischer Persistenz (30s)
- Presence: Cursor/Viewport-Broadcasting mit Heartbeat-Cleanup
- Viewport: Spatial-Klassifizierung (hot/warm/cold) für Node-Culling
- WebSocket: Ping/Pong Heartbeat (30s), Broadcast via tokio::sync::broadcast
- Release-Binary: 4.7MB (LTO, strip, codegen-units=1)
- R*-Tree (rstar): O(log n) viewport-queries statt O(n) brute-force
  - SpatialManager pro Board, hot/warm/cold node-klassifizierung
  - Bulk-load 10k Nodes <100ms, Query <5ms (Performance-Test)
- vectis-crdt: domain-spezifisches CRDT für Whiteboard-Strokes
  - Deterministische Z-Order via RGA/YATA-Sequenz
  - LWW-Register pro Property (Color, Width, Opacity, Transform)
  - Awareness-Protokoll mit 15s Cursor-TTL
  - Snapshot-Persistenz und Cross-Replica-Konvergenz
- 13 Unit-Tests (7 Spatial + 6 CRDT)
- SpatialIndexService: rbush-basierter R-tree für O(log n) Queries
  - bulkLoad bei Node-State-Änderungen, queryViewport bei Zoom/Pan
  - Ersetzt O(n) Array-Filter in NodesComponent
- Viewport-Query REST-Endpoint im Rust-Backend
  - POST /api/board/{id}/viewport-query → hot/warm Node-IDs
- Angular effect() hält Index synchron mit Board-State
…ion limits

- Graceful Shutdown: SIGTERM/SIGINT persistiert alle dirty Rooms vor dem Exit
- Rate Limiting: Token-Bucket pro Connection (120 Yjs-Ops/s, 30 Cursor/s)
- Connection Limits: MAX_CONNECTIONS_PER_BOARD (default 100), 503 bei Überschreitung
- Metrics: Prometheus-kompatibel /metrics (active_connections, active_rooms)
- Message Size Limits: 512KB Yjs, 64KB Presence
- Connection Counter: AtomicU64 für globales Connection-Tracking
- 15 Unit-Tests (2 neue Rate-Limit-Tests)
@juanfran

juanfran commented Apr 1, 2026

Copy link
Copy Markdown
Owner

Hi, and thanks for your interest in Tapiz.

At the moment, I can’t accept this PR in its current form.

If you want to propose new features for Tapiz, please open a separate issue for each feature first and wait for approval, so I can decide whether it is appropriate for the project. Once a feature/fix has been discussed and approved, you can then open a PR for it.

Also, PRs should be limited to a single feature only, and all commits and comments must be written in English.

Thanks for understanding.

@juanfran juanfran closed this Apr 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants