Skip to content
DoorstepOpen the demo

Cut turf.Knock every door.Count what happened.

A working demo of a voter contact tool for campaign field teams. Import a voter file, cut call lists and walkable turf, hand them to volunteers, and see every knock add up.

Open the demo

No sign in. 2,397 generated voters.

Sorry wemissed youWe’ll be back SaturdayReachedNot homeMovedLeft by Janae, P-03, 112 Cedar St
The Doorstep dashboard: contact rate, attempts per day by outcome, support IDs and contact rate by precinct
The dashboard, with the demo data loaded.

A canvass runs on lists

Volunteers need a short list in an order they can walk. Organizers need to know what happened at each door before the next shift. Here the voter file, every list cut from it and every logged attempt live in one Postgres database, so the dashboard counts the same rows volunteers saved.

  • Imports that explain themselves

    A bad row is skipped and named by line, like “Line 6: age must be at least 18”. The rest of the file still loads.

  • Turf in walk order

    Door lists sort by street, then house number. Phone lists leave out voters without a number.

  • Lists that hold still

    A list is a snapshot, so a new import never reshuffles the doors a volunteer is already walking.

  • Numbers you can trace

    Contact rate is conversations divided by attempts, by day, precinct and list, counted from the rows volunteers saved.

Every knock is logged once. Every number on the dashboard traces back to a door.

How it works

  1. Import the voter file

    Upload a CSV with precinct, age, party, phone and a support score. Fix the rows it flags, or leave them out.

  2. Cut a list and assign it

    Filter by precinct, age, score and contact history, watch the count change, then hand the list to a volunteer.

  3. Log each door in one tap

    The volunteer view shows one voter at a time: a support ID, not home or moved, and on to the next door.

Under the hood

The parts a reviewer would check first, all in the repository.

Read the code on GitHub
Plain SQL on Postgres
Every value is a parameter, including the filters that build a segment.
Rules in the database
A support ID needs a conversation, and a contact needs a voter who is on that list. Constraints enforce both.
Tests on every push
Unit, database and browser tests run in GitHub Actions against a real Postgres.
Agent context in the repo
A written plan, a CLAUDE.md with the rules, and skills for design and common changes.

Questions

Is any of this real voter data?

No. Every name, address and phone number comes from a script using Faker. Phone numbers use area code 555, which isn’t in service.

Do I need an account?

No. Open the demo and click around. There is no sign in, so whatever you change is visible to the next visitor.

Can I break it?

Edit anything you like. The data goes back to the starting set every night.

What can I try in five minutes?

Build a list for one precinct and watch the count change, assign it to a volunteer, open the canvass view and log a few doors, then check the dashboard.

What is it built with?

Next.js 16 and React 19 in TypeScript, Postgres with plain SQL, Tailwind, Vitest and Playwright. It runs on Vercel with a Neon database.

How was it built?

With Claude Code, from a written plan and a context file that sets the rules: parameterized SQL, validation at every boundary, a test for every behavior, fake data only. Both files are in the repository.

What would a real campaign need that this lacks?

Sign in with per campaign data isolation, turf cutting on a map, background imports for files with millions of rows, and an audit log.

Who made it?

I’m Jose Hernandez, a full stack engineer. I built this to show how I’d work on a voter contact tool.

See it with the demo data loaded

Nothing to install, and the data resets every night.

Open the demo

No sign in. 2,397 generated voters.