
The QR opens the door. The video was already inside.
- Role
- Head of Tech · platform, backend, delivery
- Context
- QR to AR overlay on packaging, pre-linked media, per-customer data and callbacks
- Tools
- Next.js
- Supabase
- PostgreSQL
- React
- Turborepo
- Timing
- 2026
Our first real campaign was 250 QR codes. We generated them on our platform, then handed them to our partner HappiBox, who printed them onto the packaging that ships to their customers.
Once a sticker is on a box and that box is on its way, the QR is frozen. We can't edit it. We can't recall it. Whatever each code points at when someone scans it next week, or next month, is what they see. If we linked one QR to the wrong video, that mistake would sit on a shelf somewhere and play to a real person opening a real box. No error, no warning. Just the wrong video for the wrong customer.
250 codes is small. The point is they all had to be right. We're around 400 now, and the next batch will be bigger.

What actually happens when you scan
The interesting part of MagiqAR isn't the scan. It's that the box itself becomes the screen.
Someone points their phone at a Magiq QR code on a piece of packaging. Their browser opens, the camera turns on, and a video starts playing as an AR overlay locked to the printed artwork. Move the box, the video moves with it. Tilt it, the video tilts. No app, no download. Just the camera and the browser.
While the camera is doing the visible magic, our server is doing one boring thing in the background: looking up which video belongs to this QR for this customer. The video itself was rendered days earlier and has been sitting in storage waiting for this exact scan. No AI generates anything in the moment. The fancy bits, the AR tracking and the overlay rendering, all happen on the phone. The job on our side is much smaller: get the right video for the right QR, every single time.
For that to work, three things have to stay in sync: the QR code (out in the world, on packaging), the customer (in our database), and the video file (in storage). If any of them drift out of alignment, the scan resolves to the wrong thing. A campaign gets edited. A file gets replaced. A background job runs at the wrong moment. The person scanning has no way to know.
So the system is built around one rule: the database is the only place that decides which video belongs to which QR. Not the application. Not a config file. Not someone's memory of how it's supposed to work. The database.
Under the hood: TypeScript across a single codebase. Supabase handles login, the database, file storage, and the small server endpoints a scan resolves to. Background workers do the slow stuff: importing data, listening to other systems, cleaning up after themselves.
Three ways this goes wrong
What we design against
A QR points at the wrong brand's video
Someone edits a campaign in the admin panel, clicks the wrong row, and now Brand A's sticker plays Brand B's video. The customer scanning trusts the packaging. They have no way to spot the swap. So the database has to make that kind of mistake impossible to save, not just rare.
The same event arrives twice
Other systems we plug into don't always send a notification once. They send it twice. Sometimes three times. Sometimes out of order. If our code reacts naively each time, we end up with duplicate records, wrong counts, or two video files where there should be one. So we treat every incoming event as something that might already have been handled, and we check before doing anything.
A campaign caught mid-update
A new video gets uploaded, but the link to it hasn't been switched over yet. Or the link switches first, while the upload is still in flight. For a few seconds, the system is in two states at once. Without explicit checks, a scan in that window plays whatever happens to win the race. So updates move through clear stages, and a scan only ever resolves to a video that's been marked fully ready.
What we learned
Put the rules in the database, not in code. Postgres can enforce things directly: this customer can only see their own rows, this QR can only link to a video that exists. If the rules live in application code, it's too easy for someone to write a new endpoint and forget one. If they live in the database, every path through the system follows them automatically.
Use a small number of clear states. A video isn't just "uploaded". It's pending_ingest, processing, ready, or archived. Every state change is recorded. Background workers can only move something forward when the checks pass. Staging and production behave the same way.
Assume things will happen twice. External systems retry. Internal jobs retry. Plan for it from day one.
Alert on things that stop, not just things that crash. A crashed process gets retried automatically and usually fixes itself. A record stuck in pending_ingest for six hours doesn't crash anything. It just quietly fails to ever go live. And at some point, it ships to a real customer.
From the operator side, all of this looks calm:

Those first 250 codes are out in the world now, along with the 130 that followed. Every one of them resolves, every time, to the video it's supposed to. The packaging is what people see. The database is what makes it true.