Last year, all I wanted was a QR code for a birthday present. Scan the code, get the surprise. Thirty seconds of work, I thought.
Not quite.
Every single generator wanted me to create an account first — for what is, technically, a black-and-white square with some data encoded inside. The ones that skipped the sign-up put a watermark on the download or hid the decent export quality behind a paywall. And the moment I looked at dynamic QR codes — the kind where you can change the target URL even after the code is printed — every site wanted a subscription.
I got the birthday present done in the end. But the experience stuck with me, and I did the thing you should probably never do with a minor annoyance: I turned it into a side project.
The "free QR code" business model
Once you look closer, the strategy behind most QR generators is remarkably consistent:
- You google "free QR code generator" and click one of the top results.
- You generate a code — often a dynamic one by default, whether you notice or not. It doesn't point to your website; it points to their redirect service.
- You download it and print it on flyers, menus, packaging, a gift.
- The trial ends. The redirect now leads to an upgrade page.
- Your printed material is now a billboard for someone else's pricing page. Your options: pay, or reprint everything.
None of this is illegal — it's all in the fine print. Which somehow makes it worse. It works precisely because most people don't know the difference between a static and a dynamic QR code. The switching cost isn't technical, it's physical: ink on paper.
A dynamic QR code is a 302 redirect
Here's the part that bothered me as a developer.
A static QR code is just data encoded into pixels. The URL (or Wi-Fi credentials, or contact card) lives inside the image. It works forever, nobody can turn it off, and it costs practically nothing to generate.
A dynamic QR code is a static QR code that points to a URL shortener:
Static: [QR] ──encodes──▶ https://your-site.com
Dynamic: [QR] ──encodes──▶ https://short.link/u/abc123 ──302──▶ wherever you want (editable)
That's the whole product. The premium feature that costs $15 a month in many places is a database row and an HTTP redirect.
To be fair: running servers, analytics dashboards and support costs real money. But the gap between what this costs to operate and what's being charged — combined with the expiry tactics — was big enough that I wanted an alternative to exist.
QRcodly
QRcodly is an open-source (MIT) QR code generator and management platform. The free tier is actually free:
- Unlimited static and dynamic QR codes, unlimited scans
- Custom styling, colors and logo uploads
- Scan analytics
- A built-in URL shortener
- No credit card, no expiry tricks
How does it stay sustainable? There's a Pro tier with business features: your own domain on short links, REST API access, bulk imports, third-party integrations. Companies pay for company features. Individuals don't pay ransom for a redirect.
And because it's MIT-licensed, the "no tricks" promise isn't a marketing slogan — it's enforceable. If I ever pull an expiry stunt, you can fork the repo and self-host it. The license is the guarantee.
Why not just Next.js?
Fair question. A QR code generator is mostly forms, and Next.js alone would have carried that just fine.
Two things made me set it up differently. First, I knew early on this wouldn't stay a website. I wanted a browser extension, and at some point a desktop app — so I committed to TypeScript everywhere: one language, and more importantly one set of types, across every surface.
Second, some of the work simply isn't request/response-shaped. Rendering styled QR codes server-side (jsdom + resvg + sharp), running scheduled jobs, reacting to things like "user deleted" or "subscription past due" — for that I wanted a long-running server I fully control, not API routes that time out mid-render. So the backend is a separate Fastify REST API. And honestly, there was a third reason: I'd heard a lot about Fastify's performance and wanted an excuse to finally try it. (Verdict: the hype is justified.)
One thing I deliberately avoided: Kafka, RabbitMQ, or any external message broker. At this scale that's operational overhead with no payoff — and every extra piece of infrastructure makes self-hosting harder, which actually matters for an open-source project. Instead I built a small event system straight into the backend: a decorator-based emitter where a class tagged with @EventHandler(UserDeletedEvent.eventName) simply handles that event, plus cron jobs guarded by a Redis lock so they don't double-run across instances. Boring, self-contained, zero extra moving parts.
All of it lives in one pnpm monorepo: frontend, backend and browser extension as apps, plus shared packages they all import. When the desktop app lands, it plugs into the exact same setup.
The best decision: one shared schema package
The glue of that monorepo is a shared package holding Zod schemas as the single source of truth for every QR code content type:
export const QrCodeContent = z.discriminatedUnion('type', [
createContentSchema('url'),
createContentSchema('text'),
createContentSchema('wifi'),
createContentSchema('vCard'),
createContentSchema('email'),
createContentSchema('location'),
createContentSchema('event'),
createContentSchema('epc'), // SEPA payment QR codes
]);
The same schemas validate the React forms in the frontend and the request bodies in Fastify, and they generate all the TypeScript types on both sides. Adding a new content type means: write a schema, write a converter, build the form. Everything else falls out of the types. With the web app, the API and the browser extension all consuming this package — and the desktop app next — that saves an absurd amount of duplicated code.
The rest of the stack: Drizzle ORM on MySQL, Redis for caching, Clerk for auth, S3-compatible storage for logo uploads, and next-intl for nine languages. Dynamic QR codes are — true to the analysis above — a URL shortener: middleware catches /u/:shortCode, the backend records the scan and 302s you to the target.
I'm planning to write more about the individual pieces (building the URL shortener, the shared-schema setup, self-hosting the whole thing). Tell me in the comments which one you'd actually read.
One year later
A year in, QRcodly is stable and the first paying customers are in — and I've learned that building the product was maybe half the job. The other half is making sure anyone finds it. This post is, in part, me working on that second half.
If this resonated: I built QRcodly, an open-source QR code platform with free static and dynamic QR codes. Try it, or come contribute:
QRcodly
QRcodly is a free, open-source QR code generator and management platform. Create, customize, and track QR codes for URLs, contact details, Wi-Fi credentials, and more.
Features
- Multiple QR code types — URL, vCard, Wi-Fi, Email, Calendar Event, Location, Plain Text
- Full customization — colors, sizes, backgrounds, and custom icon uploads
- Export formats — PNG, JPEG, SVG
- URL shortening & analytics — shorten links and track scans
- Templates — save and reuse QR code configurations
- Custom domains — use your own domain for short URLs (Cloudflare integration)
- Internationalization — 8 languages (EN, DE, ES, FR, IT, NL, PL, RU)
- Authentication — powered by Clerk
- Browser extension — generate QR codes from any page
Monorepo Structure
qrcodly/
├── apps/
│ ├── backend/ # Fastify REST API
│ ├── frontend/ # Next.js web application
│ └── browser-extension/ # Vite-based browser extension
├── packages/
│ ├── shared/ # Zod schemas, DTOs, and shared…And I'm curious: what's the worst "free until you depend on it" trick a tool has ever pulled on you?
Top comments (4)
The MIT license guarantees you can fork the code, but it doesn't guarantee your dynamic codes keep resolving — and that's the part that actually bites people. A dynamic QR is a
/u/:shortCoderow plus a domain. If qrcodly.de ever goes away, every printed dynamic code pointing at that host is dead, license or not, because the redirect lived on infrastructure the fork doesn't inherit. The self-host escape hatch only works if you self-hosted before you printed, on a domain you own — otherwise you've just swapped one landlord for another.Which is why the honest framing is: static codes are the thing MIT actually protects (the data's in the pixels, nobody can revoke it), and dynamic codes are a hosting dependency no license can un-make. Worth saying that loudly on the free tier, because "unlimited free dynamic codes" quietly re-introduces exactly the durability problem you built this to escape — just with a nicer landlord.
Thanks for raising this — I actually agree with the core point.
You're right that the MIT license only guarantees the software, not the availability of any hosted redirect infrastructure. If a dynamic QR code points to
qrcodly.deand that domain disappears, those redirects disappear with it. That's an inherent property of any hosted dynamic QR service, regardless of whether it's open source or proprietary.We do mention this in our documentation around custom domains and self-hosting, but it's clearly not prominent enough. We'll make this much more explicit on the website itself, especially on the free tier pages, so people understand the trade-off before creating dynamic codes.
Static QR codes remain the only format that is truly infrastructure-independent, because the destination is encoded directly into the symbol. Dynamic QR codes, by definition, always rely on a redirect service somewhere.
Thanks for pointing this out—we'll make sure that distinction is much more visible.
I like the approach of keeping the free features genuinely free instead of relying on expiry tricks. The explanation of static vs. dynamic QR codes was also clear, it makes the tradeoffs easy to understand!
To addon, in case anyone's looking for more open-source tools, here's a useful collection: 40+ Best Open Source Tools.
Building a QR code generator from scratch is a fantastic way to dive deep into binary data manipulation and canvas rendering in the browser. I remember when I had to implement a similar feature for a ticketing system, and handling the error correction levels in the Reed-Solomon algorithm was surprisingly tricky to get right without relying on a heavy third-party library. Did you end up implementing the encoding algorithms entirely from scratch in TypeScript, or did you wrap a lighter WASM module for the heavy lifting? It is always fascinating to see how open source projects evolve from a simple personal need into a robust tool for the community.