vercel/platforms
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
15 files reviewed·July 9, 2026
More findings
Verdict
Visual restraint is genuine, gray typography and simple layouts show real taste, but the interactions guarding tenant data are almost nonexistent. An unauthenticated admin dashboard sitting next to a one-click destructive delete is the biggest risk here, not the gradients or spacing.
Files Rams reviewed
app/page.tsx
app/s/[subdomain]/page.tsx
app/admin/page.tsx
app/layout.tsx
app/admin/dashboard.tsx
app/globals.css
app/not-found.tsx
app/subdomain-form.tsx
components/ui/button.tsx
components/ui/card.tsx
components/ui/dialog.tsx
components/ui/emoji-picker.tsx
components/ui/popover.tsx
components/ui/input.tsx
components/ui/label.tsx
Accessibility
Error message text fails contrast at 3.64:1 against white
The submission error `<div className="text-sm text-red-500">{state.error}</div>` renders red-500 (#ef4444) text-sm on a white form background.
Why it matters
3.64:1 fails WCAG AA's 4.5:1 threshold for small text, so users with low vision struggle to read exactly the message telling them why their subdomain wasn't created.
Fix
Darken the red to a shade that clears 4.5:1 against white, such as red-600 or red-700.
<div className="text-sm text-red-500">{state.error}</div><div className="text-sm text-red-700">{state.error}</div>Corner navigation link has a tap target far under the 44px touch minimum
The `{rootDomain}` link ("`rootdomain.com`") is rendered as bare `text-sm` text with no padding, inside a `top-4 right-4` positioned wrapper.
Why it matters
At roughly 14-20px tall with no padding, this is well under the 44x44px target size platform guidance recommends, making it hard to tap accurately on mobile, especially in a corner where mis-taps are common.
Fix
Give small interactive links padding so the clickable area reaches at least 44x44px.
className="text-sm text-gray-500 hover:text-gray-700 transition-colors"className="inline-flex items-center px-3 py-2 text-sm text-gray-500 hover:text-gray-700 transition-colors"Primary CTA's tap target falls short of comfortable mobile size
The only action on the page, `Create {subdomain}` / `Go to {rootDomain}`, is a `Link` styled with `px-4 py-2 text-sm`, which resolves to roughly 36px tall.
Why it matters
This clears the 24px WCAG AA floor but sits below the 44px platform guidance for primary actions, and this is the single button a user has to tap to recover from a broken subdomain, often from a mobile browser.
Fix
Increase vertical padding on the primary CTA to reach at least a 44px tap height.
className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"className="rounded-md bg-blue-600 px-4 py-3 text-sm font-medium text-white hover:bg-blue-700"UX
Admin dashboard renders tenant data with no authentication check
`AdminPage` calls `getAllSubdomains()` and passes every tenant straight into `<AdminDashboard tenants={tenants} />` with only a `// TODO: You can add authentication here` comment marking the gap. There is no auth check, redirect, or unauthorized state before the full tenant list renders.
Why it matters
Anyone who reaches this route sees the complete subdomain list for every tenant. This is a reachable state (unauthenticated visitor) that the component never accounts for, and it ships in production exactly as written.
Fix
Gate the data fetch behind a server-side session check and redirect or render an unauthorized state before calling getAllSubdomains().
// TODO: You can add authentication here with your preferred auth provider
const tenants = await getAllSubdomains();const session = await getSession();
if (!session?.isAdmin) redirect('/login');
const tenants = await getAllSubdomains();Delete button destroys a tenant subdomain with zero confirmation
The trash icon `<Button variant="ghost" size="icon" type="submit">` inside each tenant card submits `deleteSubdomainAction` directly on click, no confirm dialog, no undo.
Why it matters
A single misclick on any card permanently removes a live subdomain with no recovery path, which is the exact failure mode destructive-action confirmation exists to prevent.
Fix
Wrap the delete trigger in an AlertDialog that requires explicit confirmation before submitting the form.
<Button
variant="ghost"
size="icon"
type="submit"
disabled={isPending}
className="text-gray-500 hover:text-gray-700 hover:bg-gray-50"
><AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="ghost"
size="icon"
type="button"
disabled={isPending}
aria-label={`Delete ${tenant.subdomain}`}
className="text-gray-500 hover:text-gray-700 hover:bg-gray-50"
>Want this on your repo?
Rams reviews your next PR automatically and posts inline fix suggestions.
Review my public repo for freeSpacing
Redundant mt-8 duplicates spacing already set by parent's space-y-8
The form card div has both a parent `space-y-8` (which already applies `margin-top` to non-first children) and its own explicit `mt-8` class. Two rules targeting the same margin-top property on the same element is dead weight in the class list.
Why it matters
Redundant spacing utilities make it unclear which value actually controls the layout, so a future edit to one and not the other silently does nothing, costing debugging time.
Fix
Let the parent's space-y utility own the gap between children and drop the duplicate margin class.
<div className="mt-8 bg-white shadow-md rounded-lg p-6"><div className="bg-white shadow-md rounded-lg p-6">Typography
Color
Components
Motion
Craft
Working well
- Putting the form inside a single `shadow-md rounded-lg` card against a plain page gives the layout one clear focal point: there's exactly one thing to interact with, and the corner-anchored 'Admin' link stays visually secondary by using plain text instead of a button treatment.
- The page commits to a single heading and a single supporting line: "Welcome to {subdomain}.{rootDomain}" then "This is your custom subdomain page." There's no competing element fighting for attention, so the emoji, heading, and subtext read in a clean top-to-bottom order.
- Fetching `tenants` in the server component and handing them to `<AdminDashboard tenants={tenants} />` as a prop keeps data-fetching separate from presentation. This is the right split: the dashboard component stays reusable and testable without owning the fetch logic.
- The CTA label changes with context: "Create {subdomain}" when a specific subdomain is missing versus "Go to {rootDomain}" for the generic case. Naming the actual outcome instead of a flat "Get Started" is exactly what a recovery action on an error page should do.
Score your own repo with Rams.
Free on public repos. Rams reviews every pull request for accessibility, color, type, spacing, components, motion, UX, and craft, and posts inline fixes.