This is the smallest thing that’s still a real SaaS. Accounts, public pages, custom URLs, analytics, and an obvious free/paid line — all in a project you can finish in a weekend.
It also has two security problems that are specific to letting users claim URLs on your domain, and neither is obvious.
The Build
Next.js App Router, TypeScript, Tailwind, Supabase.
Tables:
profiles: id, user_id, username citext unique, display_name, bio, avatar_url, theme, created_atlinks: id, profile_id, title, url, icon, position integer, is_active boolean, click_countlink_clicks: id, link_id, clicked_at, referrer, country, device_typeRoutes: /[username] public profile (no auth), /dashboard editor (auth required), /dashboard/analytics.
Public profiles statically generated with ISR, revalidating when the owner edits. Editor is dynamic.
Splitting static public pages from the dynamic editor is the architectural decision that makes this scale — public pages take essentially all the traffic.
Username Claiming: Two Real Problems
Reserved words
Without a blocklist, someone claims /admin, /login, or /api. Now their profile shadows your own routes, or worse, sits at a URL people trust.
Enforce a reserved username list covering: all existing app routes (dashboard, login, signup, api, admin, settings, pricing, blog), plus impersonation risks (support, help, billing, security, team, staff, official, verified), plus common file paths (favicon, robots, sitemap).
Validate usernames: 3–30 characters, lowercase letters, numbers, hyphens and underscores only. Normalise to lowercase before storing and checking.
Homograph impersonation
Subtler and worth catching. Unicode contains characters that render almost identically to ASCII — Cyrillic а looks like Latin a. Someone registers аpple (Cyrillic first letter) and it’s visually indistinguishable from apple.
Reject any username containing non-ASCII characters. Additionally, check that a new username isn’t visually confusable with an existing one after normalising common lookalike substitutions.
The Editor
Build the dashboard editor: edit display name, bio and avatar; add, edit, delete and reorder links via drag and drop; toggle links active without deleting them; live preview alongside the editor.
Reordering must persist the position field. Save the whole order in one request, not one request per link.
That last point matters — dragging one item to the top otherwise fires twenty requests.
Upload avatars to Supabase Storage. Validate: images only, max 2MB, resize server-side to 400x400. Never trust the client-reported content type — check actual file bytes.
Trusting the declared MIME type lets someone upload a script with an image extension.
Click Tracking Without Slowing Redirects
Route outbound clicks through /r/[linkId], which records the click and then redirects. Record after issuing the redirect using waitUntil so the user never waits. Use a 302, not a 301, so links stay editable and clicks keep counting.
Don’t store full IP addresses — derive country and discard.
Then the analytics:
Analytics page showing total clicks, clicks per link, clicks over time, top referrers, and device breakdown. Aggregate in SQL. Free tier sees 7 days, paid sees unlimited history.
Themes
The most-requested feature and the one most likely to become unmaintainable.
Implement themes as a set of predefined CSS custom property values stored as a theme name on the profile — not as arbitrary user CSS. Provide 6 presets. On paid plans, allow custom accent colour and font choice from a fixed list.
Letting users supply raw CSS on a page you serve is a cross-site scripting vector. Constrained options are both safer and produce better-looking results.
The Free/Paid Line
Natural split for this product:
| Free | Paid |
|---|---|
| Up to 5 links | Unlimited links |
| Preset themes | Custom colours and fonts |
| 7 days of analytics | Full history |
| Branded footer | Remove branding |
| — | Custom domain |
Enforce plan limits server-side in the API, not by hiding buttons. Attempting to add a sixth link on the free plan must fail at the endpoint with a clear upgrade message.
See vibe code a pricing page for the gating helper and Stripe checkout for billing.
Performance
Public profiles are your entire product surface.
Statically generate every public profile with ISR. Revalidate on edit via an on-demand revalidation call. Inline critical CSS. Preload the avatar. Target sub-1-second load on mobile.
The Checklist
- Reserved username list covering routes, roles, and file paths
- Usernames ASCII-only, normalised lowercase
- Homograph confusion check
- Avatar uploads validated by actual bytes, size-limited, resized
- Themes constrained to presets, no user CSS
- Reorder saves in one request
- Clicks recorded after redirect, 302 not 301
- No full IPs stored
- Plan limits enforced server-side
- Public profiles statically generated with ISR
- Ownership filters on every editor query
On Actually Shipping This
The market is crowded and a generic Linktree clone won’t win. Narrow ones can — a version built for musicians with embedded players and tour dates, or for restaurants with menus and booking links, competes on fit rather than features.
The build is a weekend either way. The positioning is the hard part, and no amount of vibe coding helps with that.
Related reading: