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 (0)