# How to Add Dynamic QR Code Generation to Your App (Copy-Paste Prompts Included)

- Tool: App Features
- Last updated: July 2026

## TL;DR

Dynamic QR code generation creates codes that point to a stable short URL you control — when you update the destination, the printed code still works. Three pieces: a client-side QR renderer (qrcode.react or qr_flutter — both free), a Supabase redirect table that maps slugs to destination URLs, and an Edge Function that handles the 302 redirect and logs scan analytics. Build time with Lovable or FlutterFlow is 2–4 hours at $0–25/month.

## What Dynamic QR Code Generation Actually Is

A dynamic QR code always encodes the same short URL — something like yourdomain.com/r/abc123 — but the redirect target behind that URL can be changed at any time without reprinting the code. A restaurant changes its menu URL; the QR codes already on the tables still work. An event organiser updates the check-in page URL; the codes on printed tickets still resolve correctly. The QR image itself is generated entirely in the browser by a free library in milliseconds. The dynamic behaviour comes from the redirect service you build on top: a Supabase table mapping slugs to destination URLs, and an Edge Function that looks up the current destination and issues the 302. Scan analytics are a natural add-on because the Edge Function handles every scan.

## Anatomy of the Feature

Six components — three on the client and three server-side. The redirect layer and error correction configuration are where first builds most commonly go wrong.

- **QR code render engine** (ui): qrcode.react (React) or qr_flutter (Flutter) renders the QR image client-side from the stable redirect URL slug. Supports both SVG and PNG output. Logo overlay is composited on top of the canvas using the Canvas API's drawImage() after the QR is drawn. Error correction level must be set to H (30% fault tolerance) when a logo is used — the default level L fails at 7% occlusion.
- **Redirect and short URL layer** (backend): Each QR code points to a stable path on your domain (e.g. yourdomain.com/r/abc123). A Supabase Edge Function reads the slug from the URL, looks up the current destination_url in the qr_codes table, checks the is_active flag and expires_at timestamp, and returns an HTTP 302 to the destination. Updating the destination is a simple UPDATE on one row — no reprint needed.
- **QR code management table** (data): Supabase qr_codes table with columns: id, owner_id, slug (unique), destination_url, label, scan_count, expires_at, is_active, created_at. RLS policy restricts read and write to the owner. A unique constraint on slug prevents duplicate redirect paths. scan_count is incremented by the Edge Function on each successful redirect.
- **Scan analytics tracker** (backend): The redirect Edge Function, after issuing the 302, inserts a row into qr_scan_events (qr_code_id, scanned_at, user_agent, ip_hash) and increments scan_count on the qr_codes row. ip_hash stores SHA-256 of the IP address for privacy-preserving uniqueness estimation rather than raw IP logging.
- **Download and export UI** (ui): A download button triggers canvas.toBlob() to produce a PNG file, or serialises the SVG element as a string for SVG export. qrcode.react's renderAs='svg' prop enables vector export suitable for print at any size. For PNG, canvas.toDataURL('image/png') creates a data URL used as the anchor href with a download attribute.
- **Branding customiser** (ui): React controlled inputs for foreground colour, background colour, and an image upload for the logo. The logo file is uploaded to Supabase Storage and its public URL stored alongside the QR code record. The logo is composited onto the QR canvas using drawImage() centred in the quiet zone after QR rendering completes. Maximum logo size is 20% of the QR canvas area.

## Data model

Two tables: the QR code registry and the scan event log. Run this in the Supabase SQL editor — it creates both tables with RLS, the unique slug constraint, and the index for analytics queries.

```sql
create table public.qr_codes (
  id uuid primary key default gen_random_uuid(),
  owner_id uuid references auth.users(id) on delete cascade not null,
  slug text unique not null,
  destination_url text not null,
  label text,
  scan_count int not null default 0,
  expires_at timestamptz,
  is_active bool not null default true,
  created_at timestamptz not null default now()
);

alter table public.qr_codes enable row level security;

create policy "Owners can manage their QR codes"
  on public.qr_codes for all
  using (auth.uid() = owner_id)
  with check (auth.uid() = owner_id);

create index qr_codes_owner_idx
  on public.qr_codes (owner_id, created_at desc);

create table public.qr_scan_events (
  id uuid primary key default gen_random_uuid(),
  qr_code_id uuid references public.qr_codes(id) on delete cascade not null,
  scanned_at timestamptz not null default now(),
  user_agent text,
  ip_hash text
);

alter table public.qr_scan_events enable row level security;

create policy "Owners can view scan events for their codes"
  on public.qr_scan_events for select
  using (
    qr_code_id in (
      select id from public.qr_codes where owner_id = auth.uid()
    )
  );

create policy "Service role can insert scan events"
  on public.qr_scan_events for insert
  to service_role
  with check (true);

create index qr_scan_events_code_time_idx
  on public.qr_scan_events (qr_code_id, scanned_at desc);
```

The qr_scan_events insert policy restricts writes to the service role — only your Edge Function (which runs with the service role key) can log scans. This prevents users from inflating their own scan counts by calling the table directly from the browser.

## Build paths

### Flutterflow — fit 4/10, 2–4 hours

FlutterFlow with the qr_flutter package and a Custom Action handles QR rendering and PNG download — best for mobile apps where QR generation and scanning coexist in the same app.

1. Add qr_flutter to FlutterFlow Settings → Pubspec Dependencies
2. Create a Custom Action RenderQrCode that takes a redirect URL string and returns a widget; display it inside a Container on the QR creator page using a Custom Widget wrapper
3. Add a Supabase Backend Action to INSERT a row into qr_codes with the slug (generated with nanoid), destination_url, owner_id, and label when the user taps Create
4. Create a second Custom Action DownloadQrPng that calls RenderRepaintBoundary to capture the QR widget as a PNG and saves it to the device gallery using image_gallery_saver
5. Add a list page bound to qr_codes filtered by owner_id showing label, scan_count, and a Copy Link button

Limitations:

- qr_flutter rendering and the download action both require Custom Action Dart code — not a fully visual build
- Logo overlay compositing on Flutter requires additional Canvas painting logic in the Custom Action
- Flutter Web output for QR download is less polished than iOS/Android — PNG download uses a web-specific path

### Lovable — fit 4/10, 2–3 hours

Lovable with qrcode.react generates styled QR codes with a full Supabase redirect table in a few prompts — best for web-first QR management dashboards with analytics.

1. Create a Lovable project and connect Lovable Cloud for Supabase auth and database provisioning
2. Paste the prompt below in Agent Mode; Lovable will add qrcode.react, the QR creator form, the management dashboard, and the Edge Function stub for the redirect
3. Publish the project and open the Supabase Cloud tab to verify the qr_codes table has RLS enabled
4. Test the redirect by copying a slug URL and opening it in a browser — verify it redirects to the correct destination_url
5. If logo upload is needed, configure Supabase Storage via the Cloud tab → Storage section before testing image upload

Starter prompt:

```
Build a dynamic QR code generation feature using the qrcode.react library. QR Creator page: a form with fields for Label (text), Destination URL (text input with URL validation — show error if empty or not a valid URL before generating), Foreground Colour (colour picker, default #000000), Background Colour (colour picker, default #ffffff), and Logo Upload (image file input). When the form is submitted: generate a unique slug using Math.random().toString(36).slice(2, 8), insert a row into Supabase qr_codes table (owner_id from auth, slug, destination_url, label, is_active: true, expires_at: null), and display the QR code image using qrcode.react with renderAs='svg', level='H', size=256, fgColor and bgColor from the form. If a logo is uploaded, composite it onto a canvas at the centre of the QR (max 20% of QR area) using drawImage(). Include a Download PNG button that calls canvas.toBlob() and triggers a browser download with filename [label].png. Include a Download SVG button that serialises the SVG element. QR Management page: list the current user's qr_codes newest first, showing Label, Destination URL (editable inline), Scan Count, Created At, and Active toggle. Allow editing destination_url with an UPDATE on the row — the QR image does not change. Include an Expiry Date datepicker per code. Create a Supabase Edge Function /r/[slug] that: reads the slug from the URL path, queries qr_codes, checks is_active and expires_at, returns HTTP 302 to destination_url if valid, or renders an 'This code has expired' page if expired or inactive. The Edge Function also increments scan_count and inserts into qr_scan_events (qr_code_id, scanned_at, user_agent, ip_hash) using the service role key. Show scan_count per code in the management list. Handle empty destination URL validation before generating the QR code.
```

Limitations:

- Redirect Edge Function testing requires the published URL — the Lovable preview iframe will not follow the redirect correctly
- Logo upload to Supabase Storage requires the Storage bucket to be created and configured via the Cloud tab before the upload works
- Branding customisation for print-quality output (high-resolution PNG at 300 DPI) requires additional canvas scaling logic beyond what Lovable generates by default

### Custom — fit 3/10, 2–3 days

Custom development gives full control over white-label redirect domains, bulk CSV import, and PDF generation with branded QR codes embedded.

1. Use qrcode npm package for server-side PNG/SVG buffer generation (useful for bulk generation and PDF embedding) or qrcode.react client-side for the management UI
2. Build the redirect service as a Next.js Route Handler at /r/[slug] or a Supabase Edge Function — Next.js Route Handler is simpler if the whole app is on Vercel
3. Add @react-pdf/renderer for bulk PDF generation with QR codes embedded per page — useful for printing batches of event tickets or asset labels
4. Build a CSV import flow that accepts a spreadsheet of labels and destination URLs, generates slugs in bulk, and batch-inserts into qr_codes
5. Configure a custom domain for redirects (e.g. go.yourbrand.com) using a Vercel subdomain or Cloudflare Workers route

Limitations:

- Custom domain redirect setup adds a day of DNS and SSL configuration work beyond the core feature
- Overkill for simple use cases where Lovable's Edge Function approach covers the redirect pattern adequately

## Gotchas

- **QR code points to preview URL and breaks after republish** — AI tools sometimes generate QR codes containing the Lovable preview URL (e.g. abc.lovable.app/preview/...) or the V0 sandbox URL as the encoded value. When the project is republished, that URL changes or expires, and every printed QR code becomes invalid. Fix: Always generate QR codes that encode your own stable redirect slug (yourdomain.com/r/abc123), not the AI tool's preview URL. Set the QR value to the redirect path immediately when the Supabase row is inserted, not from the browser's current URL.
- **Logo overlay breaks QR scanability** — Adding a logo to the centre of a QR code obscures modules that the decoder needs to reconstruct the data. The default error correction level in qrcode.react is level L, which only tolerates approximately 7% occlusion. A logo covering 15–20% of the code causes scan failures on stricter decoders, particularly on older iOS camera apps. Fix: Set error correction to level H (30% fault tolerance) in qrcode.react using the level='H' prop whenever a logo is used. Keep the logo to a maximum of 20% of the QR canvas area. Test the resulting code with multiple devices before printing at scale.
- **Expired QR codes return a blank page instead of a helpful message** — AI tools often implement expiry logic in the redirect Edge Function by returning a 404 response when expires_at is past. QR scanner apps show an empty browser screen with no explanation — the user does not know whether they scanned correctly or the code is genuinely expired. Fix: Return a proper HTML response page (not a 404) when a code is expired or inactive, with the owner's contact information or a renewal call-to-action. Pass a configurable expired_redirect_url field to the qr_codes table so owners can choose where expired codes redirect.
- **Bulk QR code generation hits Supabase insert rate limits** — Generating 100 or more QR codes at once by calling individual INSERT statements in a loop — common in AI-generated code — triggers Supabase free tier rate limiting almost immediately, causing 429 errors and partially created batches. Fix: Use a single batch INSERT statement with multiple rows: INSERT INTO qr_codes (slug, destination_url, ...) VALUES (...), (...), (...). Supabase's PostgREST accepts batched inserts as a JSON array. For very large batches, add a 100ms pause between batches of 50 rows.
- **Redirect Edge Function missing analytics insert due to async fire-and-forget gap** — The Edge Function issues the 302 redirect response quickly, but the scan_events INSERT and scan_count increment run after the response is sent. If the function runtime terminates before the async operations complete, scan events are silently lost. Fix: Use Supabase Edge Function's waitUntil() equivalent — ensure the analytics inserts are awaited before returning the Response. In Deno Deploy (which powers Supabase Edge Functions), use EdgeRuntime.waitUntil(promise) to extend the function lifetime past the response.

## Best practices

- Always use level H error correction when adding a logo to a QR code — level L (the default) breaks at 7% occlusion, level H tolerates 30%
- Generate the QR short URL slug using a random ID (nanoid) rather than sequential integers — sequential IDs expose your total code count and are easy to enumerate
- Store the destination URL in Supabase and point the QR image to your redirect slug from day one, even before you need dynamic updating — retrofitting a static QR to a redirect system means reprinting everything
- Render QR images at minimum 512x512 pixels for digital use and 1024x1024 for print — low-resolution QR codes fail on cameras at distance
- Provide an expiry landing page that explains why the code is expired and gives the owner's contact info — blank 404 pages cause user confusion and support requests
- Cache the QR render result as an image URL stored in Supabase Storage for codes that are shared or embedded in emails — regenerating the image on every page load is unnecessary work
- Test every generated QR code with at least two different camera apps before distributing — iOS Camera, Google Lens, and dedicated QR apps decode with slightly different tolerances

## Frequently asked questions

### What makes a QR code 'dynamic' versus static?

A static QR code encodes the destination URL directly — if the URL changes, every printed code is invalid. A dynamic QR code encodes a stable short URL that redirects to the current destination. The redirect target lives in a database you control. Changing the destination is a database update; the physical code never needs reprinting.

### Can I change where a QR code redirects after it has been printed?

Yes — that is the entire point of a dynamic QR. Update the destination_url column on the qr_codes row in Supabase. The next scan of the same physical code immediately follows the new destination. No reprint required.

### How do I add my logo to a QR code without breaking it?

Set the QR error correction level to H (30% fault tolerance) using the level='H' prop in qrcode.react before adding any logo. Keep the logo to a maximum of 20% of the total QR canvas area. At level L (the default), a logo covering 15% of the code will cause scan failures on many devices.

### How do I track how many times a QR code has been scanned?

Route every scan through your redirect Edge Function. When the function receives a request for /r/abc123, it looks up the destination, issues the 302, increments scan_count on the qr_codes row, and inserts a row into qr_scan_events with the timestamp and a hashed IP. Display scan_count on the management dashboard.

### Can I set an expiry date on a QR code?

Yes. Add an expires_at timestamptz column to qr_codes. In the redirect Edge Function, check whether the current timestamp is past expires_at before issuing the 302. If expired, return an HTML expiry page explaining the code is no longer active. Store a configurable redirect URL for expired codes so owners can send users somewhere useful.

### How do I export a QR code as a high-resolution PNG or SVG?

For SVG: use qrcode.react with renderAs='svg', then serialise the SVG element to a string and trigger a download. For PNG: draw the QR onto an HTML canvas at 1024x1024 (or higher for print), composite the logo if needed, then call canvas.toBlob('image/png') and attach the result to an anchor tag with the download attribute.

### Can I build dynamic QR code generation in FlutterFlow without a separate backend?

The QR rendering itself (using qr_flutter) works entirely on-device. But the 'dynamic' part — updating the destination after printing — requires a backend redirect service. Without a server-side redirect, changing the destination means reprinting the code. You need at minimum a Supabase table and Edge Function for the redirect, which FlutterFlow can connect to via Backend Actions.

### What is the best error correction level for branded QR codes?

Level H (30% data restoration capacity) when adding any logo or design overlay. Level M (15%) for clean codes without a logo in everyday use. Level L (7%) only for digital-only codes displayed on screens where printing quality and physical damage are not concerns. Most AI tools default to level L — change it explicitly in your prompt or component configuration.

---

Source: https://www.rapidevelopers.com/app-features/dynamic-qr-code-generation
© RapidDev — https://www.rapidevelopers.com/app-features/dynamic-qr-code-generation
