Skip to content

Give / somewhere to go on every shape of deployment - #1

Merged
holzmanshmuel merged 1 commit into
mainfrom
holzmanshmuel/holzman-135-torim-the-root-page-is-a-dead-end-on-a-multi-tenant
Aug 26, 2026
Merged

Give / somewhere to go on every shape of deployment#1
holzmanshmuel merged 1 commit into
mainfrom
holzmanshmuel/holzman-135-torim-the-root-page-is-a-dead-end-on-a-multi-tenant

Conversation

@holzmanshmuel

Copy link
Copy Markdown
Owner

Closes HOLZMAN-135.

/ rendered one fixed placeholder whose copy assumed a single-business install — "The booking page for this business isn't live yet" — which on a multi-tenant instance is true of nothing, and it offered no link anywhere. That made it a dead end, and a load-bearing one: not-found.tsx sends every visitor who follows a stale link to /, so the escape hatch from a 404 was a wall.

The ticket's stated blocker doesn't hold

HOLZMAN-135 assumed the single-business redirect was blocked by RLS. It isn't — torim.businesses is one of the three tables deliberately outside RLS (see the header of scripts/sql/001_tenancy.sql), precisely because a public booking slug has to resolve before any tenant context exists. findBusinessBySlug() already reads it through systemQuery on every booking page load.

So neither proposed workaround was needed: no env-configured landing path, no new SECURITY DEFINER function.

What this does

State Behaviour
empty First-run page — "No business is set up yet" → CTA to /login
single 307 → /b/<slug>. The common self-hosted case: one salon, one box
multi Landing: what Torim is, sign-in for owners, "use the link your business gave you" for customers

New describeInstance() uses SELECT slug … LIMIT 2. Two rows is all it takes to tell none from one from many, so a tenant list is never assembled and therefore cannot leak, however large the instance grows. The multi page states that as a property rather than silently omitting a directory.

/ is also the one route that must never 500, so an unreachable database degrades to the shared landing — true of every instance, names no business — rather than throwing.

The false string home.description is gone from both dictionaries.

Verified

Against a real Postgres on the production build, all four states: 0 businesses → 200 first-run page; 1 → 307 to /b/sole-salon, following through to a rendered booking page; 2 → 200 landing with 0 grep matches for either tenant's name or slug in the HTML; database unreachable → 200 landing, error logged, no 500. Hebrew renders RTL throughout.

Suite 512 passed / 39 files (baseline 508/38 — the 4 new ones are src/lib/instance-shape.test.ts).

🤖 Generated with Claude Code

https://claude.ai/code/session_01YZy5uLuHnuieYGrjRdwZCa

`/` rendered one fixed placeholder whose copy assumed a single-business install
— "The booking page for this business isn't live yet" — which on a multi-tenant
instance is true of nothing, and it offered no link anywhere. That made it a
dead end, and a load-bearing one: not-found.tsx sends every visitor who follows
a stale link to `/`, so the escape hatch from a 404 was a wall.

There is no single true `/` here, because there is no single shape of
deployment. So ask, and answer accordingly:

  empty  — nothing created yet, so the visitor is the operator who just
           deployed this. Send them to sign in and create the first business.
  single — the common self-hosted case: one salon, one box. `/` *is* that
           shop's booking page, so go there. No env var, no configuration —
           one business is one business.
  multi  — say what Torim is, point owners at sign-in, and tell customers to
           use the link their business gave them.

The multi case deliberately does not list the businesses, and says so on the
page: people who trim a URL are owed a way forward, not a directory of everyone
hosted alongside them. `describeInstance()` stops at `LIMIT 2` — two rows is all
it takes to tell none from one from many — so that list is never assembled and
therefore cannot leak, however large the instance grows.

The ticket assumed the single-business redirect was blocked by RLS. It is not:
`torim.businesses` is one of the three tables deliberately outside RLS (see the
header of scripts/sql/001_tenancy.sql) precisely because a public booking slug
has to resolve before any tenant context exists. Reading it through the existing
systemQuery path routes around no policy, so neither an env-configured landing
path nor a new SECURITY DEFINER function is needed.

`/` is also the one route that must never 500, so a database that does not
answer degrades to the shared landing rather than throwing. That page is true of
every instance — it names no business and promises nothing about this
deployment — so a single-business install loses one redirect while the database
is down, and nobody reads a false sentence.

Verified against a real Postgres in all four states: empty renders the first-run
page (200), one business 307s to `/b/<slug>`, two render the landing with no
tenant name in the HTML, and an unreachable database renders the landing rather
than a 500. Hebrew renders RTL throughout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZy5uLuHnuieYGrjRdwZCa
@holzmanshmuel
holzmanshmuel merged commit 572a903 into main Aug 26, 2026
1 check passed
@holzmanshmuel
holzmanshmuel deleted the holzmanshmuel/holzman-135-torim-the-root-page-is-a-dead-end-on-a-multi-tenant branch August 26, 2026 14:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant