Most QR code services charge $60-100/year. I thought I could build one that charges once. Here's how.
The problem
I was helping someone set up QR codes for a restaurant menu. They needed dynamic codes — the kind where you can change the destination URL after printing. Every service that offers this charges monthly.
That seemed wrong. A dynamic QR code is just a redirect with a database row behind it. The infrastructure cost is basically zero. Why does that need a subscription?
So I built HonestQR — $19 one-time for dynamic QR codes with scan tracking. Here's the technical rundown of how it works and what I'd do differently.
The stack
- Next.js (App Router) on Vercel
- Supabase for auth, database, and row-level security
- Stripe for one-time payments
- PostHog for product analytics
Nothing exotic. I wanted to ship fast, not build infrastructure.
How dynamic QR codes actually work
A dynamic QR code is really just two things:
- A static QR code that points to a short URL on your domain (e.g.,
honestqr.net/r/abc123) - A server-side redirect that looks up the actual destination in a database
The "dynamic" part is that you can change the destination without reprinting the QR code. The code itself never changes — only the database row does.
Here's the simplified redirect logic:
// app/r/[slug]/route.ts
export async function GET(
request: Request,
{ params }: { params: { slug: string } }
) {
const { data: link } = await supabase
.from('links')
.select('destination')
.eq('slug', params.slug)
.single()
if (!link) return NextResponse.redirect('/404')
// Fire and forget — don't block the redirect
trackScan(params.slug, request)
return NextResponse.redirect(link.destination, 307)
}
The 307 status code matters. A 301 tells browsers to cache the redirect permanently, which breaks the whole "dynamic" part. 307 means "temporary redirect — check back next time."
Scan tracking without slowing down the redirect
The tricky part is analytics. You want to log every scan (device, location, timestamp) without adding latency to the redirect. Nobody wants their QR code to feel slow.
I went with fire-and-forget: start the database insert but don't await it before sending the redirect response. The user gets redirected instantly, and the scan event gets logged in the background.
One gotcha: on Vercel's serverless functions, the execution context can be killed after the response is sent. If your tracking insert takes too long, it might get dropped. Vercel's waitUntil API handles this — it keeps the function alive until your background work finishes.
Supabase RLS: powerful but you'll get burned
Row Level Security was the right call for multi-tenant data. Each user only sees their own QR codes. The policies look something like:
CREATE POLICY "Users can view own links"
ON links FOR SELECT
USING (auth.uid() = user_id);
Where I got burned: error messages. When RLS blocks a query, Supabase returns {} — an empty object, not an error. My early code was showing {} as a literal error message to users. Not great.
The fix: check for empty/unexpected responses and show a human-readable fallback. Log the actual response shape to your analytics so you can catch weird edge cases.
One-time payments with Stripe
Most Stripe tutorials assume subscriptions. One-time payments are actually simpler:
- Create a Checkout Session with
mode: 'payment'instead ofmode: 'subscription' - Use a webhook to listen for
checkout.session.completed - Update the user's tier in your database when payment succeeds
No subscription lifecycle, no billing portal, no dunning emails. It's refreshing.
The business tradeoff is real though — no recurring revenue means I need a steady stream of new customers. But the product pitch writes itself: "why are you paying monthly for QR codes?"
What I'd do differently
Analytics from day one. I added PostHog late and missed early user behavior data. First 100 users are the most valuable to learn from, and I was basically blind.
Fewer features at launch. I built folder organization, bulk generation, and custom branding before I had 10 paying users. Should have launched with just dynamic links + scan counts and added the rest based on what people actually asked for.
Better onboarding. My biggest drop-off is between "created a QR code" and "paid for Pro." The free tier might be too generous — or the upgrade path isn't clear enough. Still figuring this out.
The numbers, honestly
A few months in:
- Organic Google traffic growing steadily
- A handful of signups per day
- Conversion from free to paid needs work
- Infrastructure costs: basically the Supabase and Vercel free/hobby tiers
It's not ramen-profitable yet, but the unit economics work. Each QR code costs me fractions of a cent in storage and compute. A one-time payment model works when your marginal costs are near zero.
Try it out
If you want to kick the tires: honestqr.net. Free static QR codes work without an account.
If you're building something similar with Next.js + Supabase, happy to answer questions in the comments.
Top comments (0)