Zum Inhalt springen

Architektur

Orimora ist ein Full-Stack-Monolith auf Basis von SvelteKit. Es gibt keine Microservices: SvelteKit-Server-Routen sind die API, derselbe Node.js-Prozess dient dem Frontend, verarbeitet WebSocket-Verbindungen für kollaboratives Editieren und führt Hintergrundjobs aus.

flowchart TD
    Browser["Browser-Client\nSvelteKit SSR · Svelte 5 Runes\nTipTap · Yjs · Hocuspocus"]

    subgraph Node["Node.js-Anwendung (Port 3000)"]
        SK["SvelteKit\nHTTP-Routen / API"]
        HC["Hocuspocus\nWebSocket /collab"]
        BQ["BullMQ Worker\n(Hintergrundjobs)"]
    end

    Browser -- "HTTP / SSR" --> SK
    Browser -- "WebSocket" --> HC

    SK -- "Drizzle ORM" --> PG[("PostgreSQL 16\npg_trgm · Volltextsuche")]
    HC -- "Yjs-Zustand" --> PG
    BQ -- "Jobs" --> PG

    SK -- "Sessions · Cache · Rate-Limit" --> RD[("Redis 7")]
    BQ -- "Queue" --> RD

    SK -- "S3 SDK (optional)" --> S3[("S3 / Object Storage\nUploads · Exports")]
    BQ -- "SMTP" --> Mail["Mailserver\n(SMTP)"]
    BQ -- "HTTP POST" --> Hooks["Ausgehende Webhooks\n& Publishing-Kanäle"]

Request-Lifecycle (vereinfacht):

  1. Browser sendet HTTP-Request → Traefik (TLS-Terminierung) → Node.js
  2. SvelteKit-Route authentifiziert Session (Redis) und prüft Berechtigungen
  3. Service fragt Drizzle ORM → PostgreSQL ab
  4. Antwort als JSON serialisiert (API-Routen) oder als HTML gerendert (Seiten-Routen)
  5. Nebeneffekte (Webhooks, Benachrichtigungen, Backlinks) werden per BullMQ asynchron ausgelöst
src/
├── lib/
│ ├── components/ # Svelte-UI-Komponenten
│ │ └── editor/ # TipTap-Editor + Extensions
│ ├── server/ # Nur Server-Code
│ │ ├── db/ # Drizzle-Schema + Migrationen
│ │ ├── services/ # Business-Logik
│ │ └── policies/ # Autorisierungsprüfungen
│ └── utils/ # Geteilte Hilfsfunktionen
├── routes/
│ ├── (app)/ # Authentifizierte App-Routen
│ ├── (auth)/ # Login / Registrierung
│ └── api/ # API-Endpunkte (RPC + /v1/ REST)
└── hooks.server.ts # Auth, Rate-Limit, Security-Header
  1. Request trifft in hooks.server.ts ein — Rate-Limit, Session wird geladen
  2. Route-Handler ruft eine Service-Funktion auf (z. B. getDocument, createDocument)
  3. Service fragt Drizzle ORM → PostgreSQL ab
  4. Antwort als JSON (API) oder HTML (Seiten) serialisiert
  5. Hintergrundeffekte (Webhooks, Benachrichtigungen, Backlinks) über BullMQ

Echtzeit-Kollaboration nutzt Yjs (CRDT) mit Hocuspocus als Server:

  • Jedes Dokument hat einen eindeutigen Raum (UUID)
  • Der Yjs-Zustand wird über @hocuspocus/extension-database in PostgreSQL persistiert
  • Beim Verbinden lädt der Server den aktuellen Inhalt und hydratisiert den Yjs-Zustand
  • Beim Trennen wird der finale Yjs-Zustand in die documents-Tabelle geschrieben
  • Sessions: HTTP-only, Secure, SameSite=Lax Cookies mit zufälligen Tokens
  • CSRF: Mutierende API-Requests aus dem Browser erfordern Session-Auth
  • API-Keys: Präfix kb_, verschlüsselt at rest, scope-begrenzt
  • CSP: Nonce-basierte Policy über SvelteKit
  • CORS: Nur auf /api/v1/* mit Authorization-Header-Unterstützung