A team-built link-management SaaS with ad-monetized redirects — I owned the authentication system underneath it: local and OAuth login, email verification, password reset, and the JWT token model.
February 2026 · Team Project · ~3 weeks, part-time (Feb–Mar 2026), plus a landing-page pass in May 2026
NanoLink is a link-management SaaS built by a two-person team as a Final Year Project — a NestJS API and a Next.js 16 frontend in a shared pnpm monorepo. It covers short-link creation with custom domains and expiry rules, multi-tenant workspaces, click analytics on a Tinybird pipeline, a link-in-bio page builder, and an interstitial ad-monetization layer that pays link creators per click. The codebase is large — 24 backend modules, 45 Prisma models — and most of it is my teammate's. My own work was the authentication layer everything else in the app runs on top of, plus the public marketing site.
A platform with multi-tenant workspaces and a monetization layer where money is attributed per click needs identity to actually hold up — correctly telling a workspace owner from a member, a verified account from an unverified one, and a user who signed up with a password from the same person returning through Google, rather than just gating a dashboard. Getting any of that wrong is a real security or billing bug, not a UX nit. My job was to build that layer end to end: local and OAuth login, email verification, password reset, and the token model everything else authenticates against.
Frontend
Backend
Services
Solution
Replaced it with a 6-digit OTP with a 60-second expiry and a bounded resend budget (5 per 24 hours), and explicitly handled re-registration of an email that exists but was never verified.
Result
Shrinks the verification credential's live window from 24 hours to 60 seconds, while still tolerating a mistyped code through resends.






The auth module has zero test coverage — only the url, user, and domain modules have specs in this codebase. Everything here was verified by hand through manual flows, which is the first gap I'd close if I revisited this code.
Building inside a teammate's fast-moving frontend meant parts of what I shipped — the axios refresh interceptor, the edge middleware — were later substantially rewritten as the app grew; a good reminder that on a team codebase, the code you write is a starting point for someone else, not a finished artifact.
Signing the access and refresh JWT from the exact same payload, just with different secrets, keeps the token logic simple — but it also makes the two tokens structurally interchangeable except for which secret verifies them, which I'd design differently (a distinct token-type claim) if I did it again.