Full-Stack / IoT · 2026
NFC cloud portal shipped
Smart product redirector system
The problem
Physical products need a bridge to digital experiences. NFC-embedded items require a cloud backend that can dynamically route taps to custom URLs, track analytics, and provide owners with a management dashboard.
The solution
Engineered an end-to-end NFC product platform: hardware programming, cloud redirector service, admin dashboard, and analytics pipeline. Each NFC chip is programmed with a unique ID that resolves through the cloud portal to a configurable destination.
System architecture
How it's wired
NFC tap resolves through a stateless redirector. HMAC + rate-limiting block ID enumeration on the edge. Admin console sits behind an OIDC identity provider with MFA, short-lived tokens, and per-request token verification — no single admin password is ever the blast radius. Coral arrows = failure paths.
Engineering rationale
Technical decisions
Why this stack, what the trade-offs were.
How is NFC ID enumeration prevented?
The /resolve endpoint only returns redirect data when the request includes a valid HMAC signature derived from a server-side secret. Forging signatures requires the secret; without it, incrementing IDs in the URL gets you 403s. Per-chip rate-limiting blocks brute-force enumeration of valid IDs. Admin endpoints (where the real data lives) require JWT auth scoped to chip ownership — even if you found a valid ID, you can't read its analytics or change its destination unless you own it.
Why a stateless redirector instead of programming URLs directly on the chip?
Chips are write-once in production. A stateless redirector means destination URLs can change after the product ships — same physical chip, new campaign. The chip stores a permanent ID + signed token; the backend owns the routing.
Why an in-memory LRU cache instead of just hitting the DB every time?
Fail-soft over fail-closed. If Neon has a 30-second blip, the redirector still serves the last-known destination from cache rather than 404'ing every tap. Cache TTL is short (60s) so legitimate admin changes still propagate quickly. Trade-off: a malicious admin change is visible for up to 60 seconds.
What protects the admin console if a session is hijacked?
The admin plane sits behind an OIDC identity provider (Auth0) with MFA enforced on the admin role. Access tokens are short-lived (15 minutes) and verified per request against the provider's JWKS endpoint, so a stolen cookie expires fast. Refresh tokens rotate on every use — replay of an old refresh token invalidates the chain and forces reauth. There is no single shared admin password: each operator authenticates as themselves, every write is audit-logged with the operator's user ID, and HMAC signing secrets live in environment config (rotatable) rather than the user-facing auth surface. Compromising one operator's account doesn't expose the chip-signing keys; compromising the keys requires server access, not a phishing email.
Why an external IdP instead of rolling auth in Flask?
Hand-rolled auth is where most small projects die: password storage, session invalidation, MFA enrollment, account recovery, lockout — each one is a security paper waiting to happen. Auth0 / Clerk / Cognito hand you OIDC + MFA + audit trails out of the box for the price of a small monthly bill. The trade-off is vendor lock-in on the auth surface, which I accept because the upside (correct primitives) dwarfs the risk (migrating IdPs is a one-week project, not a rewrite).
Why a single admin key today instead of per-user JWT?
Honest answer: it's a single-operator product right now. Adding multi-user OIDC + role management before there's a second user is over-engineering. The diagram shows where the IdP slots in — designed for that path, not built speculatively. The HMAC-signing secret for chips is already separated from admin auth, so the migration is auth-only and doesn't touch the redirect plane.
Failure modes
Edge case handling
What breaks at the edges, and how the system responds.
- Invalid HMAC signature (forgery attempt) → 403 + entry in audit log + per-IP rate-limit counter increment
- Admin session hijacked → access token expires within 15 minutes; refresh-token rotation detects replay and invalidates the entire token chain; suspicious activity flagged via the IdP's anomaly detection
- MFA device lost → IdP-managed recovery flow with backup codes; admin write privileges remain blocked until reauth completes through a verified channel
- DB momentarily unreachable → redirector serves cached destination from in-memory LRU (fail-soft, 60s TTL)
- Destination URL deleted by admin while chip is in the wild → graceful fallback page instead of 404
- High-frequency taps from same device → rate-limited per chip-ID + IP combo to prevent analytics noise
- User taps with NFC disabled → no request hits backend; product docs cover enabling NFC on iOS/Android
Current scope
Limitations
What this system is not today — to be precise about scope.
- Admin auth today is a single environment-keyed login — no per-user identity, no MFA, no audit trail per operator. The diagram shows the OIDC path the system is designed for; shipping it before there's a second admin would be speculative work
- Analytics writes are synchronous; high tap volumes (>100/s sustained) would back up the redirector
- No A/B testing or scheduled redirect changes — destination URL is a single value, not a campaign object
Scaling
What breaks first at 10x
Stateless Flask service sits behind Render's load balancer — horizontal scale is a config change. Neon Postgres autoscales reads. Next bottleneck is analytics writes; that moves to an async queue (Redis stream → batched insert) before becoming a problem.
Roadmap
What I'd build next
Implementation notes
Build details
- Flask backend on Render with Neon Postgres handles NFC ID resolution and URL redirection
- Admin dashboard (vanilla JS) for managing redirects, viewing tap analytics, and configuring products
- Content Security Policy headers and admin-key authentication protect against injection and unauthorized access
- Each NFC chip is individually programmed and linked to a database record with configurable destination URLs
- Geolocation analytics track where and when products are tapped for market intelligence
- Designed for horizontal scaling — stateless redirector can sit behind a load balancer
Tech stack
Want to dig deeper?
Happy to walk through code, decisions, or design files.
Get in touch →