CatQR Studio is a production-minded dynamic QR code generator and URL shortener. A generated QR code stores a stable short URL such as /r/{token}. The server owns the mutable destination, redirect rules, lifecycle state, and analytics.
Workspace:
Admin dashboard:
- Creates QR codes that encode short URLs like
/r/{token}. - Redirects active links with
302. - Updates destinations after QR creation without regenerating the QR image.
- Soft deletes QR links.
- Returns
410for deleted or expired links. - Returns
404for non-existent tokens. - Generates PNG QR images with a cat-themed frame.
- Records scan analytics.
- Provides a protected admin analytics dashboard at
/admin.
- Dynamic QR behavior: the QR image remains stable while the destination changes in the database.
- Correct redirect semantics: use
302, not301, so browsers and intermediaries do not permanently cache an old target. - Operational safety: deleted and expired links stop redirecting immediately.
- Update authorization: public QR tokens are not credentials; owner mutations require a private
management_keyin production. - Observability: redirect traffic is recorded as scan events for analytics.
- Deployability: frontend, backend, database, cache, and reverse proxy can run through Docker Compose.
- Local ergonomics: frontend and backend live in one repo and can also run directly for fast development.
flowchart LR
User["User Browser"] --> Web["React / Vite Web App"]
Web --> API["FastAPI API"]
Scanner["Scanner / Browser"] --> Redirect["GET /r/{token}"]
Redirect --> API
API --> DB[("Postgres or SQLite")]
API --> Redis[("Redis")]
API --> QR["QR PNG Generator"]
Admin["Admin Dashboard"] --> API
API --> Target["302 Location: destination URL"]
The web app is only a management surface. Scanning a QR code goes through the backend redirect endpoint so the server can check status, expiration, URL policy, and analytics before issuing a redirect.
- The user submits a destination URL and style options.
- The API validates and normalizes the URL.
- The API creates a random token and a private management key.
- The database stores
token -> normalized_urlplus lifecycle metadata. - The response includes
short_url,qr_code_url, and the one-time management key.
- A scanner opens
/r/{token}. - The API validates the token format.
- The API loads the QR link from the database.
- Missing links return
404. - Deleted or expired links return
410. - Blocked links return
403. - Active links record a scan event and return
302 Location: normalized_url.
Redirect responses use:
Cache-Control: no-store, max-age=0
Pragma: no-cacheThis keeps destination updates effective after a QR has already been scanned.
- The owner submits a new destination.
- In production mode, the request must include
X-QR-Management-Key. - The API validates the key, then validates and normalizes the new URL.
- The existing token and QR image URL stay the same.
- Future scans redirect to the updated destination.
For local development, ALLOW_INSECURE_TOKEN_MUTATIONS=true allows update/delete without a key. Production compose sets it to false.
Main tables:
qr_links: token, original URL, normalized URL, status, style, expiration, delete timestamp, management key hash.qr_scan_events: QR link reference, scan timestamp, hashed IP, user agent, referer.admin_users: seeded admin identity and password hash.admin_sessions: signed server-side admin sessions.
The management key itself is not stored in plaintext. The server stores an HMAC hash and verifies future mutation requests against it.
The URL policy layer handles:
- scheme validation: only
httpandhttps; - HTTPS-only production mode;
- host normalization through IDNA;
- path normalization;
- userinfo rejection;
- localhost and private/internal IP blocking.
This protects the redirect endpoint from obvious SSRF-style and local-network targets.
Every successful redirect records a scan event. The admin dashboard aggregates:
- total links;
- active/deleted/expired links;
- total scans;
- scans today;
- scans by day;
- top QR links by scan count;
- recent scan events.
The per-link analytics endpoint returns total scans and day buckets for the selected QR code.
apps/api/ FastAPI backend
apps/web/ React + Vite frontend
docker/ Nginx reverse proxy config
docs/ API, operations, plan, and screenshots
scripts/ API smoke verification
docker-compose.yml Production-like local compose
docker-compose.dev.yml Development compose
Makefile Common local and Docker commands
Start the Docker development stack:
make docker-dev-upOr run the local direct dev servers:
make devIf port 8000 is already occupied by another local service, use:
make dev-8001Default URLs:
- Web:
http://localhost:5173 - API:
http://127.0.0.1:8000 - Admin:
http://localhost:5173/admin - Admin seed login:
admin@example.com/change-me
make docker-upOpen http://localhost:8080.
Production-like compose places Nginx in front of the web app and API. The API uses PostgreSQL and Redis services inside the compose network.
API smoke test:
make verify-apiBackend tests:
make test-apiBackend coverage gate:
make test-api-covFrontend tests:
make test-webFrontend typecheck and build:
make typecheck-web
make build-webThe QR token is public because it appears in the QR code. It must never authorize updates or deletes by itself. Production update/delete flows require the private management_key returned at creation time through X-QR-Management-Key.
Admin analytics use a separate admin login with an HttpOnly cookie. Do not store admin session secrets in localStorage.
Change these before any real deployment:
SECRET_KEYIP_HASH_SECRETADMIN_SEED_PASSWORDorADMIN_SEED_PASSWORD_HASH- public base URL and TLS termination

