← All work

Case study · Mobile / social · Own venture

DELULU

A location-aware social discovery app. You make yourself Findable for a while; if someone nearby is Findable too, you both get told there is an opportunity. Either can Wave, and only a mutual Wave opens a chat. The product needs to know where you are. Another user never does, and that rule shapes the architecture rather than sitting in a privacy policy.

Role
CTO
Build
In-house, end to end
Shape
Mobile app, API, admin console, marketing site
Data
Postgres with PostGIS, row-level security
Status
In build, not yet released
  • Expo
  • React Native
  • TypeScript (strict)
  • Hono
  • Zod
  • Supabase
  • Postgres
  • PostGIS
  • Row-level security
  • Next.js
  • React Query
  • Tailwind
  • Vitest
  • pnpm workspaces
  • GitHub Actions
The DELULU home screen while Findable, showing a countdown and the nearest opportunity
Findable, with the time remaining. The session expires on its own; there is no switch you can leave on by accident.
The DELULU home screen showing matches, a received wave and a blast-radius count
The same screen once something is happening: matches waiting, a Wave received, and how many people are in range. A count, never a map.
The DELULU people screen, with a wave to return and others inside the blast radius
Everyone in range, with a Wave to send or return. No distance in metres, no direction, and nobody can message you off the back of one.
The DELULU match screen, shown after both people have waved
Only a mutual Wave opens a chat. One Wave is not a match.

Selected work

What the build involved

01

A privacy rule the architecture has to honour

The product needs to know where you are to be useful. Another user does not. That single line is the constraint the whole system is built around, and it is enforced in five independent places rather than stated once in a document.

  • Coordinates live in exactly two tables. The table peers actually read has no geometry column at all, so there is nothing there to leak.
  • Consumer responses are assembled field by field rather than by spreading a database row, so a new column cannot silently reach a client.
  • Distance becomes one of four wide bands before it becomes a response. The API never sees a metre value for another person, which is what makes trilateration impossible rather than merely discouraged.
  • Remaining Findable time is rounded to five-minute steps, so polling repeatedly cannot read a precise clock.
  • Row-level security in Postgres backs all of it, so a mistake in application code is not sufficient on its own.
02

Business rules that can be tested without a database

Session expiry, consent, eligibility and the privacy projection are pure functions in their own package. The API applies them; the database enforces the ones that need a transaction or a unique index.

  • Branded IDs and discriminated unions in the type layer, so a session ID cannot be passed where a user ID is expected.
  • Zod schemas at both trust boundaries, validating what arrives and what leaves.
  • 493 tests passing, the fast ones needing no Docker at all, which keeps the loop short enough to actually run them.
  • Typecheck, lint, tests, the admin production build and the iOS bundle all clean in one command.
03

Four applications, one set of rules

A monorepo with a shared domain, so the phone, the API, the console and the marketing site cannot disagree about what a Match is.

  • An Expo and React Native consumer app, built for the App Store route rather than a wrapped website.
  • A Hono API holding every operation that carries a safety rule, deliberately separate from the client.
  • A Next.js operations and moderation console, which holds no privileged credentials of its own by design.
  • A static marketing site, and a design package whose palette and type scale are enforced by tests rather than by convention.
04

Safety as a feature, not a policy page

  • Chaperone: a human role built into the product and the console, not bolted on after launch.
  • Avatars are exchanged only after mutual consent, so a photograph is not the thing a stranger sees first.
  • Age assurance specified and recorded as a decision, with the provider left as an explicit open choice rather than a silent gap.
  • Every architectural decision written up with what was rejected and why, so a future change that weakens one is visible as a change to something named.

Across the build

How it was worked

  • Documented as it was built

    Specifications, architecture decisions and delivery state are in the repository beside the code. The decisions record what was rejected, which is the half that usually goes missing.

  • Honest about what is not done

    Error reporting, native builds and the age-assurance provider are open, and listed as open. Anything that needs credentials or a product decision is named rather than quietly deferred.

  • Nothing ships without saying so

    Deployment and store submission require explicit approval, written into the project's own engineering rules. The build being ready is not the same as the build being released.

Need an app built where the hard part is the rule, not the screens?