Last verified: 2026-03-13 Target:
apps/linksCompanion: none (no@repo/app-linkspackage exists)
Links App Architecture
The links app is an internal tool that gives HappyClient administrators a UI to create and manage branded QR codes. Each QR code encodes a short URL (links.thehappyclient.com/{shortId}) that redirects visitors to any target URL, typically a video testimonial flow. The app also generates printable poster pages that can be displayed at physical customer locations.
Purpose & Audience
| Concern | Detail |
|---|---|
| Primary users | HappyClient org admins |
| End users | Customers scanning physical QR codes |
| Local port | 3005 |
| Production URL | https://links.thehappyclient.com |
| Deployment | Vercel (Next.js standalone, output: "standalone") |
Route Inventory
| Route | Type | Auth | Purpose |
|---|---|---|---|
/ | Page (SC) | Org admin required | QR code creation + management dashboard |
/[shortId] | Route Handler | Public | Short URL redirect with PostHog tracking |
/poster/[shortId] | Page (SC→CC) | Public | Printable QR code poster |
/sign-in/[[...sign-in]] | Page | Public | Clerk sign-in |
/sign-up/[[...sign-up]] | Page | Public | Clerk sign-up |
/api/qrcodes | Route Handler | None (internal) | GET all QR codes |
/api/upload-logo | Route Handler | None (internal) | POST logo to Vercel Blob |
/api/info | Route Handler | Public | Service build info |
/health | Route Handler | Public | Health check |
Auth model: There is no
middleware.ts. Authentication is enforced at the page level viaorganization.verifyOrganisationAdmin()in Server Components. Public routes (redirect, poster, sign-in) are intentionally unprotected.
Module Overview
Primary Data Flow — QR Code Creation
Redirect Flow
Integration Map
Key Components
Server Actions (lib/actions.ts)
All QR code CRUD operations and analytics events are Server Actions ("use server"). This is the only server-side entry point for data mutations.
| Action | Purpose |
|---|---|
createQRCode | Create or return existing QR code by shortId |
getQRCodes | List all QR codes |
getQRCodeByShortId | Fetch single record (used by redirect + poster) |
getQRCodeSvg | Re-generate SVG for existing QR code |
updateQRCode | Update target URL only |
updateQRCodeDetails | Update name, description, targetUrl |
reconfigureQRCode | Update styling (colors, style, logo) |
deleteQRCode | Delete QR code + logo from blob storage |
trackQRCodeRedirect | PostHog event: qr_redirect |
trackQRCodeNotFound | PostHog event: invalid_qr_code_accessed |
hashIpAddress | HMAC-SHA256 IP hashing for analytics privacy |
LinksRegistry (packages/registries/src/server/links-registry.ts)
Extends BaseStorage from @repo/storage. Stores all QR codes as a single JSON array under the key qr_codes in Vercel KV. Every read/write loads the full array.
Scalability note: This read-modify-write pattern is not atomic. Concurrent writes can cause data loss. Acceptable at current scale (small internal tool), but worth monitoring. See improvement task
arch-improvement-links-kv-storage-scalability.
QR Code SVG Generator (lib/qr-styles.ts)
Uses the qrcode npm package to generate QR code data, then builds custom SVG output for 5 visual styles:
| Style | Module shape | Finder pattern |
|---|---|---|
default | Square 1×1 | Standard |
rounded | Rounded rect 0.8×0.8 | Standard |
dots | Circle r=0.4 | Standard |
elegant | Rounded rect 0.8×0.8 | Nested rounded rects |
classic | Square 1×1 + border | Standard |
Optional logo overlay uses higher error correction level (H) to preserve scannability.
Authentication Pattern
The app uses Clerk v6 (@clerk/nextjs ^6.36.5) with ClerkProvider in the root layout. There is no middleware.ts — auth is checked at the component level:
// app/page.tsx (Server Component)
const { organization } = getClients();
await organization.verifyOrganisationAdmin(); // throws redirect if not admin
Public routes (/[shortId], /poster/[shortId], /sign-in, /sign-up, /health, /api/info) are intentionally unauthenticated. The analytics tracking in the redirect handler uses checkOrganizationAuth() (non-throwing) to optionally include auth context in events.
Environment Variables
| Variable | Required | Purpose |
|---|---|---|
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY | Always | Clerk client auth |
CLERK_SECRET_KEY | Always | Clerk server auth + HMAC salt for IP hashing |
NEXT_PUBLIC_CLERK_SIGN_IN_URL | Always | Sign-in route |
NEXT_PUBLIC_CLERK_SIGN_UP_URL | Always | Sign-up route |
KV_URL | Cloud only | Vercel KV connection string |
BLOB_READ_WRITE_TOKEN | Cloud only | Vercel Blob access token |
SUPERADMIN_ORG_IDS | Optional | Comma-separated org IDs with super admin access |
POSTHOG_FLOW_ANALYTICS_API_KEY | Optional | PostHog analytics (disabled if absent) |
POSTHOG_FLOW_ANALYTICS_HOST | Optional | PostHog host URL |
SENTRY_DSN | Optional | Sentry error reporting |
Known Architectural Gaps
| Area | Status | Task |
|---|---|---|
Local components/ui/ shadcn copies | Open | arch-improvement-links-local-ui-components |
| Single-array KV storage (non-atomic writes) | Open | arch-improvement-links-kv-storage-scalability |
Raw toast.error() instead of useSentryToast | Open | arch-improvement-links-raw-toast-errors |
No middleware.ts for route-level auth | By design (page-level auth acceptable for current scope) | — |
typescript: { ignoreBuildErrors: true } in next.config | Tech debt | — |
| Poster text hardcoded in Dutch | Product limitation | — |
| Direct Radix UI deps in package.json | Open | deduplicate-radix-ui-in-links-app (existing) |
| Logo blob storage migration | Open | links-logos-asset-registry-migration (existing) |
Dependency Graph
apps/links
├── @repo/auth — OrganizationClient for org admin verification
├── @repo/core — Logger, env schema utilities, health/info builders
├── @repo/design-system — copyToClipboard (web utilities only)
├── @repo/registries — LinksRegistry (KV-backed QR code storage)
├── @repo/storage — BrandAssetsClient (Vercel Blob for logos)
└── @clerk/nextjs — ClerkProvider, auth UI pages
packages/registries has apps/links listed as a dependent in its usage graph.