Collaborative, multi-tenant presentation system for large video walls.
Gemma Shop lets multiple users edit decks in real time and publish synchronized output to distributed wall nodes. The platform is optimized for low-latency editing, fast wall playback, and commit-based versioning.
- Architecture Overview: high-level system structure, ownership boundaries, and onboarding path.
- Realtime Protocol:
/busand/yjstransport/message semantics. - Bus Piping: detailed topology for the realtime bus, scope model, YJS co-bus integration, and naming/refactor proposals.
- Gallery State Machine: current gallery card/dialog transitions, sync semantics, and refactor direction.
- README.md: high-level project overview and contributor onboarding.
- Real-time collaborative editing of slide-based decks
- Multi-endpoint runtime with specialized clients:
editor: authoring UIwall: render nodecontroller: wall binding/orchestrationgallery: presentation-aware public control/listing surfaceroy: specialized graph/telemetry client
- Commit graph with mutable head + immutable snapshot history
- Asset pipeline for image/video upload, processing, and live broadcast to editors
- Turborepo + bun
- React 19 + React Compiler
- TanStack Start + Router + Query + Form
- Vite 8 + Nitro v3
- Tailwind CSS v4 + shadcn/ui + Base UI (base-maia)
- MongoDB
- Better Auth
- Oxlint + Oxfmt
├── apps
│ ├── web # TanStack Start web app + Nitro websocket routes
├── packages
│ ├── auth # Better Auth
│ ├── db # MongoDB
│ ├── emails # Template for emails
│ └── ui # shadcn/ui primitives & utils
├── tooling
│ └── tsconfig # Shared TypeScript configuration
├── turbo.json
├── LICENSE
└── README.md- Tracks peers by role (
editor,wall,controller,gallery,roy) - Interns
(projectId, commitId, slideId)into numericScopeId - Maintains in-memory scope layer state for fast relay/hydrate
- Runs periodic loops:
- VSYNC sync loop for active videos
- Autosave loop for dirty scopes
- Stale-peer reaper loop
- Broadcast bridges:
__BROADCAST_EDITORS__for processing progress__BROADCAST_ASSET_ADDED__for newly created assets__BROADCAST_WALL_BINDING_CHANGED__for server-side bind/unbind mutations__BROADCAST_PROJECT_PUBLISH_CHANGED__for publish/unpublish propagation to gallery peers
For full flow maps (bind/unbind, hydrate, scope internals, YJS bridge path), see PIPING.
gallerysocket scaffolding is now available viaapps/web/src/lib/galleryEngine.ts.- Initial
/bushandshake support forspecimen: 'gallery'andgallery_statesnapshots is in place. - Implemented over WS:
- bind override approval flow for editor bind requests
- publish/unpublish live feed
- admin/gallery wall bind/unbind propagation
- Remaining migration work is mostly UX-level data shaping and removing residual query polling paths.
- Gallery dialog sync uses separate UI intent signals:
forceCloseSignal: synced close for connected cards on wall unbind (applies to fullscreen/minimized).forceCloseMinimizedSignal: live-session transition helper to close minimized cards while fullscreen cards are demoted to expanded. These are intentionally separate because unbind and live-transition flows require different state transitions.
- Full transition contract is documented in Gallery State Machine.
- Gallery now issues short-lived controller API tokens (
gem_ctrl_*) when opening a controller session. - Token is injected into controller URL as
_gem_t=<token>. - Tokens are bound to the current wall + bus scope and are revoked automatically when:
- wall is rebound,
- wall is unbound,
- or scope is garbage-collected.
- Current proof-of-concept endpoint:
POST /api/portal/v1/reboot- Auth via
Authorization: Bearer <token>(query fallback_gem_tis also accepted). - Optional node targeting using
{ c, r }in JSON body for wall node coordinates.
- CORS is enabled for
/api/portal/v1/rebootto support external custom controllers on other domains.
- Zustand store for layers, slides, selection, and tool state
- Handles optimistic updates and server synchronization
- Uses throttled layer update sends to reduce network chatter
- Tus upload ingestion
- Media type detection and post-processing
- Image path:
- Copy original
- Blurhash compute
- WebP size variant generation
- Video path:
- FFmpeg transcode to MP4
- Preview frame extraction
- Blurhash + variant generation from preview
- Inserts asset metadata and broadcasts to active editors
- Goal: isolate CPU-heavy media work (
sharp,ffmpeg) from web request handlers, while preserving current UX and protocol behavior. - Phase 1 contract:
STRICT_BLOCKING=trueby default.- Meaning: upload response still waits for processing completion (same behavior as today).
- Reason: avoids changing editor assumptions about immediate asset readiness during initial isolation rollout.
- Future direction: optional async mode (
202 Accepted) once editor/runtime supports pending assets and progressive metadata hydration.
- Job state model: hybrid persistence + signaling.
- Persistent truth in DB (job lifecycle, retries, recovery after restarts).
- Pub-sub for low-latency progress/completion events between processes.
- Rationale: pub-sub alone is fast but not durable; DB alone is durable but noisier for live signaling.
- Timeout policy for long video processing:
- Use inactivity timeout based on last progress heartbeat, not absolute wall-clock job duration.
- Long-running transcodes are valid; only fail when no progress has been observed for the configured stale interval.
- Architectural note for async pipeline adoption:
- Fully asynchronous media processing implies layers may exist before complete asset metadata is ready.
- We likely need a cleaner separation between layer content and asset metadata/progress state.
- Potential outcome: keep commit
content.slides[*].layerslean and fetch/enrich asset metadata via dedicated asset state paths.
- Hardening next steps:
- Add maintenance controls for
jobsretention (TTL or scheduled prune) so completed/failed rows do not grow unbounded. - Harden retry classification and stale-job recovery policy (transient vs permanent failures, deterministic multi-instance behavior, clearer user-facing errors).
- Replace direct
urlfields in image/video/web layers with stable asset pointers (for exampleassetId). - This prevents metadata duplication across layers (including variant sizing and blurhash concerns), improves file lifecycle management, and keeps commit payloads lean.
- Add maintenance controls for
- Projects, commits, assets in MongoDB
- Mutable HEAD commit used for active editing/autosave
- Manual save creates immutable snapshot and advances chain pointer
- Slide metadata updates persist independently from layer payloads
projects: ownership/collaborators,headCommitId,publishedCommitIdcommits: graph nodes withparentId,content.slides[*].layersassets: media metadata, URLs, preview/blurhash, variants, visibility
- Bun
- MongoDB replica set
- Environment variables configured in
.env
- Install deps:
bun install - Run all dev targets:
bun run dev - Run web only:
bun run dev:web - Lint:
bun run lint - Format:
bun run format - Quality checks:
bun run check - Build web (generates legal notice artifacts):
bun run --filter=@repo/web build - Docker test stack up (build + detached):
bun run docker:test:up - Docker test stack status:
bun run docker:test:ps - Docker test stack logs (follow):
bun run docker:test:logs - Docker test stack down:
bun run docker:test:down - Docker test stack reset (remove volumes):
bun run docker:test:reset
- Local debug image with source maps embedded:
BUILD_SOURCEMAPS=true KEEP_SOURCE_MAPS=true bun run docker:build - Local production-like image without source maps:
BUILD_SOURCEMAPS=false KEEP_SOURCE_MAPS=false bun run docker:build - CI workflow
Container Image (OCI)supports a manualworkflow_dispatchtoggle (include_sourcemaps) to keep.mapfiles in published images.
/galleryproject listing/quarryproject management/quarry/editoreditor flow/wallwall node endpoint (query paramsc,r,w)
- Scope identity is
(projectId, commitId, slideId)and must remain stable - Bus cleanup must not delete active scopes (editors/walls present)
- Video sync timestamps are authoritative server-side once playback starts
- Autosave only updates mutable HEAD context
- Build-time plugin:
apps/web/plugins/thirdPartyNotices.ts - Generated artifacts:
/third-party-notices.json/THIRD_PARTY_NOTICES.txt
- In-app page:
/legal/notices
The notices are generated from tree-shaken modules detected in production bundle chunks.
- Text styling is authored in Lexical HTML and rendered through both:
- editor DOM,
- canvas via SVG
foreignObject, - wall DOM renderer.
- Baseline text context (font family, base font size, line-height, padding) is centralized in
apps/web/src/lib/textRenderConfig.tsand reused by all renderers to avoid drift. - Font size in toolbar is displayed as virtual
pxfor UX, but stored asemin inline styles for scale-safe persistence. - Canonical text scale for font-size conversion uses
scaleYby design. Alternative considered: isotropic averagesqrt(scaleX * scaleY).
- High complexity hotspots:
EditorSlateToolbar- upload route
onUploadFinish - upload route
detectMediaType
- While a controller is bound to a wall under active editor live broadcast, controller slide state can drift from scope reality;
slides_updatedmetadata events are now partially reconciled client-side, but structural slide changes still require full commit refetch and can momentarily desync. - Gallery takeover confirmation modal currently uses a custom layering treatment instead of the shared app dialog stack; this was introduced to work around dialog z-index conflicts with project cards and should be replaced by a unified, application-level dialog layering fix.
- Upload/session tokens are currently in-memory and unsigned (including upload tokens and portal tokens), so process restart or multi-instance deployment can invalidate active tokens unexpectedly.
- Upload dialog (
apps/web/src/components/UploadDialog.tsx): progress tracking is keyed by filename, so same-name files can collide in UI status updates. - Upload flow token lifecycle: upload tokens are short-lived (15 minutes), and finalize validation occurs server-side; long uploads may fail at finalize if token expiry is hit mid-transfer. Consider a refresh/reissue strategy or finalize-window policy.
- Upload dialog (
apps/web/src/components/UploadDialog.tsx): progress tracking is keyed by filename, so same-name files can collide in UI status updates. - Upload flow token lifecycle: upload tokens are short-lived (15 minutes), and finalize validation occurs server-side; long uploads may fail at finalize if token expiry is hit mid-transfer. Consider a refresh/reissue strategy or finalize-window policy.
- Controller endpoints are intentionally public-facing for wall operation flows, but this currently means custom/public controller paths may attempt to access protected app assets without an explicit authz contract.
- A dedicated authorization flow is still required for controller sessions (and likely other public runtime surfaces), so unauthenticated clients cannot access private assets/configuration.
- Recommendation: introduce scoped, short-lived controller/session tokens with explicit asset permissions and origin constraints, then apply the same pattern consistently across other public endpoints.
- CSP is set server-side in middleware and uses a per-request nonce for script execution.
- Development uses
Content-Security-Policy-Report-Onlyso violations are visible without breaking local workflow. - Production enforces CSP and keeps
script-srcstrict (dev-onlyunsafe-evalsupport exists for tooling/HMR behavior). - Styles are split intentionally:
style-src/style-src-elemstay nonce-based in production for<style>blocks.style-src-attr 'unsafe-inline'is enabled because the app currently relies on many React inline style attributes (style={{ ... }}) for runtime positioning/rendering.- Reporting is wired to
/api/report-cspon the same origin, and both legacy (report-uri/Report-To) and modern (Reporting-Endpoints) signals are emitted to improve browser coverage. - Current resource directives (
connect-src,frame-src,img-src,media-src,font-src,worker-src) are intentionally broader than minimum to support websocket transport, iframe web layers, map resources, and media pipelines; tighten these host lists over time using collected CSP reports.
- Pure performance internals with no API changes
- Dead code removal guarded by lint/tests
- Component decomposition and behavior-preserving rewrites
- Optional protocol/index optimizations
- Wall hydration on bind/unbind
- Active video sync consistency
- Autosave and manual save semantics
- Asset creation + editor broadcast
- Commit history and branch promotion flows
- Lint + type checks
- Upload image/video and verify asset records
- Multi-editor sync test on same scope
- Wall bind/unbind + hydrate verification
- Manual save + publish/unpublish regression pass