The problem
A group meets — a collective, a cohort, a class — and everyone wants to find everyone else afterwards. What actually happens is a chat thread of pasted links that scrolls away by the next morning, or a signup sheet someone promises to type up and never does.
The information isn't hard to collect. Each person has one link and one sentence about themselves. The friction is entirely in the collecting: any tool that asks sixty people to make an account has already lost most of them, and a tool that asks one organiser to do the typing has just moved the work rather than removing it.
Design goal: from hearing a URL to being on the wall in under a minute, on a phone, standing up, with no password involved.
What it is
A room is a URL, and the URL is the whole invitation. An organiser names a room and picks the platform it runs on — LinkedIn for a professional group, Instagram for a creative one — which produces an address at the site root, like theconnectionwall.com/coletivodelas, ready to read off a stage or drop in a group chat.
Everyone who opens it sees the same page: a short form and the wall as it stands. Add your name, your link, and one line about yourself, and you become a circle other people can click straight through — there's no members-only view, because there are no members. The first room was built for Coletivo Delas, a women-in-tech collective, and it runs live today.
How it works
- 01
Name the room
The organiser names the room and picks the platform its members will link — LinkedIn today, with Instagram, X and the rest already in the picker.
- 02
Share one link
Members open it and fill three fields. Validation is immediate and human: “Use a full LinkedIn URL”, not “invalid input”. The description caps at 280 characters, with the counter running live.
- 03
The wall fills in
Each person lands as a circle carrying their initials, name and one line. Hovering lifts the tile; clicking opens their profile page, and from there, their LinkedIn.
Decisions
Decision 01
No accounts — so the database had to say no instead
Anyone with the link can add themselves, which is also the whole attack surface. The public database role holds SELECT and nothing more; every write passes through a server function that validates first. A leaked anon key gets someone a copy of a public wall, not a way to write to it.
supabase/migrationsDecision 02
Every room name has to produce a usable address
Room names fold into lowercase alphanumeric slugs, but a name in Japanese, an emoji, or something like “admin” leaves nothing safe behind. Every name still gets a room — it just gets a generated address when the slug collides or comes back empty, with a reserved list keeping rooms from ever shadowing a real route.
src/lib/rooms.functions.tsDecision 03
One person, one tile — decided on the link, not the string
The same person rarely pastes the same URL twice. The wall stores exactly what was typed, but compares a normalised form — host without www, path without case or trailing slash, query dropped — so two slightly different pastes still land as one tile, with a unique index as the backstop.
src/lib/profiles.functions.tsDecision 04
Rejection belongs under the field, not in a toast
Early versions surfaced one error at a time in a toast that vanished — bad on a phone, mid-form, at an event. Validation now reports every problem at once, under the field it belongs to, moves focus to the first one, and clears the message the moment that field is edited.
src/components/wall-form.tsxDecision 05
The room decides which links are acceptable — never the client
Platforms live in one table that drives the dropdown, the label, the placeholder and the accepted hosts, on both sides of the wire. The server re-reads the room to find out what counts as valid rather than trusting whatever platform the form sent, and URLs are parsed rather than regex-matched.
src/lib/platforms.tsDecision 06
Ship the code before the migration, without taking the wall down
Rooms were introduced after one wall was already live. Deploying code and applying a migration are separate events, so every read tries the new schema first and falls back to the old shape until the migration lands — which adopts every existing profile into a room at its already-published slug.
src/lib/legacy-schema.tsBuild order
Built narrow before general: each stage below only started once the previous one was in real use, which is why the hardest problem turned out to be generalising without breaking what was already live.
Stage 01
One hard-coded wall
A single LinkedIn wall at a fixed path, profiles in one table, no concept of an owner — just enough to find out whether people would actually add themselves.
Stage 02
Identity: logo, favicon, dark palette
A node-graph mark, eight nodes around a hub, two of them cyan — the shape that also decided the tile layout: circles, not cards.
Stage 03
Rooms as real rows at their own URLs
Slugs, reserved paths, eight platforms, and a migration that adopted every existing profile so the already-published address kept working.
Stage 04
The things a real event exposed
A copy-the-link button, because the address bar is the worst place to copy from on a phone. A duplicate guard, because people submit twice. Inline errors, because toasts disappear.
Built with
- React 19
- TypeScript
- TanStack Start
- TanStack Router
- TanStack Query
- Tailwind v4
- shadcn/ui
- Radix
- Supabase
- Zod
- Vite 8
- Bun
Server-rendered with file-based routing and typed server functions, so the wall is readable before JavaScript loads and shareable links resolve on the server.
What I’d do next
Ownership without accounts. A creator token in the link would let an organiser remove a tile or close a room — the one power the current model can't express.
Retire the compatibility bridge. The legacy fallbacks stop being reachable the moment the migration is applied; the file already names its own deletion criteria.
Room-level moderation. Public write access with no login has exactly one failure mode, and it needs an answer before a wall runs unattended.
Measure the funnel. Opens versus submissions per room is the number that would say whether the under-a-minute goal is actually met.
Craft notes
Small things took disproportionate care: the room name is stored as typed and uppercased in CSS, so every wall header reads alike without shouting at whoever named it. The share link reads its origin from the browser instead of a hard-coded domain, so preview deploys copy their own URL, not production's. Tiles are circles at a fixed aspect ratio with clamped text, so one long name can't break the grid.
The product is one form and one grid. Almost all of the work is in what happens when someone uses it imperfectly.