Case Study
G7 Smart Cards
Client work for G7: NFC digital business cards, with an admin portal for the card directory and mobile card pages that save straight into phone contacts.
- Period
- –
- Role
- Sole engineer, client project for G7
1 day (25 commits on 23 July 2026)
Core system built
52 backend, 59 frontend
Automated tests
Every 6 hours, restore-tested weekly
Database backups
43
Commits
NFC business cards for a client, in production, that save cleanly into iPhone and Android contacts
Problem
G7 hands out NFC tags as business cards. Each tap has to open a fast, mobile-first card page that a visitor can share or save as a contact, and G7 needed to manage the whole directory themselves while letting each cardholder edit only their own card.
What was built
I built it for G7 as a modular monolith: Next.js as the site and the only browser-facing API, and a private FastAPI service with SQLAlchemy, Alembic and PostgreSQL behind it. Administrators and cardholders are separate actors with their own routes, sessions and cookies, and FastAPI applies the final authorization and contact scoping on every request. Banner and logo images are cropped and converted to WebP in the browser, uploaded straight to private DigitalOcean Spaces storage through short-lived signed URLs, and checked by the server before they are committed.
The hardest part was the saved contact. Phones import vCards differently, so I researched iPhone, Android, Google and macOS importers and wrote down a compatibility policy: one vCard 3.0 file for every platform, the logo embedded as a single JPEG photo, custom labels kept exactly as entered, and a separate text-only fast path for Chrome on Android. Operations are part of the product: PostgreSQL is backed up every six hours with tiered retention, and a weekly job restores the latest backup into a throwaway database to prove it works.
Outcome
The system is live at g7-smart.com, running with Docker Compose on a VPS behind Caddy, which is the only service exposed to the internet. Migrations apply on startup before the API accepts traffic. The source belongs to the client and stays private.
Shipping Timeline
From zero to production.
01 · Build
Backend, portal and public cards
Two actor types, contact APIs, private media uploads, the admin portal and card pages.
02 · Deploy
Production operations
Compose stack behind Caddy, six-hourly backups with weekly restore checks, deployment docs.
03 · Build
LinkedIn contacts and runtime site control
An unlisted URL switches the running site on or off without a restart.
04 · Build
Contacts that survive the import
Logo embedded as the contact photo, custom labels preserved, structured names.
05 · Build
Contact descriptions and media removal
Engineering
How it is built.
Stack
- TypeScript
- Python
- React
- Next.js
- Tailwind CSS
- Node.js
- FastAPI
- PostgreSQL
- Docker
- Vitest
- pytest
Decisions
- Only Caddy is public
- Caddy is the only service with host ports. FastAPI has no public route and PostgreSQL sits on an internal network, so every browser call passes through Next.js route handlers with explicit method and path allowlists.
- Separate, path-scoped sessions per actor
- Administrators and cardholders get their own HttpOnly cookies scoped to /admin or /client, their own session tables and token versions. Refresh credentials rotate on use and only their hashes are stored; mutations also require a same-origin check and a CSRF header.
- Uploads go straight to private storage
- The browser uploads directly to a five-minute signed URL for a key the server chose, so the upload itself bypasses the app servers. On completion the server reads the object back and checks its format, size, dimensions and aspect ratio before committing it, and a storage failure never leaves a database reference behind.
- One vCard 3.0 for every phone
- Platform documentation does not promise how imported photos and labels are kept, so the export uses the smallest portable form: one inline JPEG photo from the logo and labels passed through unchanged. Chrome on Android gets a text-only contact intent, with the .vcf as fallback everywhere else.
- A backup counts only once it restores
- Each backup is checksummed, listed with pg_restore and marked complete only after every file is uploaded. A weekly job restores the latest complete backup into a temporary database, checks the core tables and drops it.

