Skip to content

Architecture

Orimora is a full-stack monolith powered by SvelteKit. There are no microservices: SvelteKit server routes are the API, the same Node.js process serves the frontend, handles WebSocket connections for collaborative editing, and processes background jobs.

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

    subgraph Node["Node.js Application (port 3000)"]
        SK["SvelteKit\nHTTP Routes / API"]
        HC["Hocuspocus\nWebSocket /collab"]
        BQ["BullMQ Workers\n(background jobs)"]
    end

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

    SK -- "Drizzle ORM" --> PG[("PostgreSQL 16\npg_trgm · full-text search")]
    HC -- "Yjs state" --> 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["Mail server\n(SMTP)"]
    BQ -- "HTTP POST" --> Hooks["Outbound Webhooks\n& Publishing channels"]

Request lifecycle (simplified):

  1. Browser sends HTTP request → Traefik (TLS termination) → Node.js
  2. SvelteKit route handler authenticates session (Redis) and checks permissions
  3. Service queries Drizzle ORM → PostgreSQL
  4. Response serialised as JSON (API routes) or rendered to HTML (page routes)
  5. Background effects (webhooks, notifications, backlinks) dispatched via BullMQ
src/
├── lib/
│ ├── components/ # Svelte UI components
│ │ └── editor/ # TipTap editor + extensions
│ ├── server/ # Server-only code
│ │ ├── db/ # Drizzle schema + migrations
│ │ ├── services/ # Business logic
│ │ └── policies/ # Authorization checks
│ └── utils/ # Shared utilities
├── routes/
│ ├── (app)/ # Authenticated app routes
│ ├── (auth)/ # Login / registration
│ └── api/ # API endpoints (RPC-style + /v1/ REST)
└── hooks.server.ts # Auth, rate-limit, security headers
  1. Request arrives at hooks.server.ts — rate-limit, auth session loaded
  2. Route handler calls a service function (e.g. getDocument, createDocument)
  3. Service queries Drizzle ORM → PostgreSQL
  4. Response serialised as JSON (API routes) or rendered to HTML (page routes)
  5. Background effects (webhooks, notifications, backlinks) dispatched via BullMQ

Real-time collaboration uses Yjs (CRDT) with Hocuspocus as the server:

  • Each document has a unique room identified by its UUID
  • The Yjs document is persisted to PostgreSQL via @hocuspocus/extension-database
  • On connect, the server loads the current document content and hydrates the Yjs state
  • On disconnect, the final Yjs state is flushed back to the documents table
  • Sessions: HTTP-only, Secure, SameSite=Lax cookies with random tokens
  • CSRF: All mutating API requests from the browser require session auth
  • API keys: kb_ prefix, encrypted at rest, scope-limited
  • CSP: Nonce-based policy managed by SvelteKit
  • CORS: Only allowed on /api/v1/* with Authorization header support