URL copied to clipboard
April 24, 2026 · 13 min read
Building P3 - Won't Fix — A Designer's Guide to Shipping a Real Web App
P3 – Won't Fix is public gallery of software bugs that I assume product managers closed as "won't fix." It's the kind of thing that makes you laugh, groan, and feel seen all at once. Anyone can submit a screenshot. If it's good, I'll publish it. Simple idea. Turns out, a lot to build.
I wanted to build something real. Not a tutorial project, not a mockup, but an actual web app that lives on the internet, accepts submissions, and doesn't fall over. I also wanted to understand how modern web apps are built. This is the story of what I learned.
The tools
Next.js: The app framework
Think of a framework like a set of rules and tools that give your project structure. Without one, you're making decisions from scratch: how do pages load, how does data get fetched, how do you handle forms, etc. Next.js makes those decisions for you.
It's built on top of React (the most widely used way to build interactive web interfaces), and it handles everything from routing to performance to deployment.
Why it matters for designers: Understanding that frameworks exist and that they impose structure helps you have better conversations with engineers. When a developer says "that's hard to build," it often means "that conflicts with how our framework expects things to work."
Vercel: hosting and infrastructure
Vercel is the company that makes Next.js. They also run the hosting platform where p3wontfix lives. Think of it like Squarespace for developers except instead of dragging blocks around, you push code and the site updates automatically.
Vercel handles: deploying the site, storing uploaded files, measuring performance, and more. Critically, it scales automatically so if the site suddenly got 10,000 visitors, nothing would break.
Why it matters for designers: When you hand off a design, it needs to get deployed somewhere. Knowing that Vercel exists (and how it works) helps you design for the real deployment environment.
TypeScript: safer code
TypeScript is JavaScript with rules. It forces you to say upfront what kind of data every variable holds. For example, "this is a list of strings," "this object always has a company name and an image URL." If you try to use data incorrectly, it tells you before the code even runs.
Why it matters for designers: TypeScript is why engineers catch a lot of bugs at design handoff time. When a design component can receive data in multiple shapes (what if the image is missing, what if the tag list is empty, etc.), TypeScript is what ensures all those cases are handled in code.
Tailwind CSS: styling system
Tailwind is a way of writing CSS by attaching small utility classes directly to HTML elements, instead of writing separate stylesheets. text-sm font-bold text-red-500 is shorter to write than creating a class called .error-label and writing the CSS for it.
It also ships with a built-in design system: consistent spacing, color palettes, typography scales, etc.
Why it matters for designers: Tailwind has become the dominant styling approach in modern React projects. Knowing that it exists explains why developer-built UIs often look more consistent than you'd expect. They are working from a constrained palette. It also means if you want to change a color or spacing value globally, there's usually one place to do it.
Vercel Blob: file storage
A "blob" in programming just means a chunk of data. Vercel Blob is a service that stores files (images, documents, JSON) and gives each one a public URL. Think of it like Dropbox for your app.
All the screenshots submitted to p3wontfix are stored here as is the list of published entries which is just a single text file called metadata.json.
Why it matters for designers: Storage is a real consideration in design. Where do uploaded files live? How big can they be? How fast do they load? Vercel Blob answers all of those for a project at this scale.
Resend: email delivery
Sending email from a website is harder than it sounds. If you just send from a random server, it may land in a spam folder. Resend is a service that handles the technical side of email delivery: authentication, reputation, and deliverability so you just write the content and call their API.
Every time someone submits a bug screenshot, I get an email with approve/delete buttons. Those buttons are built in code and not in an email marketing tool.
shadcn/ui: component library
shadcn/ui is a collection of pre-built interface components: buttons, forms, dialogs, dropdowns, etc. They are designed to be copied into your project and customized. Unlike most component libraries, you own the code and can change anything.
Why it matters for designers: This is the kind of component library a developer might reach for when building your designs. Knowing it exists (and what it looks like by default) can save you from designing components that conflict with what's already available.
What was built
The public side
- Feed: a scrollable gallery of published bug screenshots, newest first.
- Tag filtering: click a tag and the feed filters; the URL updates so you can share a filtered view.
- Submit form: anyone can drag-and-drop or paste a screenshot and fill in the details.
- About page: written in Markdown and rendered as HTML.
The admin side (password-protected)
- Login: sets a session cookie that lasts 30 days - Queue — a list of pending submissions; one click to approve or delete.
- Manage: view and delete published entries.
- Direct submit: I can publish directly without going through the queue.
How a submission flows
- Someone fills out the submit form and uploads a screenshot.
- Their submission is saved to storage and I get an email with approve/delete buttons.
- I click Approve in the email and it publishes immediately without entering a usernamd and password.
- The feed updates within seconds.
Intriguing decisions
No database
Most web apps store their data in a database, a structured system for querying and updating records. p3wontfix doesn't have one. All published entries are stored in a single text file called metadata.json. To add an entry, you read the file, add to the array, and write it back.
Why this works: For a personal project with one admin and low write frequency, a database is overkill. Removing it removes an entire category of infrastructure to maintain, secure, and pay for.
The tradeoff: This approach breaks down at scale. If multiple people were writing to that file at the same time, you'd get collisions. For p3wontfix, that's not a real concern.
What this teaches: The right tool depends on the scale and requirements. Over-engineering is a real cost; not just in time, but in complexity.
One file per submission in the queue
Early on, all pending submissions were stored in a single shared file. This caused a problem: when you overwrite a file on a CDN (a global network that caches files for fast delivery), the old version can hang around for a few seconds before the new one takes over. During that window, a submission might disappear from the queue or appear twice.
The fix: give every submission its own file. Files are created once and never overwritten; only deleted.
What this teaches: CDN caching is a real constraint that affects data design, not just frontend performance. Understanding how content gets cached and invalidated matters when you're designing systems that involve user-generated content.
Email buttons that work without logging in
When I receive a notification email about a new submission, the "Approve" and "Delete" buttons need to work with one click without my having to log in first.
The first implementation embedded my actual password in the URL. Convenient, but a real security problem. Anyone who saw that email (or an email log, or a forwarded message) would have my password.
The fix: instead of the password, each link contains a cryptographic signature, a one-way mathematical fingerprint generated from a secret key and the specific submission ID. The server verifies the signature when the link is clicked. If it matches, the action is performed. The password never appears anywhere.
Why this matters: This is a real security pattern used everywhere. Password reset links, unsubscribe links, email verification links, etc. They all use the same concept: a secret key signs a specific action, and the signature proves the request is legitimate without revealing the secret.
Performance
Performance isn't just about speed. It's about how the site feels.
The feed loads fast
Every time the feed page is requested, it needs to know which entries to show. Fetching that list from storage on every single visit would be slow. Instead, it's cached on Vercel's servers. The cache is cleared the instant a new entry is published, so the feed is always current.
What this teaches: Caching is one of the most powerful performance tools available, and also one of the easiest to get wrong. The key insight here is that the cache is invalidated precisely; only the data that changed is cleared, not everything.
Images don't cause layout shift
You've seen this: you're reading a page, an image loads, and everything jumps down. That's called Cumulative Layout Shift (CLS), and it's measured as part of Google's Core Web Vitals score.
The fix is simple: when an image is uploaded, store its width and height. When rendering the feed, pass those dimensions to the browser so it can reserve the right amount of space before the image loads.
Why this matters for designers: CLS is a design problem, not just an engineering problem. It's caused by images without known dimensions. If you're designing image-heavy layouts, you should know that dimensions need to be captured and passed through the system.
The first two images load immediately
The rest load as you scroll. This is called lazy loading, and it's the default behavior for web images. Overriding it for the first two entries means something is visible immediately, even on slower connections.
Upload progress feels... good
When you upload a screenshot, the button shows the upload percentage. But uploading a small image can happen so fast that the progress bar just flashes which feels broken. The animation was set to run for a minimum of three seconds regardless of actual upload speed. It's a small lie that makes the experience feel more trustworthy.
Why this matters for designers: This is a pure UX decision. The user doesn't benefit from knowing the upload took 0.2 seconds. They benefit from feeling like something happened.
Security
Security isn't just for apps with sensitive data. Any website that accepts user input, authenticates users, or takes actions on behalf of someone needs to consider this.
Passwords never appear in URLs
The original implementation sent emails with approve/delete links that looked like:
https://p3wontfix.com/api/queue/some-entry/approve?token=mypassword
This is a real vulnerability. Email gets logged. It gets forwarded. It gets screenshotted. If the password is in the URL, it's no longer a secret.
The fix uses HMAC (Hash-based Message Authentication Code) , a standard cryptographic technique. Instead of the password, the URL contains a signature:
https://p3wontfix.com/api/queue/some-entry/approve?token=3f9a8b...
The signature is generated by running the entry ID and the secret password through a one-way mathematical function. The server regenerates the same signature when the link is clicked and checks if they match. If someone intercepts the token, they can't reverse-engineer the password from it. The token only works for that specific entry and it can't be reused to approve something else.
Why this matters: This is the exact same pattern used by password reset emails, magic login links, and unsubscribe links across the entire internet. Understanding it means you can reason about whether features you're designing ("send a link to do X without logging in") are being implemented safely.
Sessions use httpOnly cookies
When you log into the admin area, the server sets a cookie (a small piece of data stored in your browser). This particular cookie is marked httpOnly which means JavaScript running on the page can't read it.
This protects against a class of attack called XSS (Cross-Site Scripting), where malicious code injected into a page tries to steal your session token.
Why this matters: The decisions designers make (like whether to show user data in the page, or where to put sensitive controls) affect the attack surface. Understanding these patterns helps you make better decisions at the design stage.
Testing
Testing might seem like a developer-only concern, but it directly affects what you can safely change after launch.
Two kinds of tests
Unit tests: test individual functions in isolation. Does the "filter entries by tag" function return the right results? Does the auth check correctly reject wrong passwords? These run in milliseconds and catch logic errors early.
End-to-end (E2E) tests: simulate a real person using a real browser. Open the page, click a button, check that the right thing appeared. These are slower but catch problems that unit tests miss (like a button that works in code but is hidden behind another element on screen).
p3wontfix has ~45 unit tests and ~48 E2E tests.
The hard lesson: environment states matter
Tests kept failing in confusing ways until one pattern became clear: always start from a clean slate. Specifically:
- Kill any server already running on the test port.
- Clear the Next.js build cache.
- Then run the tests.
A stale server serving old code will make tests fail in ways that look like bugs in your app when they're just bugs in your test environment. This is now automated: one command handles all three steps.
Why this matters: This is a lesson about reproducibility. The same thing applies to design. A prototype that only works in one specific browser state, or a user test that only succeeds with pre-loaded data, isn't a reliable signal.
What working with AI looked like
This project was built with Claude as a coding partner, and I have a few honest observations.
It's not magic. Claude writes code, but the decisions (what to build, what tradeoffs to accept, what to cut) still require a human. The AI is a very fast, very knowledgeable collaborator, not an autonomous builder.
Explaining concepts matters as much as writing code. The most useful thing wasn't "write this component." It was "explain why this is the right approach and what the alternatives are." That's what actually transfers as knowledge.
Tests reveal misunderstandings. Several times, tests failed not because the code was wrong, but because the expectation was wrong. A feature that seemed clear in a design turned out to have edge cases that required decisions. Tests force you to make those decisions explicit.
The feedback loop is everything. Build something small → see it work → build the next thing. Every time a step was skipped, problems compounded. This of course applies to design iteration too.
By the Numbers
- 5 pages (feed, submit, about, queue, manage)
- 8 API endpoints
- 15 components
- 45 unit tests
- 48 end-to-end tests
- 0 databases
- 1 JSON file as the entire data store
- 1 admin (sup)
Thank you
Thank you for joining me on this journey, and please start capturing those P3s at p3wontfix.com.