All docs/general

docs/architecture/links-app-architecture.md

Last verified: 2026-03-13 Target: apps/links Companion: none (no @repo/app-links package 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

ConcernDetail
Primary usersHappyClient org admins
End usersCustomers scanning physical QR codes
Local port3005
Production URLhttps://links.thehappyclient.com
DeploymentVercel (Next.js standalone, output: "standalone")

Route Inventory

RouteTypeAuthPurpose
/Page (SC)Org admin requiredQR code creation + management dashboard
/[shortId]Route HandlerPublicShort URL redirect with PostHog tracking
/poster/[shortId]Page (SC→CC)PublicPrintable QR code poster
/sign-in/[[...sign-in]]PagePublicClerk sign-in
/sign-up/[[...sign-up]]PagePublicClerk sign-up
/api/qrcodesRoute HandlerNone (internal)GET all QR codes
/api/upload-logoRoute HandlerNone (internal)POST logo to Vercel Blob
/api/infoRoute HandlerPublicService build info
/healthRoute HandlerPublicHealth check

Auth model: There is no middleware.ts. Authentication is enforced at the page level via organization.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.

ActionPurpose
createQRCodeCreate or return existing QR code by shortId
getQRCodesList all QR codes
getQRCodeByShortIdFetch single record (used by redirect + poster)
getQRCodeSvgRe-generate SVG for existing QR code
updateQRCodeUpdate target URL only
updateQRCodeDetailsUpdate name, description, targetUrl
reconfigureQRCodeUpdate styling (colors, style, logo)
deleteQRCodeDelete QR code + logo from blob storage
trackQRCodeRedirectPostHog event: qr_redirect
trackQRCodeNotFoundPostHog event: invalid_qr_code_accessed
hashIpAddressHMAC-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:

StyleModule shapeFinder pattern
defaultSquare 1×1Standard
roundedRounded rect 0.8×0.8Standard
dotsCircle r=0.4Standard
elegantRounded rect 0.8×0.8Nested rounded rects
classicSquare 1×1 + borderStandard

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

VariableRequiredPurpose
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEYAlwaysClerk client auth
CLERK_SECRET_KEYAlwaysClerk server auth + HMAC salt for IP hashing
NEXT_PUBLIC_CLERK_SIGN_IN_URLAlwaysSign-in route
NEXT_PUBLIC_CLERK_SIGN_UP_URLAlwaysSign-up route
KV_URLCloud onlyVercel KV connection string
BLOB_READ_WRITE_TOKENCloud onlyVercel Blob access token
SUPERADMIN_ORG_IDSOptionalComma-separated org IDs with super admin access
POSTHOG_FLOW_ANALYTICS_API_KEYOptionalPostHog analytics (disabled if absent)
POSTHOG_FLOW_ANALYTICS_HOSTOptionalPostHog host URL
SENTRY_DSNOptionalSentry error reporting

Known Architectural Gaps

AreaStatusTask
Local components/ui/ shadcn copiesOpenarch-improvement-links-local-ui-components
Single-array KV storage (non-atomic writes)Openarch-improvement-links-kv-storage-scalability
Raw toast.error() instead of useSentryToastOpenarch-improvement-links-raw-toast-errors
No middleware.ts for route-level authBy design (page-level auth acceptable for current scope)
typescript: { ignoreBuildErrors: true } in next.configTech debt
Poster text hardcoded in DutchProduct limitation
Direct Radix UI deps in package.jsonOpendeduplicate-radix-ui-in-links-app (existing)
Logo blob storage migrationOpenlinks-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.