Role
Full-Stack Developer / Architect
Client
Concept Dash
Duration
Aug – Sep 2026 (~2 weeks, inferred from file timestamps; no git history)
This is the pivot out of a multi-tenant architecture that had stopped scaling. In the older platform, tenant identity was baked in at compile time: adding a customer meant editing a config file, committing, redeploying, and provisioning separate hosting. Six properties meant six deployments and six sets of credentials. Tour Studio replaces that with one application and many projects, where a project is created at runtime, lives in its own space, and shares nothing with any other. Nothing about a project is known at build time — that is the entire change.
It is a deliberate no-backend POC. Panoramas live in IndexedDB as Blobs via Dexie; there is no server and no account. What makes that a POC decision rather than a rewrite risk is a single enforced seam: only `lib/storage/repo.ts` knows the data is in IndexedDB, every feature calls the repo, and swapping that one module for a fetch client leaves everything above it untouched. Dependencies point one way — `routes → features → lib` — and a feature may never import another feature, a rule that forced image probing down into `lib/images/` the moment the demo seeder needed the same validation.
The schema carries the domain insight. Structure and placement are stored separately: a `Link` knows it leads to the dining room but not which way to look, and the yaw/pitch that aims the marker lives in a `Placement` row keyed by id. Re-aiming a marker never touches structure, and "what has been calibrated" is one query. Angles are optional and must stay optional — no defaulting to zero, no computed fallback — because on the previous project uncalibrated markers were auto-spread around the horizon, which drew an identical ring in every room and read to users as though the last room's markers were following them. Those placeholder angles then exported as real, so tooling reported a half-finished tour as complete. The repo's `docs/lessons-inherited.md` catalogues thirteen such bugs, each paid for once already.
The workflow is four steps — Photos, Rooms, Calibrate, Publish. Upload enforces 2:1 equirectangular with a plain-words refusal and decodes sequentially to survive large batches. Rooms names panoramas, assigns floors, declares one-way doorways (a door is not always visible from both sides) and reports isolated and unreachable rooms from the declared graph. Calibrate runs four stages per room in dependency order — dot on the plan, north offset against a live compass cone, arrival view, then arm-and-click every doorway and item — where placing one marker arms the next, making a room roughly one click per marker. Publish zips `tour.json` plus every asset with JSZip behind a preflight that omits and counts unplaced markers rather than inventing angles for them, and requires dead ends to be acknowledged by hand before the download unlocks. The bundle ships its own player: a second Vite build produces a standalone viewer that `scripts/inline-viewer.mjs` folds into one HTML file, and the exporter injects the tour JSON into it — escaping every `<` so a room named `</script>` cannot break out of the tag.
Tech Stack
Features
- Runtime projects, not build-time tenantsEvery project is created in the browser at runtime with its own rooms, floors and assets, replacing an architecture where adding a customer meant a config edit, a commit and a new deployment.
- Guided four-stage calibrationPer room: place a dot on the floorplan, set north against a live compass cone, choose the arrival view, then arm-and-click each doorway — placing one marker arms the next, so a room is about one click per marker.
- A schema that cannot fake progressMarker angles are nullable with no sentinel and no fallback, so an unplaced marker is unrepresentable as a placed one, and the exporter omits and counts them instead of shipping guesses.
- Self-contained tour exportPublish zips the tour data and every asset together with a standalone single-file viewer built by a second Vite pass, so the folder can be dropped on any static host and simply is the tour.
- A swappable storage seamOne module knows the data lives in IndexedDB; every feature talks to the repo instead, so adding a real backend means rewriting that file and nothing above it.
- Graph checks on the room networkDead ends and unreachable rooms are detected as separate questions on the declared link graph, and dead ends must be acknowledged by hand before a tour can be published.
- One-click sample projectA finished 14-room sample tour ships with the app and loads as an ordinary editable project, so the studio can be evaluated with no camera and no assets.
