0038 — Meta glasses as a client-side capture UX for the existing turnover intake

Context

This is a thin client-side UX layer, not an architectural change. The entire backend pipeline this depends on already exists. Adopting Ray-Ban Meta smart glasses adds one input device and one PM-side iOS Shortcut on top of the unchanged Slice 3 condition-report intake.

What's already in the codebase (no change proposed by this ADR)

Surface File / location Status
Turnover entity (full lifecycle, conditionReportId field) src/lib/data/types.ts:2085 shipped
ConditionReport entity (lease-anchored, immutable; capturedVia: 'web' | 'voice' | 'sms' | 'system' is already metadata, not a type discriminant; photos: ConditionPhoto[] are S3 keys in the evidence bucket; draftProjection + frozenProjection for the Slice 3 LLM projection and PM-approval freeze) src/lib/data/types.ts:1986 shipped
Clara turnover agent agents/clara/lib/agent/clara-turnover.ts (156 lines) shipped
Voice-agent intake (the turnover-intake ElevenLabs/Clara conversation surface for the move-out walk) agents/clara/lib/voice-agents/turnover-intake.ts (136 lines) shipped
Inspection tools (start_inspection stamps Turnover.conditionReportId, append_observations writes notes idempotently, the inbound-MMS accrual path attaches photos to the draft report) src/lib/tools/turnovers.ts shipped
Inbound Twilio webhook (voice + MMS) src/app/api/twilio/webhook/route.ts shipped
Turnover UI (list + detail) src/app/(workspace)/(operations)/maintenance/turnovers/page.tsx and [id]/page.tsx shipped

A PM today can already do the full walk by voice call + SMS-with-photos: dial the turnover-intake line, narrate, text photos in, watch them attach to the draft ConditionReport. The glasses change only how the PM performs that exact same flow.

What this ADR proposes (the entire delta)

  1. One iCloud Shortcut, hosted at iCloud Shortcuts (versioned by replacing the link, not by repo). Triggered by an iOS Personal Automation on Bluetooth pair with Ray-Ban Meta. Action: poll the iOS Photos library filtered on Ray-Ban Meta EXIF metadata, send each new photo as an MMS to the existing turnover-intake inbound number. That is the whole client.
  2. One screen-recording / onboarding doc showing the ~60-second setup (Shortcut add + Personal Automation + Photos/Messages permission grants).
  3. No new code in this repo. No new entity. No new agent. No new tool. No new endpoint. No schema migration. The MMS lands on the same webhook that already accrues photos onto the draft ConditionReport.

The user-stated UX, mapped to the existing pipeline:

PM action What the existing system does (no change)
"Hey Meta, call <turnover line>" Glasses become BT headset; existing turnover-intake voice agent answers via Twilio
Walks and narrates findings append_observations writes notes to the draft ConditionReport
Presses temple button to capture photo Photo lands in iOS Photos when glasses sync (see "Folded-only sync" below); Shortcut sends as MMS; existing webhook attaches to the draft report
Folds glasses, ends call Twilio call terminates; existing intake handler closes the session; draft report ready for PM-approval freeze

Hard constraints from the user

Research summary — Meta wearables developer surface (mid-2026)

Critical finding — folded-only photo sync

From the official Meta help docs, stated consistently:

"Auto-import will occur when your glasses are switched on and are in the charging case or their hinges are closed, and your phone's Bluetooth and Wi-Fi are on."

Photos do not sync to phone while the PM is wearing the glasses. They buffer on the glasses (up to 500 photos / 25 30-second videos per charge) and only transfer over the glasses' temporary Wi-Fi when the hinges close. The V1 Shortcut therefore picks up photos in a batch when the PM folds the glasses, not mid-walk. ADR-0034 Slice 3 makes no real-time requirement; the PM-approval freeze runs after capture either way.

Decision

Adopt a two-tier rollout, both tiers under ADR-0034 with zero backend changes.

V1 (2-day delivery): iOS Shortcut → existing inbound MMS pipeline

V2 (post-MVP, gated on Meta DAT preview admission): real-time photos via DAT BLE

Entity classification

This ADR proposes zero new export interfaces. The entire change is a client-side capture UX producing existing ADR-0034 entities. Per ADR-0027 the classification table is therefore omitted.

Consequences

Easier

Harder

Follow-up work

Open questions to verify on real hardware

  1. Exact EXIF camera-make / camera-model strings on Ray-Ban Meta captures — determines the Shortcut's Find Photos filter. ~5 min.
  2. iOS Shortcut "Send Message" auto-send without per-message confirmation for MMS — if iOS requires confirmation per send, fall back to HTTP multipart POST to the existing turnover endpoint. ~15 min.
  3. Personal Automation reliability with "Run Immediately" on Bluetooth connect — 10 pair/unpair cycles in mixed states. ~30 min.
  4. Shortcut polling loop survival over a 30-min walk — phone in pocket, screen locked, voice call active. Measure termination latency. ~45 min.
  5. End-to-end latency from "glasses folded" → photo in iOS Photos library — determines staleness of report finalization. ~10 min.
  6. (V2 scoping) DAT session.photoDataPublisher emission on user-pressed temple-button — ~2h with DAT preview slot + minimal sample app.

Update — 2026-05-28: hardware & SDK research pass

A follow-up research pass (web verification against Meta's developer + consumer docs, prompted by field-testing the manual flow — a full turnover by voice call with stills texted in worked end to end) confirmed the load-bearing limitation but surfaced one stale framing, two missing hardware constraints, and two better no-app transport options. The V1/V2 decision above stands; this section amends the supporting detail and corrects a few claims.

Confirmed

New constraint the original ADR missed — the Web Apps path has no camera access

On 2026-05-14 Meta opened the Ray-Ban Display glasses to third-party developers with two build paths. The two are not interchangeable for our purpose:

Path Camera / captured photos Motion · GPS · Neural Band · local storage In-lens UI
Web Apps (HTML/CSS/JS, deploy-by-URL, no app store; starter kit is explicitly Claude-Code-friendly) none
Native DAT (iOS Swift / Android Kotlin)

Camera and photo/video capture are native-DAT-exclusive. A Display Web App cannot reach the camera or the phone's photo library, so the tempting "host a TypeScript page in-lens that grabs the last capture and POSTs it" pattern is not buildable as a Web App — anything touching a photo needs the native companion (full app + preview admission + per-phone install). The Web App's real role here is a heads-up display surface (in-lens turnover checklist, "photo received ✓", which-unit status, driven by Neural Band / GPS), not a photo pipe. Recorded so the camera-less fact isn't rediscovered the hard way. Still hardware-gated.

New constraint — the microphone is single-owner; no video while on the Clara call

The glasses' mics are committed to the call audio while the PM is on the turnover line (glasses = BT headset). Per Meta's FAQ:

Net: stills via the temple button are the only capture that runs concurrently with the call — the medium V1 already uses. Video buys nothing (same folded-only sync, heavier to move, on-glasses clip-length cap).

Better no-app transport options for V1 (supersedes "Shortcut → MMS" as the only path)

All three need no native app and all three inherit folded-only batch (the photo must reach the iOS camera roll first). The ADR previously named only the Shortcut:

  1. iOS Shortcut → MMS or HTTP POST (original V1). Mechanics below. Weakness: iOS background-execution reliability + a non-shareable automation (see Corrections).
  2. Cloud-backup webhook (recommended for a real rollout). PM enables Google Photos / Dropbox / iCloud auto-backup of the camera roll (an OS-level daemon, far more reliable than a Shortcut). A server-side watcher we own (Google Photos Library API, or Dropbox webhooks + longpoll) pulls new Ray-Ban Meta captures filtered by EXIF and feeds the existing accrual path. No phone app, no per-phone automation wiring, scales to unlimited PMs, watcher lives on our infra. With company-managed backup accounts there is no personal-privacy issue. Caveat: this is the one option that is not strictly zero-backend — it adds a small server-side watcher + an OAuth connection — but it is reusable and off-device, and it is the most reliable path to scale.
  3. Off-phone Mac watcher. iCloud Photos syncs the camera roll to a company Mac; a launchd folder-watcher there POSTs new captures. Watcher entirely off the PM's phone.

The capability triangle (pick two)

{ real-time · hands-free · no app } — any two, never all three:

How the iOS automation actually works (detail behind option 1)

Corrections to original assumptions

Open questions — status after this pass

Update — 2026-05-30: V2 native DAT proven end-to-end on real hardware

The V2 native DAT app was built and validated end-to-end on a real Ray-Ban Meta (Gen-2) — capture → upload → attach to a turnover, all confirmed server-side. Repo: PropFlow-Technologies/propflow-smart-glasses (native iOS Swift, XcodeGen, DAT 0.7.0). This resolves the load-bearing V2 question and corrects two assumptions in the original ADR.

Proven (on hardware)

Q6 answered — the temple button does NOT feed the app

A user-pressed temple/capture photo does not emit on photoDataPublisher; it goes to the native Meta gallery / iOS Photos (momentsd activity, zero stream events on press). DAT 0.7.0 exposes no hardware-button, touchpad, or gesture API at all (verified across the full MWDATCore + MWDATCamera surface). The only capture trigger available to an app is programmatic capturePhoto(). So real-time + hands-free via the hardware button is impossible in 0.7.0; the capability triangle's "hands-free" leg now requires a built trigger (a phone-side voice command), not the temple button.

Video & audio — what DAT 0.7.0 actually exposes

Config gotchas that blocked it (for whoever picks this up)

Three of our misconfigurations — not the glasses, not attestation, not a paid account — produced a generic Meta "internal error / no callback" for hours:

  1. Contradictory MetaAppIDMetaAppID = 0 at the plist top level and a non-zero production ID inside the MWDAT dict silently forces the production-attestation path a free sideload can't pass, because the SDK reads only the MWDAT dict. Fix: put all DAT keys exclusively in the MWDAT dict, with one consistent value.
  2. Missing CFBundleTypeRole = Editor in CFBundleURLTypes — without it the <scheme>:// callback can't route back, so registration completes on Meta's side but never returns to the app.
  3. Permission-flow hangsrequestPermission() throws requestTimeout and checkPermissionStatus() hangs until the glasses link is live. Detect the grant off the .onOpenURL callback (permission_granted=true) and timeout-guard every SDK call. Also: the glasses must be actively connected (out of the case, awake) in Meta AI before the camera prompt appears.

Net effect on the decision

V2 is no longer "post-MVP, admission-gated, may collapse." It is proven and buildable now for real-time capture — minus hands-free-via-hardware-button, which 0.7.0 cannot do. The turnover flow's remaining gaps are product work, not platform unknowns: (a) point the app at production + a real turnover + a device service-token; (b) a phone-side voice-notes layer (speech-to-text on the BT-mic) to replace the V1 voice-call narration; (c) an optional voice-command shutter for hands-free. V1 (Shortcut / cloud-backup) remains the zero-app path for PMs who won't install an app; V2 is the rich path for a managed fleet.

Alternatives considered

"Hey Meta, send <turnover line> a photo" per shot

Real-time send via the contact's WhatsApp / iMessage path. Rejected — per-photo voice violates the stated UX constraint.

DAT-native iOS app with BLE photo capture as V1

Deferred to V2. Requires DAT preview admission plus a TestFlight install on the PM's phone. Out of the 2-day budget; violates "no third-party app." Same backend contract as V1, so it swaps in cleanly later. Distribution reality (2026-05-28): no public App Store publishing during preview — install is via TestFlight / release channel, capped at ≤100 testers (per Meta's DAT FAQ), and TestFlight builds expire after 90 days, forcing re-install (Apple TestFlight). Fine for a pilot, painful as a real deployment until Meta opens open publishing. A minimal SDK-glue snippet looks small, but the real project scope — Xcode project, Apple Developer account, code signing, preview admission, and per-phone install — is far larger than any minimal code sample implies.

Display Web App as the photo capture / upload surface

A standalone in-lens Web App (HTML/CSS/JS, deploy-by-URL) that the PM invokes to send the last capture. Rejected — Display Web Apps have no camera or photo-library access (native-DAT-only; see Update — 2026-05-28). The Web App can render a HUD (checklist, status, "photo received ✓") but cannot reach a photo to upload, so it cannot be the transport. Held as a companion display surface for after Display hardware is in inventory, not a capture path.

Cloud-backup-account watcher (Google Photos / Dropbox / iCloud) → server-side webhook

Promoted, not rejected — see Update — 2026-05-28, "Better no-app transport options," option 2. The strongest no-app path for a real rollout: OS-level backup of the camera roll + a server-side watcher we own. Listed here for discoverability; it is the recommended V1.5 transport once past the 2-day MVP.

WhatsApp / Messenger video call with frame extraction on our backend

Continuous video stream via the Be-My-Eyes pattern; backend extracts frames on cue. Rejected — WhatsApp Business API does not accept inbound video calls; receiving them requires a headless WhatsApp account (Baileys-style), against WhatsApp ToS.

Facebook / Instagram Live → Live Video Graph API ingestion

Rejected — voice-triggered start, the stream lives in Meta's infrastructure, frame-extraction adds latency, and PMs would not livestream to a Meta-branded surface for property work.

Dedicated PropFlow work iPhone on tool belt

$60 used iPhone SE per property in kiosk mode, app pre-loaded; PM's personal phone uninvolved. Held as fallback if the Shortcut path proves unworkable in field testing.

Voice-only V1 with no photo channel

PM dictates findings with enough detail that no images are needed. Rejected — ADR-0034 Slice 3 mandates voice/photo-native capture from day 1; photo evidence is the most decisive piece in deposit disputes (Slice 4).

A new TurnoverFinding entity (initial draft of this ADR)

The first draft of this ADR proposed a new TurnoverFinding shape with source: 'glasses-voice' | 'glasses-photo'. Rejected during review — ADR-0034 already settles the capture data model. ConditionReport.capturedVia is already 'web' | 'voice' | 'sms' | 'system' — capture-source is metadata, not a type discriminant. Glasses produce voice and sms captures into the existing pipeline; no new entity, no new enum value.

References

Primary sources

Added in the 2026-05-28 research pass

Internal references