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(docker5173:3000). ORIGINmust 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
importno longer merges chunks.