
ScanKit
scankit.appScanKit is a SaaS product I designed and built for small marketing agencies: a tool for creating trackable QR codes whose destinations you can change after they have already been printed. An agency prints a code on a flyer or poster, and if the campaign URL changes later, they just repoint the code in ScanKit instead of reprinting everything. It is a real, live product with paid subscriptions, not a demo.
Dynamic by design
Every QR code points at a short redirect URL (like scankit.app/r/spring-flyer) rather than the final destination. The printed code never changes, but the destination behind it can be updated at any time, with the full history kept. That one indirection is the whole value: printed collateral stays useful for the life of a campaign instead of going stale the moment a link changes.
Built for agencies
Agencies juggle many clients, so the app is organized around workspaces and tags that keep each client's codes separate and tidy. Scan analytics (country, city, device, and totals over time) are GDPR and CCPA compliant and clean enough to drop straight into a client report, with CSV export when someone needs the raw numbers. The codes are customizable too: colors, shapes, logos, frames, and captions, exported as SVG or print-ready PNG.
How it was built
The app is a full-stack Next.js 16 (App Router) project in TypeScript. Authentication and data run on Supabase, with Postgres and row-level security on every table, and subscriptions are handled through Stripe across a free, Plus, and Pro tier. The blog is managed in Sanity through an embedded studio, transactional email goes through Resend, and product analytics run on PostHog. The marketing site is fully internationalized across English, German, and Dutch using locale-segment routing, and the scan redirect endpoint at /r/[slug] is what makes the dynamic codes work.
Design
ScanKit's customers live in tools like Linear, Figma, and Vercel and judge software by how it looks, so the interface leans into a modern, minimalist style: a white canvas, near-black text, slate neutrals, and a single bright blue accent. The type is Geist throughout, with hierarchy coming from weight and size rather than extra fonts. Hairline borders and generous whitespace do the structural work instead of heavy shadows, and analytics figures use tabular numerals so the columns line up cleanly. It is styled with Tailwind CSS v4 and shadcn/ui, with the brand tokens centralized as CSS variables so light and dark mode stay in sync.