Skip to main content
vibld

Template

Baywick

A starter for products where each customer gets their own subdomain: visitors claim a name, pick an emoji as their mark, and immediately get a live tenant page at name.rootdomain. An admin area lists and removes tenants. It demonstrates host-based routing, per-tenant content and shared layouts in one small codebase.

Subdomain-per-tenant SaaS starter with claim form and admin · App · Small tools and apps · full-stack app (auth + DB)

A mock-up of the homepage, drawn from this design’s layout, palette and typefaces. A build follows the full prompt below.

Add app screens

Pick up to 6 screens, such as a dashboard, settings or an empty state. Each is built in this design’s own palette and typefaces, with its states and guardrails.

Account setup
Add edit
Analytics
Appearance
Calendar
Chat
Checklist
Checkout
Confirmation
Dashboard
Delete account
Details
Discovery questions
Empty state
Flowchart
Import export
Inbox
Integrations
Invite team
Loading
Login
Onboarding
Plans
Playground
Product tour
Referral
Search
Settings
Share
Sign up
Success
Table
Team members
Text editor
Upgrade
Usage
Verification
Welcome

Start from this templateRead the build prompt

Typefaces

Pontano Sans is a quiet, neutral sans with a component-library plainness, so the tenant emoji stays the only colour.

  • Pontano SansHeadings: domain title 30px 700 at -0.02em, tenant heading 36px 700
  • Pontano SansBody: body 16px/1.5 400, labels 14px 500, helper text 14px

Patterns

  • centred single-card claim form on soft gradient
  • input with trailing domain suffix addon
  • emoji/icon picker popover
  • disabled primary until valid
  • subdomain-based tenant routing
  • admin table with row delete
  • tenant page template with shared layout

States it is designed for

  • Idle with disabled submit
  • Invalid name (too short, reserved like www/admin/api, bad characters) with inline error
  • Name taken: inline error with suggestion
  • Checking availability: small spinner in the addon
  • Submitting: button spinner, inputs locked
  • Unknown subdomain visited: friendly 404 with link to claim it
  • Admin: empty table state, loading skeleton rows, delete failure toast with retry
  • Unauthenticated admin access: redirect to sign-in

Who it is for

  • developers building multi-tenant SaaS
  • platform teams prototyping custom-domain products
  • hackathon teams needing instant per-user sites

Layout

  1. Root domain landing: full-viewport soft vertical gradient (pale blue-grey to white), centred wordmark-style domain name, one-line subtitle
  2. Claim card (max 400px): 'Subdomain' label, input joined to a grey suffix addon showing the root domain; 'Icon' label, field showing selected emoji or 'No icon selected' with a 'Choose emoji' button; helper text; full-width primary 'Create subdomain' button
  3. Tenant page (name.rootdomain): large emoji, heading 'Welcome to {name}', short paragraph, link back to root
  4. Admin (/admin): top bar with title and sign-out; table of tenants (emoji, subdomain, created date, visit link, delete button)
  5. Mobile: card goes full width with 16px gutters; admin table becomes stacked rows

Palette

Quiet, neutral and functional. The emoji is the only colour, which makes each tenant feel personal.

  • gradient top#eff4fb
  • card / page surface#ffffff
  • primary text#0a0a0a
  • muted / helper text#707070
  • suffix addon background#f5f5f5
  • input border#949494
  • primary button#171717
  • disabled button fill#737373
  • focus ring#2563eb
  • destructive#dc2626

Every checked pair, measured again

SampleWhereRatioNeeds
Aabody text on gradient top17.92:14.5:1
Aahelper text on card4.95:14.5:1
Aasuffix text on addon4.54:14.5:1
Aaprimary button label17.93:14.5:1
Aadisabled button label4.74:14.5:1
input border on white3.03:13:1
focus ring on gradient4.68:13:1
Aadestructive text on white4.83:14.5:1

As vibld’s tokens

The palette on the fifteen colour tokens vibld styles a project with, each text colour on the fill it is read on.

  • background
  • card
  • muted
  • primary
  • secondary
  • accent
  • destructive

Type scale

Display
Pontano Sans 700, 30px domain title with -0.02em tracking; tenant welcome heading 36px
Body
Pontano Sans 400 16px/1.5; labels Pontano Sans 500 14px; helper text 14px

Neutral component-library look. Observed labels and helper text were ~12px; raise to 14px minimum.

Spacing and imagery

Compact card: 24px padding, 16px between fields; 4px grid; card radius 12px, inputs 8px; soft shadow `0 1px 3px rgb(0 0 0 / 0.08)`. Admin table rows 48px high.

Emoji as tenant mark (rendered large on tenant pages). No photography.

Components

  • ClaimCard
  • InputWithSuffix (subdomain + .rootdomain)
  • EmojiPicker popover with search
  • SubmitButton with pending state
  • TenantPage template
  • AdminTenantTable
  • DeleteTenantDialog
  • Toast

Interactions

  • Subdomain input lowercases and strips invalid characters as you type; live availability check after 400ms
  • Choose emoji opens a searchable popover; arrow keys move, Enter selects, Escape closes
  • Create button enables only when name is valid, available and an emoji is chosen
  • On success, redirect to the new tenant URL with a 'Your site is live' toast
  • Admin delete opens a confirm dialog requiring the subdomain to be typed

Data

  • Tenant{id, subdomain (unique, 3-32 chars, a-z0-9-), emoji, owner_id, created_at}
  • ReservedName{name}
  • AdminUser{user_id, role (admin)}

Guardrails

Experience

  • Show the full resulting URL (name.rootdomain) live under the input
  • Explain why the button is disabled via helper text, not silence
  • Reserve system names and say so clearly when blocked
  • Keep the claim flow to one card and one click
  • Deletion in admin always requires typed confirmation

Accessibility

  • The suffix addon is part of the input's accessible description ('ends with .rootdomain')
  • Emoji picker is a dialog with a labelled search and a grid using roving tabindex; each emoji has a name label
  • Disabled button text remains legible: white on #737373 is 4.74:1
  • Availability results announced via aria-live polite
  • Input border uses #949494 (3.03:1) instead of the observed near-invisible grey

Security

  • Validate subdomains with zod on server; block reserved names and homoglyph-like patterns
  • RLS on tenants: public can select subdomain and emoji only; insert requires auth and sets owner_id = auth.uid(); update/delete owner or admin only
  • Admin routes check role server-side, not only in middleware
  • Rate-limit tenant creation per user and per IP
  • Middleware must parse the Host header strictly and ignore spoofed forwarded hosts from untrusted proxies

Build prompt

The baseline every prompt in the catalog assumes, then this design’s own ten sections, from goal to guardrails.

The baseline
### How to use these prompts
Paste an entry's build prompt into your coding agent as the first message. Each prompt names its own stack, tokens and acceptance criteria; the rules below apply to all of them and can be prepended once per project.

### Engineering baseline
- TypeScript strict mode, no `any`, small typed components, feature folders, and one source of truth for design tokens (CSS variables consumed by Tailwind).
- Validate every input with a shared zod schema on the client and again on the server or edge function. Never trust client-side checks alone.
- Show loading, empty and error states for every async view. Surface errors in plain language with a retry, and log details to the console in development only.
- Keep secrets out of the bundle. Only publishable keys (for example a Supabase anon key) belong in client code; service-role keys, API keys and webhooks live in server or edge-function environment variables.

### Data and auth baseline (full-stack entries)
- Enable Row Level Security on every table before inserting data. Default-deny, then add owner-scoped policies (`auth.uid() = user_id`) and explicit role checks for admin views.
- Store roles in a separate table checked by a security-definer function, never in a user-editable profile field.
- Upload files to private storage buckets with size and MIME limits, and serve them through signed URLs.
- Rate-limit public endpoints (forms, auth, AI calls) and add a honeypot field or captcha to anonymous forms.
- Take payments through a hosted checkout and verify webhooks by signature. Never handle raw card data.

### Accessibility and UX baseline
- Target WCAG 2.2 AA: 4.5:1 contrast for normal text and 3:1 for large text, input borders, focus rings and meaningful icons or chart lines. Every palette in this catalog lists its verified pairs; re-check with a contrast tool after any colour change.
- Keep body text at 16px or larger with 1.5 line height, nothing below 12px, no light weights under 24px, and uppercase only for short labels.
- Give every interactive element a visible focus ring, full keyboard support, semantic landmarks, labelled form fields, and alt text on meaningful images.
- Respect `prefers-reduced-motion` for every animation. Give drag-and-drop and carousels keyboard and button alternatives.
- Build mobile-first and test at 375px, 768px and 1280px.

### Content guardrails
- Use original copy, fictional sample data and placeholder or licensed imagery. Do not reuse another product's name, logo, screenshots or marketing text.
- Label demo testimonials and metrics as samples. Collect the minimum personal data the feature needs.

### SaaS screen baseline
- Design every screen for its full set of states: first-run empty, loading skeleton, partial data, error with retry, permission-denied, and success feedback. Each entry lists the states its screen needs.
- Keep destructive actions (delete, revoke, downgrade, remove member) behind a confirmation that names the object, and prefer undo over a second dialog where the action is reversible.
- Enforce authorisation on the server for every action a screen exposes. Hiding a button is not access control; check the role again in the API or RLS policy.
- Never show secrets (API keys, tokens) in full after creation. Show them once, then mask them, and offer rotate and revoke.
- Keep the app shell (navigation, workspace switcher, account menu) consistent across screens, and preserve filters, sort and scroll position when the user navigates back.
### Goal
Build **Baywick**, a multi-tenant starter where every customer gets a subdomain. A visitor on the root domain claims a name, picks an emoji as their mark and lands on their own live page at `name.rootdomain`. Admins manage tenants from a protected table. The code should make host-based routing, per-tenant rendering and shared layouts obvious and easy to extend.

### Stack
Next.js (App Router) + React + TypeScript, Tailwind CSS, shadcn/ui on Radix, lucide-react, react-hook-form + zod. Supabase for Auth (admin sign-in, optional owner sign-in) and Postgres for tenants. Next.js middleware rewrites requests by Host header to `/s/[subdomain]`. A small cache layer (in-memory LRU or edge key-value) may sit in front of tenant lookups.

### Pages & layout
1. **Root landing (`/`)**: full-height soft gradient from `--wash` to white. Centred domain-style title, one-line subtitle ("Claim a subdomain and give it a face"), then a 400px card: *Subdomain* input joined to a grey suffix addon reading `.rootdomain`; *Icon* field showing the chosen emoji or "No icon selected" with a Choose emoji button; helper line; full-width dark "Create subdomain" button. Under the input, a live preview of the full URL.
2. **Tenant page (`/s/[subdomain]`, served at the subdomain)**: large emoji, "Welcome to {name}", a paragraph of placeholder tenant content, link back to root.
3. **Admin (`/admin`)**: top bar with title and Sign out; table of tenants (emoji, subdomain, owner, created, Visit, Delete). Search box above.
4. **404 for unknown subdomains** with a "Claim this name" link.
5. **Responsive**: full-width card with 16px gutters on phones; admin rows stack into cards below 640px.

### Design system
- Colors: `--wash: #eff4fb`, `--surface: #ffffff`, `--fg: #0a0a0a`, `--muted: #707070`, `--addon: #f5f5f5`, `--input-border: #949494`, `--primary: #171717`, `--disabled: #737373`, `--focus: #2563eb`, `--danger: #dc2626`.
- Fonts: Pontano Sans; title 700 at 30px (-0.02em); tenant heading 700 at 36px; body 400 at 16px/1.5; labels 500 at 14px; nothing under 14px.
- Spacing: 4px base; card padding 24px; field gap 16px; table rows 48px.
- Radius: card 12px, inputs and buttons 8px, popover 12px.
- Shadows: card `0 1px 3px rgb(0 0 0 / 0.08)`; popover `0 8px 24px rgb(0 0 0 / 0.12)`.
- Motion: 150ms popover fade/scale; no motion under reduced motion.

### Components & interactions
ClaimCard, InputWithSuffix, UrlPreview, EmojiPicker (Radix Popover; search, keyboard grid, recent emoji), SubmitButton, TenantPage, AdminTenantTable (TanStack Table optional), DeleteTenantDialog (type the subdomain to confirm), Toast. The subdomain field lowercases input, strips invalid characters and checks availability after 400ms. The button stays disabled until the name is valid and free and an emoji is chosen; helper text says what is missing. Success redirects to the new subdomain with a toast.

### Data & state
`tenants(id uuid, subdomain text unique check (subdomain ~ '^[a-z0-9](?:[a-z0-9-]{1,30}[a-z0-9])$'), emoji text, owner_id uuid null, created_at)`, `reserved_names(name text primary key)` seeded with www, admin, api, app, mail, status, `admins(user_id primary key)`. Server actions create and delete tenants; the tenant page is fetched by subdomain with 60s caching and revalidated on change. Local dev supports `name.localhost:3000`.

### Accessibility
Labels on every field; the suffix is announced via `aria-describedby`. The emoji picker is a labelled dialog with a searchable grid, roving tabindex and emoji names as labels. Availability and errors are announced politely. Disabled buttons stay legible and have an explanation. Focus ring 2px `--focus` with 2px offset. The tenant page uses one h1 and keeps the emoji decorative with the name in text.
Verified contrast: body text on gradient top: #0a0a0a on #eff4fb = 17.92:1; helper text on card: #707070 on #ffffff = 4.95:1; suffix text on addon: #707070 on #f5f5f5 = 4.54:1; primary button label: #ffffff on #171717 = 17.93:1; disabled button label: #ffffff on #737373 = 4.74:1; input border on white: #949494 on #ffffff = 3.03:1; focus ring on gradient: #2563eb on #eff4fb = 4.68:1; destructive text on white: #dc2626 on #ffffff = 4.83:1.

### Security
RLS on `tenants`: anyone may select `subdomain, emoji`; insert only when authenticated (or via a rate-limited server action) with `owner_id = auth.uid()`; update and delete only by owner or a user present in `admins`. `admins` and `reserved_names` are not client-readable beyond what is needed. Validate names server-side with zod and reject reserved and confusable names. Rate-limit creation per IP and user. Parse the Host header strictly against the configured root domain and ignore untrusted forwarded headers. Admin delete requires typed confirmation and is logged.

### Performance & SEO
Middleware does no database calls; tenant lookup happens in the cached server component. Tenant pages set their own title and meta from the tenant name. Root landing is statically rendered. Add `noindex` to admin. Keep the emoji picker lazily loaded.

### Guardrails
- Use invented tenant names in seeds (for example "harbor-bakes", "quietfox").
- Don't imply real hosting providers in UI copy; say "your root domain".
- No copied logos or UI; neutral component-library styling only.
- Acceptance criteria:
  - [ ] Claiming a valid name creates the tenant and the subdomain resolves immediately
  - [ ] Reserved, taken and malformed names are blocked with clear messages
  - [ ] Unknown subdomains return the custom 404
  - [ ] Non-admins cannot reach or call admin actions
  - [ ] Emoji picker is fully usable by keyboard

Open the builderAll templatesThis palette on its own