No. 01

Web app · 2024–present

VisionLink Dashboard

Digital business cards linked to physical cards, with a dashboard for companies to manage their teams' cards and see how they perform.

Outcome

Grew to 238 active digital cards and over 1,200 card views a day. I added card statistics and teams, then reworked performance and security for the relaunch.

  • 238

    Active digital business cards, one per user

    Counted in the production database on 28 Sep 2026.

  • 1,207

    Digital card views per day

    Daily average, July to September 2026, from the view statistics the product records in its database.

  • 6 → 1

    Background requests per public card view

    Network captures of an anonymous card load, before and after: database reads moved into the server render and three chained tracking calls became one.

At a glance

Role
Lead developer and designer
Client
VisionLink
Engagement
Venture
Timeline
Team
4 people: 2 developers, 1 QA, 1 tester
Stack
  • Frontend Vue, Nuxt, TypeScript, Tailwind CSS, TanStack Query
  • Backend Resend, Paystack, OpenAI
  • Data Supabase
  • Infrastructure Vercel

Context

VisionLink makes physical business cards that open a digital card when they're tapped or scanned. The VisionLink Dashboard is the web app behind them. It serves a public card page for each person, and gives companies a dashboard to manage their team's cards, see how they perform and pay for them. It runs on Nuxt and Supabase and has been in production since 2024. The QR codes on the cards come from a separate service, VisionLink QR v2, which has its own case study.

My role

I'm the lead developer and designer, in a team of four: two developers, one QA and one tester. The other developer is Jacques, VisionLink's co-founder, and we split the product between us.

In the first build, Jacques built the card editor, the admin pages, the partner features and billing. I helped build the public digital cards and the sign-up flow. I built card statistics, both collecting views and showing them, and teams inside companies.

For the relaunch, Jacques added AI features, billing on Paystack and more card templates. I reworked performance and security: fewer requests on the public card, less JavaScript, and hardening on the server and in the database, including row level security (RLS).

Problem

This study is about that rework. The public card is the page that matters most: every tap or scan of a physical card lands there, 1,207 times a day on average. Yet the visitor's browser did a lot of the work. An anonymous card view made six background requests, including two database reads sent straight from the browser to Supabase and a chain of calls to record the view and look up the visitor's location. Finding the card from its URL scanned the cards table with a leading wildcard, twice per view.

Security had the same root. With Supabase the browser talks to the database, so the rules in the database are the security model. RLS was off on a large part of the schema, which left table grants as the only rule.

Constraints

  • A live product. Companies use the dashboard daily, and printed cards point at the public pages. Nothing could break a card link or lock a customer out.
  • RLS had gone wrong before. Switching it on once already locked customers out of their own data, so it had to go in a table at a time, each change with a tested rollback.
  • Two versions at once. The relaunch was built on its own branch while the old version ran production. Changes that depended on relaunch code had to wait for the switchover.
  • A small team. Jacques was building AI features and billing at the same time, so my changes had to land beside his without blocking them.
  • No testing on production. Every measurement came from a local test build.

Approach and architecture

The public card

I moved the card's database reads into the server render. The page now arrives with the card, the company's buttons and the team already in it, and the visitor's browser makes no database calls at all. I replaced the tracking chain with one beacon, which looks the card up by primary key and writes the view. Visitor location now comes from Vercel's geo headers instead of a third-party lookup. The two wildcard scans became primary-key lookups, and code the card only needs after a tap now loads on demand.

The database still does work on every view. The reads happen on the server, nothing is cached, and each view still writes a stats row.

Security

On the server, I made the routes that use the service role check for a verified session, and deleted unauthenticated routes that nothing called. In the database, I started from an inventory of every query the app makes against the tables with RLS off, on every live branch: who runs it, from which page, and what breaks if a policy is wrong. From that I wrote the migrations in waves and tested each one against the real app in a local lab before applying it. The first waves are live in production. The rest are written and wait for the relaunch switchover.

Key decisions and trade-offs

We built on Supabase from the start

Chose
Supabase for auth, Postgres and storage, with Nuxt server routes for anything that needs a secret
Over
A separate backend we would host and maintain ourselves
Cost
The database rules became the security model, and we had to retrofit RLS later

We were two developers building a product, not a platform. Supabase gave us auth, a real Postgres database and storage from day one, and it scales with the product. The catch came later: because the browser talks to the database directly, RLS has to be right from the start, and we left it off on too many tables.

Rebuild the sign-up flow rather than patch it

Chose
A new multi-step sign-up with its own store for plan, accounts and payment state
Over
Fixing the existing flow in place
Cost
Time on a rebuild instead of new features

The old flow was inefficient. We judged that reworking it would take less time than rescuing it, and the rebuild in early 2025 gave sign-up a single place to track the plan, accounts and totals as a customer moves through it.

Move billing from PayFast to Paystack

Chose
Paystack
Over
Staying on PayFast
Cost
A second payment integration to build and a move for existing subscribers

This was a product decision, made for lower fees and pay-as-you-go pricing. Jacques built the billing on Paystack. My part was on the security side, such as checking a sign-up's amount on the server before activating the account.

Move the public card's reads to the server

Chose
Server-rendered reads and one tracking beacon
Over
Tuning the browser's queries and the tracking chain as they were
Cost
Every view now does its database work on our server, and some behaviour changed

This was my call. Tuning the browser queries would have kept the database open to the browser for card data, which is the access RLS needed to close. Reading on the server fixed the speed and the security problem together. The cost is real: the server does more per view, and three behaviours changed. Views of companies with lapsed billing no longer count, cards with team slides turned off no longer show their team, and location comes from Vercel's geo headers.

Outcome

The product has 238 active digital business cards, one per user, counted in the production database on 28 Sep 2026. Cards are viewed 1,207 times a day, averaged over July to September 2026 from the view statistics the product stores.

The performance rework has merged into the relaunch branch and goes live with the relaunch. In network captures of an anonymous card load in a local test build, the visitor's browser now makes no database calls, background requests per view went from 6 to 1, the two wildcard scans became primary-key lookups, and the JavaScript the page ships went from 579 KB to 382 KB.

On security, the server routes now check who is calling, and the first RLS waves are live in production.

Reflection

I'd reverse how I built view tracking. When I built card statistics in April 2025, I recorded views from route middleware in the visitor's browser. That put a chain of calls and a third-party location lookup on every card view. A single server-side call from the start would have been simpler and faster. Eighteen months later, I took it apart as the core of the relaunch rework.

What I took into later work is a habit: start a public page by asking what the browser has to do, and keep everything else on the server.

The page people see most should ask the least of their browser.

Creation of Adam

Interested in working together?