I built a raffle webapp for my kid’s school because the sign-up sheet wasn’t cutting it

Foreword

My kid’s school does a raffle fundraiser every year. My wife runs it. Kids get tickets, they choose which prizes to put them toward, someone does a drawing, everyone cheers. The problem was how they were running it: spreadsheets, paper tickets, and manually tracking who put what where.

One day my wife asked me if I could just make her a webpage to handle it. So I did.

The first version was rough. Basic. But it worked, and that was enough to start iterating. I roped in some friends to do test raffles, got their feedback, fixed things, rebuilt things, and kept going. Each version got better based on what people actually told me they needed, not what I assumed they needed.

It kept growing from there, and eventually I realized this could help other schools too. So now it’s a real product at simplyraffle.com. That was definitely not the plan when my wife asked me to “just make a webpage.”

The smart import

This one’s worth calling out because it solved a real pain point. Every school tracks their students differently. Some have spreadsheets with “First Name, Last Name, Email, Tickets.” Others have “Student, Parent Email, Grade, Allocation.” The column names are different, the order is different, sometimes there are extra columns that don’t matter.

The smart import handles all of it. You upload an Excel or CSV file in whatever format you have, and it figures out which columns are names, which are emails, which are ticket counts. No reformatting, no template to match. Just throw your spreadsheet at it.

For schools with multiple kids per family, it automatically groups shared email addresses so parents don’t get spammed with separate emails for each kid. That one came directly from a parent complaint during the first event.

How it works: the student side

Each student gets a magic link via email. No account creation, no passwords, no “forgot my login.” Just click the link and you’re in. They see their ticket balance and all the available prizes. Tap the plus/minus buttons to allocate tickets to whatever prizes they want. Change their mind? Move tickets around. It’s dead simple, which it needs to be because we’re talking about kids here.

The system won’t let them overspend. Once tickets are allocated, they can reallocate until entries close. After that, it’s locked.

How it works: the admin side

The admin panel is where the actual complexity lives. Coordinators can:

  • Add prizes with photos, descriptions, ticket costs, and winner limits
  • Import students via the smart import (or add them one at a time)
  • Track participation in real time, see who’s logged in, who’s spent their tickets, who hasn’t touched it
  • Send targeted emails (“last chance” reminders go only to students who haven’t picked yet)
  • Download a pre-draw backup of all entries for transparency
  • Run draws manually or on a schedule

The draw modes are the interesting part. There are three:

  • Open: everyone’s in every pot, more tickets = better odds, you can win multiple prizes
  • Fair First: prioritizes students who haven’t won anything yet before opening pools up
  • Exclusive: win once, you’re out of all remaining pools. Maximum spread.

Grand prizes always draw last, regardless of mode. The random selection uses a weighted algorithm (same standard as banking and password generation, not Math.random()).

The automated pipeline

This is where it gets a little over-engineered (in a good way). There’s a cron job that handles the entire end-to-end flow:

  1. When entry time closes, it locks submissions and sends notifications
  2. Students who never picked? Their tickets get auto-allocated proportionally across remaining prizes, and they get an email telling them what happened
  3. “Watch Live” notifications go out to everyone with a link to the results page
  4. The draw executes at the scheduled time
  5. Winner notification emails go out automatically
  6. Everything is idempotent, so if a step already ran, it skips it on the next cycle

This means the coordinator can set it up in the morning and walk away. The cron job handles closures, reminders, auto-picks, notifications, the draw, and winner emails without anyone touching it.

The seven emails

This grew from like two email types to seven, all because of real feedback:

  1. Raffle opening announcement with magic link
  2. Reminders for students who haven’t participated yet
  3. Last chance notification before entries close
  4. Watch live alert with link to the results page
  5. Entry confirmation after they’ve made their picks
  6. Winner notification with what they won
  7. Auto-assign alert confirming tickets were distributed for students who never picked

Every template is customizable with the organization’s logo and branding. Multi-child families get grouped messages.

Sending is throttled at ~2/second because email providers will absolutely rate-limit you if you blast 200 emails at once. Ask me how I know.

The live results page

This was probably the most fun to build. There’s a /results page meant to be thrown up on a projector during the drawing. Prizes reveal one at a time with a suspense animation, and winner names fade in with confetti. Grand prizes get an extended reveal. There’s even a skip button because kids are impatient.

If the page is already open when a draw happens, it auto-detects the new results and starts the reveal sequence. You can also share the URL so parents watching from home see the same thing. Pretty slick for a school raffle.

The safety stuff

Because I’ve been in IT for 25 years and I know what happens when someone accidentally clicks the wrong button: destructive actions require typed confirmation phrases. Want to delete all students? Type “REMOVE STUDENTS.” Want to factory reset? Type the phrase. No “are you sure?” dialogs that people click through on autopilot.

Magic links are also rate-limited on the click side. Kids (and parents) will click the same link five times if the page doesn’t load instantly, and each click triggers a token validation. Without rate limiting, that turns into a self-inflicted DDoS on your own auth endpoint. Learned that one during testing.

All participant data is isolated per event and auto-deleted when the event is over. No selling data, no remarketing, no ads. It’s a school raffle, not a data harvesting operation.

How it became a product

Once it was working well for our school, I started looking at what was already out there for other schools running raffles. And honestly? It’s rough. Most of the options are either overpriced platforms that nickel-and-dime you with per-ticket fees, or sketchy apps that harvest parent emails for marketing. These are schools and churches running fundraisers for kids. They shouldn’t need to worry about hidden fees or their families’ data getting sold.

I wanted to build something straightforward. Fair pricing, no upsells, no data harvesting, and someone who actually picks up when you have a question. Set up pricing tiers:

  • Community (Free): up to 50 participants, good for testing or small groups
  • Starter ($49/event): up to 200 participants, perfect for most PTAs
  • Pro ($99/event): up to 500 participants, for the big events
  • Annual plans for organizations that run multiple events per year

Every tier gets the same features. I personally handle setup for most signups, usually within hours. No ticket queue, no support bot, just me.

Tech stack

Next.js 14, TypeScript, Postgres via Prisma, Resend for email, hosted on Railway. The AI-assisted development thing applies here too. Claude Code handles the Next.js/TypeScript/React side while I handle the architecture, business logic, and the “what does a school coordinator actually need” decisions. I’ve never claimed to be a frontend developer.

Did it work?

First real test was 18 participants, 10 prizes, 2,014 allocated tickets. 100% participation, zero unspent tickets, no bugs during the live draw. The coordinator’s response was basically “wait, that’s it? It’s done?”

Yeah. That’s the point.

Now it’s a real product at simplyraffle.com and I’m deploying instances for other organizations. That’s actually what prompted the Railway provisioner in breadtools, because spinning up new tenants by hand wasn’t going to scale.

Leave a Reply