So it’s time to break the fourth wall a little bit. Hi, I'm Rachael, and I write for MLH, which, to be clear, means I write words, not code. My technical credentials begin and mostly end with the HTML and CSS I taught myself on LiveJournal sometime around 1999, when the height of web development was getting your blinking text and your tiled starfield background to coexist. Make your MySpace jokes now. So when I tell you this is the story of building my first real tool, please understand the comedy baked into that sentence: I am usually learning, researching, and writing what I hope are interesting and informative pieces (which hopefully this will be for someone equally new to or unfamiliar with software development), not the person who would normally be trusted near a repository.
I've written little scripts before. I've gone directly to the little HTML button in the WordPress editor when it just...won’t....work. I once, memorably, turned the homepage of an intranet site italic because IT wasn’t answering my question fast enough and I thought I could figure it out. But I had never built a tool – something a stranger could open, use, and walk away from with a thing they wanted. And it just happened that I was ready to write about vibe coding at the same time I clocked a functional tool we needed for events. So here’s how Claude and I built a photo booth for MLH.
What I Built
The problem I needed to solve was small and concrete, which in hindsight is the best possible kind of first project. Major League Hacking (MLH) runs a ton of events, and the idea was a browser-based photo booth: an attendee opens a page on their phone, snaps a selfie with the built-in (front-facing, mirrored) camera or uploads a picture of themself from their Photo Album, drops a branded event frame over it, pinches and drags to line it up, and saves the finished square PNG straight to their camera roll to post. Organizers, who are sometimes less technical than others, and sometimes a different person at each event, sign-in to a little gated panel and upload the transparent PNG frames ahead of time.
That's it. No accounts for attendees. No app store. Just a link.
Simple to describe is not the same as obvious to build, and the gap between those two is exactly where I learned the most.
Decision One: Where Does This Thing Even Live?
My instinct as a beginner was to reach for a framework, because that's what every tutorial does. Claude talked me out of it, and the reasoning stuck with me: for a tool that has to be reliable at a live event, run by whoever's around, the fewest moving parts wins. So the entire tool is one self-contained index.html with HTML, CSS, and JavaScript in a single static file, no build step, no dependency install, nothing to break the morning of the event. It drops straight into a GitHub repo and serves from GitHub Pages for free.
The first time I really felt the value of vibe coding was here. I didn't have to know that a no-build static site was the robust choice. I described the constraints (free, reliable, runs at an event, handed off to non-devs) and the right architecture fell out of the conversation. My job was to recognize the reasoning was sound, not to have memorized it.
Decision Two: The Photo Never Leaves The Phone
This was the technical bit I'm proudest of understanding, even though I didn't write it.
The frame is a transparent PNG. The selfie is whatever the camera grabs. To combine them, you draw both onto an HTML <canvas> – the photo underneath, the frame on top – and then export the canvas as a PNG. All of it happens in the browser, on the device. The attendee's face never gets uploaded anywhere. For an event full of people who didn't sign up for anything, that's not just a nice-to-have; it's a whole privacy story.
Today I leaned into making that canvas feel like an actual photo booth instead of a file picker. There's now a live front camera that opens right in the page and mirrors you like a selfie cam (So raising your left hand raises the left hand on screen. It’s a small thing, but weirdly important), with upload still there as a fallback for desktop or locked-down cameras. And lining the photo up is now a proper gesture: one finger to drag, two fingers to pinch-zoom, a slider for fine control, and the scroll wheel on a desktop. None of that changes the privacy story – it's all still just math on a canvas that never leaves the phone.
Once I saw it work, the concept of client-side compositing went from a phrase I would've nodded along to into something I could actually explain to someone else. That's the test, right?
Decision Three: A Backend That Doesn't Need A Server
Here's where I almost made a beginner mistake, and the conversation saved me.
My plan was to just commit the frame images into the repo. Easy! Except the person uploading frames won't always be me, might be non-technical, and absolutely should not have to learn git commit to put a PNG online before an event. So we needed a real "organizers upload frames, attendees see them" flow…which sounds like it requires a backend and sounds like it kills my lovely static site.
The thing that unlocked it: Supabase, called directly from the browser with a publishable key and access rules. Frames live in a Supabase Storage bucket. Anyone can read them (so attendees see the frame options); only a signed-in organizer can add or remove them. The whole thing is enforced by storage policies, which look like this:
-- Anyone can view/list the frames (so attendees see them)
create policy "Anyone can view frames"
on storage.objects for select to public
using ( bucket_id = 'frames' );
-- Only signed-in organizers can add frames
create policy "Organizers can upload frames"
on storage.objects for insert to authenticated
with check ( bucket_id = 'frames' );
Understanding that "static site" and "has a backend" aren't opposites was a real moment of clarity for me. The site stays a single file on GitHub Pages (there are no serverless functions of my own) and the database does the heavy lifting over the network. The publishable key is meant to be in the client; the actual security lives in those policies, not in hiding a string. Understanding that distinction (public key in the file = fine; database password in the file = never, ever) felt like leveling up.
I also turned off public sign-up entirely. Organizer accounts get created from the Supabase dashboard, so there's no "anyone can register and start deleting frames" hole, and no single person who's permanently the only one with the keys.
One more reliability detail I'm glad we added: the Supabase client loads from a CDN, and the page is written to degrade gracefully if that script doesn't load. If the network has a bad day, the booth still opens, the camera still works, and you just don't get the cloud frames. "Fails into something usable" is the bar for anything running at an event.
The Part Nobody Warns You About: The Boring Footguns
Vibe coding is great at architecture. It's also great at the small operational stuff I would never have thought to ask about, and which would have absolutely wrecked me live.
Two that I'm now never going to forget:
- Free Supabase projects go to sleep after about a week of inactivity. A photo booth that's been quiet since the last event will be paused, and you'll find out about it in front of a line of people. The fix is dumb and important: open the dashboard the day before any event and wake it up.
-
Cross-origin images can silently "taint" a canvas and make the saved photo come out blank. Because the frames now load from Supabase (a different origin than the page), pulling one onto the canvas and then exporting it can trip a browser security rule. The one-line fix, which Claude helped me wire in yesterday, is to request remote frame images with
crossOrigin = "anonymous"so the canvas stays exportable. I would never have guessed that on my own; it's exactly the kind of thing that works perfectly with local files and then breaks the moment real cloud frames show up.
Neither of these is glamorous. Both are the kind of thing that separates "it works on my laptop" from "it worked at the event." I wouldn't have known to care.
Saving The Photo: Download Vs. The Share Sheet
This was the change I was most surprised mattered to me. Originally the finished image just downloaded as a file, which is fine on a laptop but inconvenient on a phone, where a "download" disappears into some Files app limbo and isn't obviously in your photos.
So now the save button is smarter about where it's running. On a phone, it hands the PNG to the native share sheet (navigator.share with the file), which is the sheet that already has "Save Image" / "Add to Photos" plus Instagram, Messages, AirDrop, and everything else. The photo lands in the camera roll, or goes straight to a story, in one tap. On desktop — where that share sheet doesn't exist — it falls back to the plain download. If someone opens the share sheet and then cancels, it just quietly does nothing instead of double-saving.
The lesson I keep relearning: "make a file" and "give a person their photo in the place they expect it" are different problems, and the second one is the one that actually matters at an event.
Making The Tool Actually Feel Like MLH
A tool that looks like a default Bootstrap page undercuts the whole "official event booth" vibe I wanted. So the design leans into the real MLH palette (red #E73727, navy #122F4C, yellow #F8B92A) with a narrow red/navy/yellow tricolor bar under the header as a signature accent. Display type is Space Grotesk, body is Inter, both from Google Fonts.
There was a nice little development trick here too. Early on I had Claude generate a few built-in sample frames in code (a DEFAULT_FRAMES array) so the picker was never empty while I was testing and demoing. Today, now that the real "frames come from Supabase" flow works, I cleared that array out. The picker will only show actual event frames, and when none are uploaded yet it shows a friendly "no frames yet" message instead of nothing. Scaffolding that you delete once the real thing works is a pattern I want to remember.
(Note to self, and to anyone copying this: confirm logo usage against the actual brand guidelines before anything goes properly public. Using a logo and using it correctly are different tasks.)
Deploying, And The Surprisingly Nice PR Loop
Deployment was almost anticlimactic, which is the goal. Public repo, drop index.html at the root, flip on GitHub Pages from the branch, and a live URL appears at https://<username>.github.io/<repo>/. All of which, Claude walked me through.
The part that genuinely changed how I think about maintaining a project: I can keep editing it through Claude Code on the web, connected to the repo over GitHub. I describe a change in plain English like, "remove the three sample frames and make the picker show a friendly message when there are none,” and it opens a pull request. I review it, I merge it, and merging auto-redeploys Pages.
That review step matters. It's not "AI changes my live site." It's "AI drafts a change, I read the diff, and I decide." For someone like me, that's the perfect amount of training wheels: I'm still the one accountable for what ships, but I'm reading real diffs instead of staring at a blank file.
So is Vibe Coding "Real"?
Here's my honest take after my first one.
Vibe coding did not mean I didn't understand my project. If anything I understand this tool better than things I've tried to build before, even when I was trying to customize my LiveJournal profile page, because every decision came with a why I had to evaluate, instead of a copy-paste I never questioned. The split that worked for me: Claude handled the syntax, the boilerplate, and the operational gotchas; I handled the constraints, the tradeoffs, and the final call.
What I'd tell past-me, one week ago, staring at the empty repo:
- Pick something with hard boundaries. "Photo booth that downloads a PNG" is a real finish line. "A social app" is not.
- Make the AI explain the why, not just hand you code. The reasoning is the part you keep.
- Read the diffs. Merge the diffs. Own the diffs.
- Write down the boring operational stuff (the sleeping database, the cross-origin thing) the moment you learn it, because you will not remember it at 8am before an event.
It's one static file, a public key, and some storage rules. It is also the first thing I've made that someone I've never met can open, use, and walk away from with something they wanted. That feels like a real line crossed, and I crossed it faster, and with a better understanding, because I had a very fast, very patient pair to argue with. And ask around MLH because I’ve been grinning about my success for days. (Everyone has been very excited for me too.)
Next up is mostly content, not code: loading the real designed frames, provisioning organizer accounts in Supabase for whoever's running the next event and remembering to wake the database up the night before. The plumbing is in. Now I just need an event to take it to.
See the tool in action here: https://bookoisseur.github.io/EventPhotoSharing/
FAQ
What is vibe coding?
Vibe coding is building software by describing what you want in plain English and letting an AI like Claude write the actual code. You stay in charge of the decisions — the constraints, the tradeoffs, the final call — while the AI handles the syntax and boilerplate. It's less "AI builds it for you" and more "you have a very fast, very patient pair to argue with."
Can you really build a web app if you can't code?
Yes. I write words for a living, not code, and I built a working photo booth that strangers can open and use. You don't need to know how to write the code, but you do need to understand the decisions well enough to say yes or no to them. That part you can't outsource.
Do you need to know how to code to use Claude for a project like this?
No, but it helps to be willing to learn why. I didn't write the JavaScript, but I made Claude explain every architectural choice so I could evaluate whether the reasoning was sound. The reasoning is the part you keep; the syntax is the part the AI handles.
What did you actually build?
A browser-based photo booth for events: an attendee opens a link on their phone, takes a selfie or uploads a photo, drops a branded event frame over it, lines it up, and saves a square PNG to their camera roll. No app to download, no account to make. It's one self-contained HTML file with a Supabase backend, hosted free on GitHub Pages.
What tools did you use?
Claude (for the vibe coding), a single static index.html file for the whole front end, Supabase for the backend that lets organizers upload frames, and GitHub Pages for free hosting. Ongoing edits happen through Claude Code on the web, which opens a pull request I review and merge.
How do you pick a good first vibe-coding project?
Pick something with hard boundaries. "A photo booth that saves a PNG" is a real finish line you can actually reach. "A social app" is not — it has no edge, so you never feel done. A small, concrete problem is the best possible first project.
How much does it cost to build something like this?
Nothing, for a project this size. GitHub Pages hosting is free, and Supabase has a free tier that's plenty for serving small frame images. The one catch: a free Supabase project goes to sleep after about a week of inactivity, so you wake it up the day before you need it.
Is the photo private — does it get uploaded anywhere?
The photo never leaves the device. The selfie and the frame are combined right in the browser on an HTML canvas, and the finished image is exported locally. For an event full of people who didn't sign up for anything, that's the whole point.
Can a static site really have a backend?
Yes — "static site" and "has a backend" aren't opposites. The page stays a single file with no server of its own, and Supabase is called directly from the browser using a publishable key plus access rules. The security lives in those rules, not in hiding the key.



