Skip to content

Architecture β€” overview

NEXORA is a Docker Compose platform: a SvelteKit web app, an AI worker, a Postgres database, a data lake (Parquet) and an LLM reporting layer.

flowchart LR
  subgraph UI["Web app (SvelteKit / adapter-node) Β· :5173"]
    DASH["Dashboard<br/>Post-Lesson Β· tabs"]
    ADMIN["Admin<br/>upload / pipeline"]
    PARENT["Parent portal"]
  end
  API["REST API<br/>(/api/*)"]
  GOLD["Gold layer (DuckDB)<br/>parquet + SQL"]
  PG[(PostgreSQL<br/>users Β· sessions Β· messages)]
  LAKE[["Data lake<br/>bronze β†’ silver β†’ gold"]]
  WORKER["Worker<br/>(AI pipeline, mock)"]
  R2(("Storage<br/>R2 / S3 or filesystem"))

  UI --> API
  API --> GOLD
  API --> PG
  API --> R2
  WORKER -.polls sessions.-> PG
  WORKER --> LAKE
  WORKER --> R2
  GOLD --> LAKE
  LLM["LLM reports<br/>(OpenRouter)"]
  GOLD --> LLM --> PG

The 5 services (docker-compose.yml)

Service Role
db PostgreSQL 16 (internal to the Docker network, no published port)
seed-accounts one-shot: Drizzle schema + demo accounts (idempotent)
import one-shot: imports the 15 lessons + builds the gold (skips if .import_done)
app web (5173 β†’ 3000), starts after the init
worker video/AI analysis pipeline (mock mode by default), polls every 5 s
docs documentation site (MkDocs) on port 8080

Startup dependency chain

flowchart TD
   A["db (healthy)"] --> B["seed-accounts"]
   B --> C["import (one-shot)"]
   C --> D["app"]
   C --> E["worker"]

Data flow of a lesson

flowchart LR
   V["video .mp4"] --> U["POST /api/admin/videos"]
   U --> SS["session status=processing"]
   W["worker"] --> POLL["polling sessions"]
   POLL --> CSV[("annotations CSV")]
   CSV --> IMP["import_march / pipeline"]
   IMP --> BR["bronze (parquet)"]
   BR --> SI["silver (normalized windows)"]
   SI --> GO["gold (engagement, timeline…)"]
   GO --> API2["web API (DuckDB)"]
   API2 --> O["dashboard / reports"]

Network & access

  • The app listens on http://localhost:5173 (docker 5173:3000).
  • ORIGIN must be the public domain / IP in prod, otherwise Better Auth rejects redirects.
  • The database is not exposed to the host; the worker and import go through the internal network.

Known edge cases

  • Transcoding / chunks: transcode.ts (ffmpeg) needs a real filesystem; on a remote backend (R2) streaming falls back to reading the whole object (no HTTP Range).
  • Import in R2 mode: the CSV must be the merged CSV (timelapsed over the total duration), because in R2 mode import no longer merges chunks.