Immersive 3D — The Stream & The Dome — Handoff
Status: shipped to prod, in active tuning. Last updated 2026-07-09.
This is the technical handoff for the WebXR work. The vision/product narrative lives in AR_GLASSES_LIVING_VOICE.md (Living Voice Track B). Read this one to pick the work back up; read that one for why it exists.
Built almost entirely blind — the agent could not see the scenes render; the user sends screenshots and the agent iterates. Every visual number (positions, scale, colors, camera) is a best-guess unless a screenshot confirmed it. The Adjust / Edit tooling exists precisely so the user can dial it in without the agent guessing.
The two surfaces
1. The Stream — /dashboard/stream/immersive
An interactive WebXR "river" of your journal notes. Stand at the water's edge (or sit in a vessel / on a dock); notes drift past; catch one → it lifts out and opens in full space, reading aloud in voice; release it → it casts back into the current.
Confirmed working in-browser (user screenshots). Features:
- Cards typed by data: text
note,voice(waveform + in-ear playback),comic/photo(attached image),drop(encouragement),mood(glowing orb), plus non-journal stream types:song(Signal Union),track(Veneer, has audio),veneer,graphene,glass(Looking Glass, "N years ago today"),book,reel. - Curated current + a selector (For you / On this day / Recent / Resurfaced).
- Opened note shows its dashboard background image + its font (PremiumUltra .ttf via troika; Google fonts fall back).
- Atmosphere: procedural star sky dome, drifting fireflies, a moon + moonglade reflection, opt-in Web-Audio ambient river, weather (clear/mist/rain + ripple rings), vessels (canoe/kayak/rowboat/paddleboard/dock), cards bump around an open panel.
- Social: dome houses on the hillsides (you + family + AI companions), tap → "about them", visit an AI companion's stream (their shared entries flow instead of yours). Tapping your own house → enters your dome.
- Persistence: current + vessel + weather in localStorage.
2. The Dome — /dashboard/dome
Your spatial "home" (Quest-Home-style). A geodesic dome interior whose décor mirrors your DreamPro goal adherence. Reached from the stream ("◇ Enter your dome") or by tapping your own house on the hill.
- The model is real:
public/models/dome.glb— the commissioned SketchUp interior, exported to glTF and compressed 17 MB → 3 MB (Draco geometry + WebP textures via@gltf-transform). Loaded withuseGLTF(url, true). - Goal-reactive layer (rides over the static model): a floating goal hologram (one
glowing tile per DreamPro dream, brightening with
progress_percentage), and the whole space's warmth/lighting lift with overall adherence. - Hubs: a "Write at the Café" desk invite (→
/write-cafe; a dedicated Write-Café 3D is the intended next step) and a corkboard of cross-pollination pins (Write Café, Veneer, Graphene, Odessa, Lovio, DreamPro). - World outside (
DomeExterior.tsx): star sky, dark hills, other domes glowing, water on one side — visible when the shell is glassed via "See out". - First-person controls (
LookControls): eye fixed at room centre at your height, drag to look. NOT orbit (orbit dipped under the floor — see landmines).
⚠️ THE OPEN BLOCKER — bake the dome layout defaults
The dome layout is not yet dialed in. The agent's default positions/scale/orientation are
guesses. The user must set them via the in-scene tools, then hand the values back to bake as
DEFAULT_ADJUST.
The handoff loop:
- User opens
/dashboard/dome→ ⚙ Adjust (bottom-right). - Sets model orientation (Tip/Spin/Roll), Scale, Height (range ±8 — the model is a full geodesic sphere so its centre ≈ the deck; Height drops it onto the user's feet), Eye height, and See out (glass the shell).
- Checks "Edit layout — drag in the scene" → clicks the goal shelf / desk / corkboard → drags the TransformControls move-gizmo to place each.
- Hits Copy settings → pastes the JSON to the agent.
- Agent bakes that JSON into
DEFAULT_ADJUSTincomponents/dome/DomeScene.tsxso every user gets the correct dome with nothing to configure.
Until step 5, new visitors see the un-tuned default. Known-wrong-ish defaults as of writing:
model floats a bit / fixtures are guessed positions. DomeAdjust shape (all persisted under
localStorage key dome:adjust):
{ rx, ry, rz, scaleMul, yOff, camY, seeOut, glassTop, glassOpacity,
holo:{x,y,z}, desk:{x,y,z}, cork:{x,y,z}, corkRotY }
If "Edit layout" or new controls aren't visible: it's almost always Vercel deploy lag (~2–3 min after merge) or the Adjust panel needs scrolling (it's scrollable; controls are below See-out). Hard-refresh.
Architecture & files
Frontend, Next 15 App Router. All 3D is @react-three/fiber v9 + drei + @react-three/xr v6
three, loaded client-only (dynamic(() => import(...), { ssr: false })).
Stream
apps/frontend/src/app/dashboard/stream/immersive/page.tsx— data fetch, overlay UI, selectors,toNotes()mapping (journal → StreamNote),handleVisit.apps/frontend/src/components/stream-vr/StreamXRScene.tsx— the scene (river, cards, sky, moon, weather, vessels, open panel, bump physics). Big file.apps/frontend/src/components/stream-vr/Neighborhood.tsx— hillsides, dome houses, AboutPanel.apps/frontend/src/lib/streamExtras.ts— fetch non-journal stream types.apps/frontend/src/lib/useAmbientRiver.ts— Web-Audio ambience.
Dome
apps/frontend/src/app/dashboard/dome/page.tsx— data fetch, adherence calc, the Adjust + Edit panel (localStoragedome:adjust, Copy-settings).apps/frontend/src/components/dome/DomeScene.tsx— model load/normalize, goal hologram, lights,LookControls,TransformControlsedit mode,DomeAdjust+DEFAULT_ADJUST.apps/frontend/src/components/dome/DomeFixtures.tsx— DeskInvite + Corkboard (interactiveprop gates nav in edit mode).apps/frontend/src/components/dome/DomeExterior.tsx— sky/hills/water/other-domes.apps/frontend/public/models/dome.glb— the 3 MB model.
Shared type shim
apps/frontend/src/types/react-three-fiber.d.ts— re-applies fiber's JSX element map.
⚠️ Landmines (learned the hard way — do not re-discover)
- Next 15's App Router runtime is React 19 (vendored).
@react-three/fiberMUST be v9+ (React-19 reconciler). v8 builds clean but crashes in-browser withCannot read properties of undefined (reading 'ReactCurrentBatchConfig'). (#1102→#1103) @types/reactpinned to 18.3.27 (rootpackage.jsondevDep +overrides) so the three-stack's peer resolution can't drag a conflicting root v19 alongside the frontend's v18 (dual-copy'Link' cannot be used as JSXerrors).@iwer/devuistubbed (src/stubs/iwer-devui.js+next.config.jswebpack alias). It's the XR emulator xr pulls in; has an undeclaredzustandimport that breaks the build.- fiber alias in
next.config.js— fiber v9 nests in the frontend workspace; xr/@pmndrs/xr hoist to the repo root and can't resolve it, so a webpack alias pins@react-three/fiberto the one copy (also guarantees the single instance r3f needs). - Keep all
@react-three/*imports out of a page module — load the scene viadynamic(ssr:false)so the reconciler chain never hits the server prerender. - The Draco GLB loads its decoder from the gstatic CDN at runtime (drei
useGLTF(url,true)default). Fine in-app. - SketchUp is Z-up; three is Y-up → the model needs
rotation.x = -π/2(the Tip control). It's a full sphere, so anchor its centre (≈ deck) to the floor, not its bottom. - Interior camera must be first-person, not orbit — orbit circles the room centre and dips the camera under the floor / outside the dome.
Math.random()/Date.now()are fine in the app (that restriction is Workflow-scripts only). But r3fuseFramegives(state, delta)— usedelta, don't call Date.now.
Build gate before any PR touching .tsx: cd apps/frontend && npm run check (lint+typecheck).
3D pages don't use useSearchParams/dynamic params, so a full next build isn't strictly
required, but it's the only thing that catches prerender/runtime-shape issues — run it for dome
changes. Node is via nvm: export PATH="$HOME/.nvm/versions/node/v22.14.0/bin:$PATH".
Data sources (all real endpoints, mapped before building)
- Stream notes:
GET /api/journal/stream-curated?limit=&mode=(modes foryou|onthisday|recent| resurfaced) — blends on-this-day/pinned/voice_stream_enabled/recent/resurfaced, tags each withstream_reason. Attached photo:getFirstImageForEntries(signed thumbnails). - Neighbors (houses):
GET /api/journal/stream-neighbors— you (profiles), family (family_members), a few AI companions (ai_personas, withvisitUserId). - Visit a stream:
GET /api/journal/stream-visit?userId=— filtered throughfilterEntryForViewerFOR THE CALLER (only what they've shared with you). Works for personas now; humans need a per-user visitUserId + share-settings gate (not built). - Non-journal types: internal to
streamExtras.ts(signal-union, veneer, story-seasons, looking-glass, cafe/books, fb-reels endpoints). - Dome goals (books):
GET /api/dreampro/dreams(progress_percentage,category.color). - Dome adherence:
GET /api/coaching/memomentum.current_streak(2-week streak = "full"), blended 50/50 with average goal progress.
Backend routes: apps/backend/src/routes/journal.ts (stream-curated, stream-neighbors,
stream-visit), apps/backend/src/services/journal-entry-attachments.ts
(getFirstImageForEntries).
Roadmap / what's pending
Immediate (blocks polish):
- Bake dome
DEFAULT_ADJUSTfrom the user's Copy-settings JSON (see blocker above). - Tune stream blind-visuals from user notes (moon scale, firefly density, vessel proportions, rain, etc. — all quick number tweaks).
Next features (in rough priority):
- Write-Café 3D — the desk invite should open a dedicated 3D writing scene, not just link
to
/write-cafe. - Map goals to the model's real shelves — instead of a floating hologram, drive the model's own bookshelf/plant/lights by goals (needs the model's mesh coords — inspect in-scene).
- Human stream visiting — give family/friends a
visitUserIdonce relationship + share-settings permit (thestream-visitread path already handles them). - VR controller grab for Edit mode (TransformControls is desktop-mouse only).
- Corkboard = real cross-pollination — post/pin actual items, two-way with services.
- Presence cues — a house/dome lights when someone shares; eventually a Lovio voice at the door (read-only bright line intact).
Assets: the user has more from the 99designs project (Dome.skp, interior renders, a
hive-dome-cropped-1.png). .skp can't be transcoded by the agent — needs a SketchUp→glTF
export. The spheres/.webloc files are dead (Google Poly shut down 2021).
PR history (this arc)
Stream: #1102 (scene) · #1103 (fiber v9 fix) · #1104 (docs) · #1105 (sky+cards) · #1106 (curation) · #1107 (current selector) · #1108 (ambient) · #1109 (catch) · #1110 (vessels) · #1111 (fireflies+ persist) · #1112 (cast-back) · #1113 (moon) · #1114 (weather) · #1115 (attached images) · #1116 (dome houses) · #1117 (visit) · #1118 (note bg+font) · #1119 (card bump) · #1120 (non-journal types). Dome: #1121 (goal-gated interior) · #1122 (you-house→dome) · #1123 (real model) · #1124 (adjust panel) · #1125 (brighter+world) · #1126 (camera fix) · #1127 (see-out) · #1128 (first-person + desk/corkboard) · #1129 (admin placement + center-anchor) · #1130 (in-scene Edit/drag).