Skip to main content

TenantRedirector

src/lib/clerk/TenantRedirector.jsx — client component rendered in layout.jsx on every page.

Consolidates four previously separate components into one: TenantSync, DemoOrgRedirect, NewTenantModal, and SlugChangeWarning.

Key concept: subdomain

getSubdomainClient() reads the hostname in the browser and returns the subdomain part, or null if there is none.

URLsubdomainWhat this page is
rat.gdnullRoot landing page — no org in the URL, user has not signed up yet or is being redirected
demo.rat.gd"demo"Shared demo landing page — same org as root, just with an explicit subdomain
acme.rat.gd"acme"A real tenant's subdomain — acme is the org's shortName in MongoDB

isDemoPage(subdomain) returns true for both null and "demo" because rat.gd and demo.rat.gd serve the exact same shared demo org. The server (getHostnameServer) maps both to shortName = "demo".

Which effects run where

Effectrat.gd (null)demo.rat.gd ("demo")acme.rat.gd ("acme")
TenantSync — sync Clerk active org to subdomain✗ no org to sync to✗ demo is a shared org, not a personal tenant✓ sets Clerk active org to acme
DemoOrgRedirect — poll memberships and redirect to personal demo-NNNN✓ main use case: user just signed up✓ same — dev has no webhooks so polling is the only mechanism✗ user is already on their tenant
NewTenantModal — poll user metadata and offer to visit new tenant✓ new users land here via OrgNotAvailable after sign-up
SlugChangeWarning — warn that org slug cannot be changed

Decision tree

Effect 1 — TenantSync

Runs on: real tenant subdomains only (subdomain is set and not "demo")

Sets the Clerk active organization to match the subdomain. Without this, Clerk would retain the previously active org across navigation.

subdomain set AND subdomain !== "demo" AND orgsLoaded
→ find membership whose slug === subdomain
→ if found and not already active: setActive({ organization: id })

Effect 2 — DemoOrgRedirect

Runs on: isDemoPage — both root domain (rat.gd) and demo subdomain (demo.rat.gd)

In production, a Clerk webhook creates a personal demo-NNNN org for the user and fires a redirect. In dev there are no webhooks, so this effect polls Clerk memberships directly as the only redirect mechanism.

isDemoPage AND orgsLoaded
→ demoAttemptsRef reset to 0 ← Bug A fix (was accumulating across re-runs)
→ if memberships already contain demo-NNNN slug:
redirect immediately to buildTenantUrl("demo-NNNN")
→ else:
start interval (2s, max 15 attempts)
on each tick: check memberships for demo-NNNN, redirect if found
NO revalidate() call ← Bug B fix (caused infinite effect restart loop)

Effect 3 — NewTenantModal

Runs on: all pages (no subdomain guard) — must run on demo.rat.gd too because OrgNotAvailable sends new users there after sign-up

Polls user.publicMetadata.newTenantSlug, set by the webhook after a new tenant org is created. Only polls for users created within the last 5 minutes.

userLoaded
→ if publicMetadata.newTenantSlug already set:
show modal immediately
→ else if user is recent (< 5 min old):
start interval (2s, max 15 attempts)
on each tick: user.reload(), check newTenantSlug, show modal if found

Render priority

Only one piece of UI is rendered at a time, in this order:

  1. NewTenantModal — shown on any page when newTenantSlug is set

    • "Visit" → clears metadata flag + redirects to demo-NNNN subdomain
    • "Dismiss" → clears metadata flag + closes modal
  2. SlugChangeWarning — shown on real tenant subdomains only (subdomain && subdomain !== "demo")

    • Displayed when activeOrg.publicMetadata.slugChangeWarning is set
    • "OK" → clears server-side flag + reloads org from Clerk
  3. null — nothing to display (root domain, demo subdomain with no pending actions)


Bug fixes included

BugSymptomFix
Bug A — premature bail-outattemptsRef carried over across effect re-runs triggered by userMemberships changes, hitting MAX_ATTEMPTS before a demo org was ever foundReset demoAttemptsRef.current = 0 at top of effect
Bug B — infinite restart looprevalidate() inside the interval triggered a userMemberships update → effect re-ran → interval restarted → loopRemoved revalidate() entirely; Clerk re-renders naturally when new data arrives