Files
classroomio/apps/api
35f5216839 fix(org): favicon fallback, OG logo styling, and Discord embed CDN URLs (#714)
* fix(org): serve OG images from CDN for Discord embeds

Discord times out on slow dynamic OG generation (~3-5s). Persist generated
images to R2, point meta tags at the media CDN, and warm the API endpoint
on org-site page loads so crawlers get a fast static PNG URL.

Co-authored-by: Rotimi Best <[email protected]>

* docs: add org-site Open Graph and favicon guides

Document user-facing sharing behavior and contributor architecture
for dynamic OG images, favicon fallback, R2/CDN caching, and embed
cache invalidation.

Co-authored-by: Rotimi Best <[email protected]>

* fix(org-site): address PR review on OG CDN fallback and docs

- Fall back to API OG URL when CDN object is not yet available (HEAD check)
- Cancel warm-fetch response body to avoid connection pool exhaustion
- Align tenant-router OG edge cache TTL with one-hour API revalidation
- Fix favicon docs navigation and add Custom Domain tab screenshot
- Replace Chiberry Artistry example with AI Academy

Co-authored-by: Rotimi Best <[email protected]>

* fix(org-site): accept docker hostnames for OG warm URL

Use isValidHttpUrl for PRIVATE_SERVER_URL so internal values like
http://api:3081 work for SSR warm fetches in Docker Compose.

Co-authored-by: Rotimi Best <[email protected]>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
2026-07-09 13:38:49 +01:00
..
2026-04-27 11:40:38 +01:00

ClassroomIO Backend

Supported Features

  1. Certificate download for students.
  2. Uploading videos to cloudflare (for storage) and Muse.ai (for transcription)

Running locally with video upload + playback

Lesson videos run through an HLS pipeline on Cloud. The browser encodes a 360/720/1080 ABR ladder with Mediabunny, uploads segments to R2 via presigned PUTs, and the player loads /hls/{assetId}/master.m3u8 from the API.

How /hls/* resolves depends on the environment:

URL the browser hits Who serves the bytes
Cloud production https://<tenant-host>/hls/... Cloudflare Worker (apps/tenant-router) — intercepts the path and streams directly from an R2 binding. Render is never in the bytestream.
Local dev / self-hosted ${PUBLIC_SERVER_URL}/hls/... (direct to API) Hono streaming route on the API — session-auth, then getS3Client().send(GetObjectCommand).

Self-hosted (PUBLIC_IS_SELFHOSTED=true) skips HLS entirely and falls back to raw-MP4 upload — no R2-specific code path. Everything below is for Cloud-style local development.

Required env

apps/api/.env:

# Object storage — Cloudflare R2 OR an S3-compatible local backend (MinIO, etc.)
CLOUDFLARE_ACCOUNT_ID=…
CLOUDFLARE_ACCESS_KEY=…
CLOUDFLARE_SECRET_ACCESS_KEY=…
# (or OBJECT_STORAGE_ENDPOINT / OBJECT_STORAGE_ACCESS_KEY_ID / OBJECT_STORAGE_SECRET_ACCESS_KEY)

# Optional — only needed if you want extract-audio → Whisper transcripts
OPENAI_API_KEY=…

# Optional — Redis enables the BullMQ media job pipeline. Without it the
# asset row is still created, but no transcription/thumbnail jobs run.
REDIS_URL=redis://localhost:6379

# Only required when you want to exercise the production cookie-mint path
# (`POST /organization/assets/:assetId/hls/cookie`). Locally the Hono /hls/*
# route uses session auth, so most of the time you can leave this unset.
# Same value must also be set as a Cloudflare secret on tenant-router for prod.
HLS_SIGNING_SECRET=…

apps/dashboard/.env:

# Already required by the existing API client — used as the HLS base URL too.
PUBLIC_SERVER_URL=http://localhost:3000

Run

pnpm --filter @cio/api dev
pnpm --filter @cio/dashboard dev
# (optional) pnpm --filter @cio/jobs dev  # for transcription + thumbnail jobs

Drop a video into a lesson; the dashboard encodes locally, you'll see segments arrive under videos/{assetId}/hls/… in R2 and an assets row with hls_manifest_key populated. Playback flows directly browser → API → R2 — no wrangler dev needed locally.

Production setup (one-time)

For Cloud deployment, generate the HMAC signing key the Worker uses to verify playback cookies and set it on both sides:

node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))"
# On Render (apps/api service env):
HLS_SIGNING_SECRET=<paste>

# On Cloudflare (apps/tenant-router):
pnpm --filter @cio/tenant-router exec wrangler secret put HLS_SIGNING_SECRET
# paste the same value when prompted

Debugging Fly.io

View logs from fly.io in real time