Portfolio · Product case study
Bungalow Match — a bilingual lead-generation platform for an Ottawa real-estate practice
A production web application that captures seller and buyer enquiries, notifies the agent instantly, measures how visitors actually behave, and gives the owner a private dashboard to work from.
Public pages
Home, promise, 3 neighbourhood pages, 2 campaign pages
Private area
Auth-guarded admin + analytics dashboards
Languages
English and French campaign routes
The problem
A real-estate practice works with buyers who are already looking for a specific type of home — a bungalow, in a specific part of Ottawa, in a specific price range. The matching homeowners are not on the market yet, so there is no listing to reach them through.
The site had to make a low-commitment first step possible: a homeowner checks whether their home matches a buyer who exists today, without committing to list. Everything else — storage, follow-up, measurement — had to happen automatically behind that single step.
Who it is for
Bungalow owners
Considering a sale, possibly months away, wary of pressure.
Bungalow buyers
Registering their criteria so they hear about homes before they list.
The agent
Needs each enquiry in an inbox immediately, and in one private dashboard.
The main journey
One path, five steps, no dead ends.
- 1
Arrive
From an ad, a campaign link or a neighbourhood page.
- 2
Understand
Video, buyer matches and a plain-language explanation of how matching works.
- 3
Submit
A short seller or buyer form with validation and clear grouping.
- 4
Confirm
A dedicated confirmation page states what happens next.
- 5
Follow up
The submission is stored and emailed to the agent instantly.
Interfaces from the live product
Screens captured from the application itself. Private dashboards are described rather than shown, so no client data appears here.

Main landing page
Conversion-oriented home page: hero with video, newsletter capture, buyer matches, how it works, seller and buyer forms.

English campaign page
/we-need-a-bungalow — a paid-traffic landing page with its own video, poster, copy and dedicated lead form.

French campaign page
/nous-cherchons-un-bungalow — the French counterpart, fully localized: copy, metadata and form, not a machine translation.

Content page
/our-promise — long-form content page with a dedicated typographic layout and scroll reveal animations.
What is built
Each item below corresponds to functionality present in the codebase.
Lead capture, two audiences
Separate seller and buyer flows, each with client-side validation, multi-select preferred areas, timeline selection and a dedicated confirmation path (/thank-you).
Persisted submissions
Submissions are written to the project database (seller_leads, buyer_leads, newsletter_subscribers) with row-level security policies applied per table.
Transactional notifications
A server function formats each submission into an HTML email and sends it through Resend, so a new lead reaches an inbox without opening the dashboard.
First-party analytics
A custom tracker records page views, section dwell time, scroll depth, clicks, exits, device type and UTM parameters through a public API route with schema validation.
Authenticated admin area
Routes under /admin and /analytics sit behind an auth guard; an admin role stored in a dedicated user_roles table gates sensitive operations server-side.
AI analytics summary
An admin-only server function sends aggregated (non-personal) metrics to an AI gateway and returns a structured French CRO read-out.
Config-driven neighbourhood pages
Three neighbourhood landing pages are generated from a single typed configuration object and one shared template, instead of three duplicated pages.
Bilingual campaign pages
Parallel English and French campaign routes with their own videos, posters, metadata and form copy — built for paid traffic in an Ottawa bilingual market.
Measurement
Analytics, audit and insight — built in, not bolted on
The product does not rely on an external analytics tag. A purpose-built pipeline records how visitors behave, filters out the team's own traffic, and turns the data into decisions through a private dashboard and an AI-assisted audit.
Dwell time per section
An IntersectionObserver-based tracker measures how long each section of a page stays on screen, so the owner can see which messages actually hold attention.
Scroll depth and exit points
Each session records how far the visitor scrolled and where they left, exposing the exact point in the funnel where interest drops.
Click and CTA tracking
Interactions with calls to action are captured per page, making it possible to compare which copy or placement earns the click.
Source and campaign attribution
UTM parameters are stored with every event, so ad campaigns can be compared on real on-site behaviour rather than platform-reported clicks alone.
Device and mobile performance split
Events carry device type, letting the dashboard compare mobile versus desktop engagement — critical when most ad traffic arrives on phones.
Clean data by construction
Local, preview and editor traffic is filtered server-side before any event is stored, so reported numbers reflect real visitors, not the team testing the site.
Admin analytics dashboard
A private, auth-guarded dashboard aggregates events into readable metrics: page views, sessions, dwell time, exits and devices, filterable by page.
AI-assisted audit
An admin-only function sends aggregated, non-personal metrics to a hosted AI gateway and returns a structured French CRO read-out — an automated audit of what the data says.
Outcomes
What the project delivers
Stated as capabilities present in the shipped product — not projections or invented figures.
A live, measurable funnel
Every public page feeds the same measurement pipeline, so decisions about ad spend and page changes can be based on recorded behaviour instead of opinion.
Zero manual lead handling
From form submission to database row to inbox notification, no human step is required — a lead cannot be forgotten in a form inbox.
Landing pages on demand
A new neighbourhood or campaign page is a configuration entry, not a development task — the marginal cost of testing a new audience is close to zero.
Bilingual reach from one codebase
English and French audiences are served from parallel routes sharing the same infrastructure, doubling market coverage without doubling maintenance.
An auditable system
Row-level security, a dedicated role table, server-side validation and server-held secrets mean the data path can be reviewed and reasoned about end to end.
Architecture
- Framework
- React 19 + TanStack Start (SSR) on Vite
- Routing
- TanStack Router, file-based routes with an authenticated layout
- Data
- TanStack Query for reads, server functions for privileged writes
- Styling
- Tailwind CSS v4 with a semantic design-token theme
- Backend
- Managed Postgres with row-level security and typed client
- Resend HTTP API, called from a server function
- Analytics
- Custom event tracker + public validated ingest route
- AI
- Hosted AI gateway, admin-only, aggregated metrics as input
How a submission flows
Step 1. A visitor submits the seller, buyer or update form in the browser.
Step 2. The values are validated, then written to the matching database table under row-level security.
Step 3. A server function formats the submission and sends it as an HTML email through Resend.
Step 4. The visitor lands on a confirmation page; the agent sees the lead in the private dashboard with read / unread state.
Step 5. In parallel, anonymous behaviour events are validated and stored so the funnel can be measured.
Decisions that shaped the build
One template, many landing pages
Neighbourhood variants read from a typed configuration file (slug, hero copy, buyer-match title, SEO). Adding a neighbourhood is a data change, not a new page to maintain.
Server functions instead of client secrets
Email sending and AI calls run in server functions, so API keys stay server-side and are read at call time rather than embedded in the browser bundle.
Role check in the database, not the UI
Admin status is resolved through a database role table and a security-definer function, so hiding a button is never the only thing protecting an action.
Analytics that exclude our own traffic
The tracking endpoint filters local, preview and editor hosts before storing an event, keeping reported numbers closer to real visitor behaviour.
Validation at the boundary
Incoming analytics payloads and admin AI requests are parsed with schemas that cap array sizes and string lengths before anything is written or forwarded.
Per-route metadata and a generated sitemap
Each public route defines its own title, description and social tags; a sitemap route and robots rules keep admin, auth and confirmation pages out of search.
Savoir-faire
Skills demonstrated
Each skill below is backed by a concrete, inspectable part of this project.
Product design
Turning a broker's offline matching process into a guided web funnel.
UI craft
A single design-token system (navy, gold, cream) applied across every page, with responsive layouts from mobile to wide desktop.
UX writing & structure
Progressive hero sequence — headline, call to action, video proof, buyer matches, call to action — and forms broken into readable groups.
Full-stack development
Routing, SSR constraints, server functions, schema design, RLS policies and typed data access.
Analytics engineering
Custom event pipeline: dwell time, scroll depth, exits, devices, UTM attribution — with internal traffic excluded server-side.
Automation
A submission triggers storage and an outbound notification with no manual step.
Integrations
Auth, database, transactional email and an AI gateway wired into one application.
Problem solving
Video encoding for cross-browser playback, internal-traffic filtering, and duplicated pages replaced by a configuration-driven template.
Business value
- Enquiries are captured and delivered without manual handling, so nothing depends on someone checking a form inbox.
- Campaign and neighbourhood pages can be launched from configuration, which shortens the time between an idea and a live landing page.
- Behaviour data — dwell time, scroll depth, exits, sources, devices — gives an evidence base for ad spend instead of guesswork.
- Bilingual routes let the same offer run to English and French audiences in the same market.
- Access controls and validation keep enquiry data restricted to the account holder.
My role
I designed and built the application end to end: the page structure and visual system, the forms and their validation, the database tables and access rules, the email notification flow, the analytics tracking and dashboards, and the authenticated admin area. Work was iterative, driven by review feedback on layout, copy and responsiveness.