Browser-based tool for real estate agents: scan a property room-by-room with a phone, get an AI-reconstructed 360° tour, pay a one-time fee sized to the listing, embed the result as an iframe on any listing portal, then track engagement (visits, leads, timestamps) from a control panel.
The product is mostly an orchestration layer over third-party services, not a hand-rolled stack — the diagram below marks every external dependency explicitly so that's visible at a glance, not just asserted.
One data core (Supabase), six functional clusters around it, two people who
touch the system. Dashed lines are actor interactions or the one retry loop;
(external) marks a third-party dependency.
flowchart TB
DATA[("Data Layer<br/>Supabase — Postgres · Auth · Storage")]
subgraph CAP["01 · Capture"]
CAPAPP["Capture App"]
CAPAI["Capture AI (external)"]
CAPAPP <-->|live frame score| CAPAI
end
subgraph REC["02 · Reconstruction"]
QUEUE["Job Queue"]
STITCH["Stitching"]
ENH["Image Enhance (external)"]
QUEUE -->|assemble panorama| STITCH
STITCH -->|sharpen / fill gaps| ENH
end
subgraph REV["03 · Review"]
VIEW["Viewer"]
GATE["Review Gate"]
VIEW -->|approve / reject| GATE
end
subgraph COM["04 · Commerce"]
PRICE["Pricing"]
STRIPE["Stripe (external)"]
PRICE -->|checkout session| STRIPE
end
subgraph DEL["05 · Delivery"]
SHARE["Share Page"]
EMBED["Embed AI (external)"]
PORTAL["Listing Portal (external)"]
SHARE -->|guides embed setup| EMBED
SHARE -->|iframe| PORTAL
end
subgraph ANA["06 · Analytics"]
AGG["Analytics"]
PANEL["Control Panel"]
end
CAPAPP -->|writes frames + manifest| DATA
DATA -->|triggers on upload| QUEUE
ENH -->|writes rendered assets| DATA
DATA -->|reads manifest| VIEW
GATE -->|status: approved| DATA
GATE -.->|retry / re-render| QUEUE
GATE -->|quote price| PRICE
STRIPE -->|webhook: paid| DATA
DATA -->|reads listing, gated: paid| SHARE
SHARE -->|visit + engagement events| AGG
AGG -->|writes events + leads| DATA
DATA -->|engagement rollup| PANEL
AGENT(["Agent"]) -.-> CAPAPP
AGENT -.-> VIEW
AGENT -.-> PANEL
VISITOR(["Portal Visitor"]) -.-> SHARE
VISITOR -.-> PORTAL
classDef ext fill:#f4dfc0,stroke:#b8681f,color:#3a2410;
classDef hub fill:#b8681f,stroke:#7a4614,color:#fff;
class CAPAI,ENH,STRIPE,EMBED,PORTAL ext;
class DATA hub;
| # | Cluster | Nodes | Mechanism |
|---|---|---|---|
| 01 | Capture | Capture App, Capture AI (ext) | Only two-way edge in the system — frame goes up, a quality score comes back in real time, while the agent is still in the room. |
| 02 | Reconstruction | Job Queue, Stitching, Image Enhance (ext) | Async — a multi-room job runs past any HTTP timeout. Queue and stitching are owned; enhancement is where an external image model replaces what a hand-rolled pipeline would otherwise do. |
| 03 | Review | Viewer, Review Gate | The gate before money moves — nobody pays for a render they haven't seen. Reject re-queues reconstruction (the one retry loop). |
| 04 | Commerce | Pricing, Stripe (ext) | Quote computed deterministically from room/frame/area counts already on the manifest. Stripe owns checkout UI and receipts; a webhook is the only state change on our side. |
| 05 | Delivery | Share Page, Embed AI (ext), Listing Portal (ext) | Share Page is gated on status = paid. Embed AI is scoped to portal-specific setup instructions, not a general assistant. |
| 06 | Analytics | Analytics, Control Panel | Every visit + lead becomes an event; Control Panel is the per-agent rollup — visits, leads, timing, source portal. |
One Postgres schema (Supabase), replacing the current flat manifest.json +
JSONL-per-scan approach. agent_id ownership and a status lifecycle are the
two things that don't exist at all yet.
agents id, email, org, created_at
scans id, agent_id, status, created_at
frames scan_id, filename, heading, quality*
rooms scan_id, name, start_frame, end_frame
listings scan_id, title, address, price_quote
payments scan_id, stripe_session_id, status
leads scan_id, name, email, phone, status
analytics_events scan_id, event_type, ts, referrer
status: capturing → rendering → ready_for_review → approved → paid
*quality: blur, exposure_score, rotation_rate, delta_yaw
Not part of the diagram above — it's where the nodes run, not a data-flow node itself.
- Marketing site — static, stays on Vercel, unchanged.
- Product backend (Job Queue + reconstruction chain) — needs a persistent process and real compute time. Rules out serverless functions with ephemeral filesystems; needs a container host (Fly.io / Render / Railway) with the worker scaled independently from the web tier.
- Data layer — Supabase auth + schema
- Ingest & storage — object storage, upload path
- Status lifecycle + review gate — unlocks everything downstream that checks scan state
- Pricing + payment — Stripe, gated on status
- Embed gating — paywall in front of the share page
- Control panel rollup — per-agent view
- Reconstruction pipeline — worker/queue, can run in parallel with 1–6
- Infra split — move backend off single-process hosting
- Capture AI + Embed AI — the two genuinely new AI surfaces, once the pipeline they feed is stable
Names that came up in scoping that don't have a confirmed place yet:
Sigma, Admanage, Omni, Claude Commerse, Stich, Impeccable,
Leanxlnx, Sensor LLC, Graphify, SPAG-4D — flagged rather than guessed
into a slot. A repo link or one-line description per item would resolve these.
src/sodar/ is a small local harness that plugs candidate external providers in
behind one stable contract (sodar.providers.base) and evaluates them
deterministically against local fixtures — no network, no cloud. It runs on a
bare Python 3.11+ interpreter (python -m unittest discover -s tests); planning
and design notes live in wat/ and docs/.
sodar provider list
sodar provider run <provider> <fixture>
sodar eval run <provider> <fixture> # persists artifacts/evals/<run_id>/eval-result.json
Providers:
| id | dependency | notes |
|---|---|---|
dummy |
none | deterministic reference provider |
opencv-stitch |
opencv-python-headless (Apache-2.0), optional extra [opencv] |
OpenCV panorama stitching; output is not byte-reproducible — see docs/HARNESS_NOTES.md |
posed-stitch |
numpy, pillow (+ opencv-python-headless for ORB yaw refinement) |
Pose-guided equirectangular stitching from scanner frames + orientations — the Photo Sphere method (third_party/360-photo-app, MIT) in Python. Always produces a panorama; see docs/PANORAMA_STACK.md |
ai-fill |
network: OPENAI_API_KEY (GPT Image 2 edit) or a logged-in higgsfield CLI |
Fills the uncovered zenith/nadir bands and seams of a stitched panorama with GPT Image 2 |
psv-viewer |
optional local asset (src/sodar/viewer_assets/psv/vendor/fetch-vendor.sh) |
Photo Sphere Viewer static tour bundle (TD-003) |
Install an optional provider's dependency with e.g. pip install -e ".[opencv]".
The default install stays dependency-free. On macOS the system Python is
externally managed, so use a venv on a Python with OpenCV wheels
(python3.12 -m venv .venv && .venv/bin/pip install opencv-python-headless numpy pillow).
sodar stitch <frames-export.zip> --out artifacts/tours/<name> [--fill] [--width 4096]
/scan on sodar.io exports every captured frame plus frames.json (yaw,
elevation, roll, FOV per frame). stitch runs posed-stitch per room, optionally
ai-fill (GPT Image 2) to paint the missing ceiling/floor bands, links the rooms,
and writes a Photo Sphere Viewer bundle under <out>/tour/ — serve it with
python3 -m http.server. Verified on fixtures/valid/panorama-posed-001
(12 posed frames of a synthetic room): 40% coverage from one equator ring,
19.7 dB PSNR against the source panorama, and the tour opens in the viewer.