[ Selected work ] / 2026
Broslunas Link
>>> Summary
Multi-tenant link pages: every person or business gets their own on a subdomain, with themes, WhatsApp and a QR code.
- Role
- Diseño y desarrollo
- Stack
- Next.js, React, TypeScript, Tailwind CSS, MongoDB, Auth.js, Cloudflare R2, Vercel
- Year
- 2026
>>> Details
A Linktree alternative where the address is yours: marta.broslunas.link. It is a portfolio project to show a real multi-tenant architecture, free and with no payments.
- +A subdomain per page: a
proxy.tsresolves the host and rewrites to the tenant route without changing the visible URL. - +Data isolation: every query goes through an access layer that requires
tenantId, with a test that checks two tenants never cross. - +Editor with sortable links (drag and drop), section headings, a WhatsApp button and YouTube or Spotify embeds.
- +Seven preset themes (Aurora, Papel, Neón, Pastel, Atardecer, Brutal, Océano) plus font, colour and card-style tweaks.
- +Downloadable QR code as PNG or SVG, and an optional custom domain.
- +Visit and click stats per link, with no cookies.
- +Per-tenant cache tag, invalidated when saved from the dashboard.
- +Sign-in with Google or an emailed magic link, and rate limits on sign-up and creation.
>>> Case study
- 1
- Days of development
- 24
- Commits
- 65
- Tests
- 7
- Themes
* Days and commits come from the git history (18 Oct 2026); tests come from `pnpm test` (Vitest, 9 files, all passing).
[ 01 ] Challenge
Freelancers and local businesses live off linking their site, bookings, WhatsApp and socials from one place, and the usual tools put the address under someone else's path (
domain.com/user). I wanted a version where everyone gets their own subdomain, with their own theme and QR, and which also works as a showcase of a real multi-tenant architecture: isolated data, dynamic domains and no payments.[ 02 ] Solution
A
proxy.tsreads each request's host and rewrites internally to/tenant/[slug]without changing the visible URL;app.serves the dashboard and the root serves the landing page. Every query goes through a single data-access layer that requirestenantId. The public page is cached with one tag per tenant and invalidated when saved from the dashboard. Each sign-up registersslug.broslunas.linkin Vercel through its API so it issues the certificate, and the editor lets you reorder links, pick one of seven themes, add WhatsApp, YouTube or Spotify embeds and download the QR as PNG or SVG.[ 03 ] Result
A platform live at broslunas.link, with 65 tests (including the tenant-isolation one), seven themes, optional custom domains and visit and click stats, built in a single day going by the git history.
[ Technical decisions ]
01A data-access layer that requires `tenantId` on every query
- Alternative considered
Filtering by tenant in each ad-hoc query and trusting code review.
- Why
forTenant(id)is the only way into links, theme and events, and every query carries thetenantIdinside. A test creates two tenants and checks that no read or write crosses data, so a slip is caught in CI rather than in production.
02Subdomains through a rewrite in `proxy.ts`
- Alternative considered
Path-style routes like
/u/martaor one deployment per customer.- Why
The proxy resolves host → slug and rewrites to
/tenant/[slug], so there is one app and one database, and the visible address ismarta.broslunas.link. The price is relying on wildcard DNS and registering each subdomain for its certificate.
03Per-tenant cache tag, invalidated on save
- Alternative considered
Rendering on every visit or caching for a fixed time.
- Why
getPublicPageusesuse cachewith thetenant:<slug>tag. The public page is static until its owner saves a change, and then only theirs is invalidated. Views and clicks are recorded separately, outside the cache.
04Only `http`, `https`, `mailto` and `tel`, and embeds only from YouTube and Spotify
- Alternative considered
Accepting any URL and escaping it on render.
- Why
A
javascript:link never gets saved. Foriframes, the user's URL is never used as thesrc: it is converted to a known host's player URL, and any other host is dropped.
05Rate limiting in MongoDB with a TTL
- Alternative considered
Adding Redis or Upstash just for this.
- Why
A fixed window with
findOneAndUpdateand a TTL index reuses the database that already exists and is shared across instances. It is coarser than a dedicated limiter, but for sign-up and page creation it is enough and adds no infrastructure.