Public Report Page
The 30-second tenant flow is the single most important user experience in PMFriend. If tenants can't submit in under a minute, they call instead, and the whole triage flow collapses.
The URL
Every property has a stable token (UUID, never rotates) at:
https://app.pmfriend.com/report/{token}
You generate the URL once when the property is created, print it onto a QR sticker, stick it inside the fridge door or meter box. It's the only interface the tenant ever uses.
What the tenant sees
PMFriend — 45 Bourke Street, Melbourne
What's going on?
┌─────────────────────────────────────┐
│ Tell us what's happening. A few │
│ sentences is plenty. │
│ │
│ │
└─────────────────────────────────────┘
Photos (optional, up to 3)
┌──────┐ ┌──────┐ [+ Add another]
│ IMG │ │ IMG │
└──────┘ └──────┘
Your name [_______________________]
Email or mobile [_____________________] (we'll only use this to
get back to you)
[ Submit ]
Is it an emergency (gas, fire, flood, personal safety)?
Call 000 or your after-hours number immediately instead.
One textarea + optional photos + name + contact. No address dropdown (the URL already encodes the property). No category picker (Claude handles that, and it reads any attached photos alongside the text).
What happens on submit
- Creates a
MaintenanceRequestwithchannel=WEB_FORM, statusNEW. - If the contact email matches an existing tenant record, links to it. Otherwise creates a new tenant.
- Each attached photo (max 3, ≤ 8 MB each, JPEG/PNG/WEBP/HEIC) is
stored via
DocumentStorage(S3 in prod, filesystem in dev) and an index row is written tomaintenance_request_photos. - Fires a Hibernate post-insert event → PMFriend notification feed shows it immediately.
- The PM can now Suggest triage from the inbox — Claude reads the
text body PLUS up to 3 attached photos via Anthropic's vision content
blocks. The photos are forwarded as base64 image blocks alongside the
text in the same
/v1/messagescall. HEIC/HEIF photos are stored but not sent to Claude (it only accepts JPEG/PNG/WEBP/GIF).
Branding
Today the report page shows the agency name (if an agency.logoUrl is
set, it also renders the logo). No CSS customisation yet — the page is
intentionally minimal so tenants submit instead of getting distracted.
Why not an app?
Three attempts at tenant apps elsewhere in the industry. None got >15% tenant adoption. Reasons:
- Install friction. "Another app? For the one thing I do four times a year?"
- Onboarding friction. Email confirmation, password, terms, consent — 100% dropoff on a burst-pipe report at 11pm.
- Update friction. App store review delays, forced updates, version fragmentation.
A React Native tenant app is on the roadmap as an optional second channel, not a replacement. See Product Roadmap.
Rate limiting
Per-IP + per-token rate limits apply to the submit endpoint (5 submissions per token per hour). Trivially bypassable by a determined attacker, but good enough to block automated form-spam.
API
The submit endpoint accepts both multipart/form-data (preferred — for
photos) and application/json (legacy/no-photos):
POST /api/v1/report/{token}
Content-Type: multipart/form-data; boundary=...
--...
Content-Disposition: form-data; name="rawText"
The dishwasher is making a loud rattling noise...
--...
Content-Disposition: form-data; name="reporterName"
Alex Nguyen
--...
Content-Disposition: form-data; name="reporterContact"
alex@example.com
--...
Content-Disposition: form-data; name="photos"; filename="leak.jpg"
Content-Type: image/jpeg
<binary>
--...--
Returns { "requestId": "...", "message": "We've received...", "photosAttached": 1 }.
Photos served back to the PM-facing UI through:
GET /api/v1/maintenance-requests/{id}/photos → metadata array
GET /api/v1/maintenance-requests/{id}/photos/{photoId} → image bytes
Both require an authenticated PM session (RLS-bound).
See also
- AI Triage — what happens after submission
- Getting Started → Step 4 — how to print the QR sticker