Resident verification application

FDH Guard

Verifies new residents of a Frankfurt student residence before they receive the WhatsApp-group invite, replacing an Apps Script workflow no successor could maintain.

The problem

No durable email delivery, an occupancy model that could silently evict legitimate residents, and the WhatsApp invite link hardcoded in the script. Every change needed the original developer.

Screens

  • FDH Guard public homepage with the headline Welcome home. Let's get you verified. and a schematic of the residence campus
  • The How verification works section showing four numbered steps: tell us who you are, confirm your room, show you live here, we check and email you
  • Verification wizard step 2 with C033 entered in the room field and Room found displayed below it
  • 3D schematic of the FDH campus showing the houses as labelled blocks beside a list of the eight open houses with their floor and room counts
  • 3D room picker zoomed into the WG level showing individual room blocks for C033
  • Verification wizard step 3 showing the proof photo upload control and a privacy note about data retention

Public homepage

The public landing page opens with the residence name and the verification proposition, beside a schematic view of the campus houses.

How verification works

Four steps set the expectation before anyone starts: contact details, the room code, the nameplate photo, and a review by a real person. Nothing is published and no ID document is ever requested.

Room code lookup

Typing a room code resolves it against the unit inventory in real time. C033 resolves to a confirmed ground-floor room; the 3D picker button becomes available once a code is entered.

Campus overview

The optional 3D picker opens at campus level. Nine houses are drawn and eight are open for registration; Haus J joins them with 149 rooms in October 2026. An applicant who does not know their code navigates down to house, floor and room.

Room-level picker

After entering a code, the picker opens at the WG and room level, confirming the resolved unit in context. The 3D view is lazy-loaded so a failed WebGL environment never blocks registration.

Proof upload

Step 3 asks for a photo of the internal intercom nameplate directly outside the applicant's WG or room. A privacy note states how long the photo is kept and when it is deleted.

1 / 6

Features

Multi-step verification wizard

A four-step public form collects name, contact details, room code, and a proof photo. Each browser attempt carries an idempotency key so a dropped response can be retried without creating a duplicate application.

Durable email outbox

Every outbound message is written to a D1 table before it is sent. A scheduled Worker claims, delivers, and retries each message independently, so a failed send is observable and correctable.

Capacity-aware displacement

Approving an applicant compares the active resident count against the unit's configured capacity. Displacement fires only when the count would be exceeded, and only after an explicit confirmation naming who is affected.

3D spatial room picker

An optional Three.js campus schematic lets applicants navigate from house to floor to room. The picker is dynamically imported so WebGL failure on an older device never prevents registration.

In-app administration

Houses, units, admin roles, and the WhatsApp invite link are all editable in the admin UI. A rotated invite link or a new building is a settings change that any super admin can make without touching the database or the codebase.

Decisions and tradeoffs

Problems we hit

Room code collision

Problem

The v1 system normalised and zero-padded room codes, treating C33 and C033 as the same unit. C33 is the 3rd WG on floor 3; C033 is a distinct ground-floor room. Padding them together silently corrupted occupancy records.

Our solution

Digits are never padded or normalised

Codes are stored and matched verbatim. An exact lookup against the units table runs first; shape-based parsing fires only on a miss. The difference between C33 and C033 is preserved at every stage.

Silent household eviction

Problem

In the v1 system, approving a new applicant moved out every existing resident matching the room string. A shared family WG would silently evict a spouse alongside the departing student.

Our solution

Displacement requires capacity overage and explicit confirmation

Approval compares active resident count against the unit's configured capacity. Displacement fires only when the count would be exceeded, and only after a confirmation screen naming exactly who is affected.

Open-gate authorisation

Problem

The v1 authorisation check treated an unknown caller and an empty verifier list the same way: as permitted. Whether someone could approve an application depended on the state of a configuration sheet rather than on an explicit grant.

Our solution

No admins row means no access

Authorization fails closed at both layers: Cloudflare Access controls entry to the admin application, and the admins table controls what an authenticated person may do. An empty configuration grants access to nobody.

SQL-only invite link

Problem

WhatsApp group invite links are routinely rotated after spam or a group rebuild. In v1 the link was embedded in the script, so rotating it required a code edit and redeployment by the original developer. Every approval between the rotation and the fix sent a dead link.

Our solution

Every operational value is editable in-app

The WhatsApp invite URL is stored in a settings table and editable in the admin UI. A rotated link is a two-field update that any super admin can make without touching the database or the codebase.

Decisions we made

Room codes are stored verbatim, never normalised

Decision

Never pad or normalise room digits.

Why we made it

`C33` and `C033` identify different places: `C33` is the 3rd WG on floor 3 and `C033` is a distinct ground-floor room. Padding or case-folding them together silently corrupts occupancy history. The unit table is the authority on whether a code exists; the parser only handles input that produces no exact match.

Cloudflare over Supabase for seasonal usage

Decision

Cloudflare Pages and D1 were chosen over Supabase because Supabase's free tier pauses a project after roughly seven days without traffic.

Why we made it

FDH usage is seasonal, heavy at move-in and near-zero over summer, so every September the first admin would find a paused database. Cloudflare D1 never sleeps, and Time Travel gives point-in-time restore automatically, alongside a daily SQL export to R2 for portability.

The 3D picker is optional, never a requirement

Decision

The campus schematic is dynamically imported and always optional.

Why we made it

A five-year-old Android or a browser with WebGL disabled must never block registration. Typing a room code remains the primary and fully supported path; the picker is an enhancement for applicants who do not yet know their code.

Next

Contact

Let's talk.

For professional opportunities, collaborations, or selected freelance work.

Open to conversations Frankfurt, Germany Europe/Berlin