Platform Overview & Glossary
This is the map of Wahy: what it is, how tenants and their sites are isolated, the four
surfaces you can reach, and a glossary of every key term with a pointer into the code. It
is written for new engineers and for curious tenant owners — deeper mechanics live in
the linked docs/* files; this page stays at altitude.
Naming note: the product and brand are Wahy. The codebase still says nyoosha internally (package names, some comments). They refer to the same platform. See the repo-root
CLAUDE.mdfor the authoritative framing.
What Wahy is
Wahy is a multi-tenant SaaS — a "bookable website in minutes" for Kenyan service SMBs (lash studios, barbers, nail techs, and similar). Each business is a tenant: it gets a public website, a self-service admin CMS to manage content and bookings, billing, and an optional WhatsApp CRM, all from one shared codebase and one shared database.
Lasheyeisland is tenant #1 — a solo luxury mobile lash studio in Nairobi (artist Sunny). It is the reference tenant the platform was first built around; everything else generalizes from it.
The tenancy model
A tenant is identified by its subdomain. The tenant axis is the host, not a URL
path — lasheyeisland.wahylabs.com is one tenant, another.wahylabs.com is a different
one. There is no /lasheyeisland/... path prefix. A tenant may also point a custom
domain at the platform (via CNAME) and have it resolve to the same site — custom-domain
resolution lives in lib/tenancy/resolver.ts against the lib/db/schema/tenant-domains.ts
table.
Every request is resolved to a tenant in the proxy (proxy.ts) from the incoming Host,
with the result cached in Redis. The proxy strips and re-sets the x-tenant-* headers so a
client can never spoof its tenant.
Isolation is enforced in the database with Postgres Row-Level Security (RLS). Every
tenant-scoped table carries a tenant_id and has RLS ENABLEd + FORCEd; the app connects
as a non-owner, non-BYPASSRLS role, and each query runs inside a transaction that sets the
current tenant (withTenant). A query with the wrong (or no) tenant context returns zero
rows rather than another tenant's data — this is the platform's most important safety
property, and cross-tenant isolation tests are the safety net.
Depth: tenancy (resolution chain, anti-spoofing, caching) and data-layer (roles, RLS rules, migrations).
The four surfaces
Wahy exposes four distinct surfaces. Each has a different audience, design language, and URL shape.
| Surface | Who uses it | Live URL shape |
|---|---|---|
| Public tenant site | The tenant's customers (public, indexable) | https://<slug>.wahylabs.com/en |
| Tenant admin CMS | The tenant owner (e.g. Sunny) | https://<slug>.wahylabs.com/admin |
| Superadmin control plane | Wahy platform operators | https://admin.wahylabs.com |
| Marketing / signup | Prospective tenants | https://wahylabs.com · https://wahylabs.com/signup |
Product docs (this page included) are served at https://docs.wahylabs.com.
Public tenant site
The editorial, per-tenant website — home, about, contact, journal, and services pages. It
is locale-prefixed (/en, /sw) and served per-subdomain. Routes live under
app/(site)/[locale] and render through the block/theme system (below).
- Home:
https://<slug>.wahylabs.com/en - About / Contact / Journal / Services:
https://<slug>.wahylabs.com/en/about(etc.)
Tenant admin CMS
Where the owner self-manages their business — gallery, FAQs, journal posts, settings,
bookings, availability, billing, and the WhatsApp CRM. Auth.js-gated to members of that
tenant. Reached at https://<slug>.wahylabs.com/admin. The live Studio editor for the
public site is a route within it (see Studio editor in the glossary).
Superadmin control plane
The platform operators' console — manage the tenant lifecycle, billing, audit trail,
impersonation, jobs, packages, and refunds. It lives on a reserved host
(admin.wahylabs.com); the proxy rewrites it to the app/control-plane segment and runs
no tenant resolution there. Gated by superadmin identity, not tenant membership.
Marketing / signup
The Wahy front door at the apex domain — its own brand (not a tenant), served from the
app/marketing segment. Prospective owners browse features/pricing at
https://wahylabs.com and self-provision a new tenant at https://wahylabs.com/signup.
Glossary
Each term links to where it lives in the code.
-
Tenant — one business on the platform (its own site, admin, data, entitlements). All tenant data is isolated by
tenant_id+ RLS. -
Tenant slug — the short, URL-safe identifier that becomes the subdomain label (e.g.
lasheyeisland). Validated at provisioning time (format, length, reserved-word checks). -
Subdomain axis — the design rule that the subdomain, not a path segment, selects the tenant.
<slug>.wahylabs.com= one tenant. Resolved inproxy.ts/lib/tenancy/resolver.ts. -
Custom domain — a tenant's own domain (via CNAME) that resolves to their Wahy site, in addition to their
<slug>.wahylabs.comhost. Stored inlib/db/schema/tenant-domains.ts. -
RLS (Row-Level Security) — Postgres per-row access policies (
ENABLE+FORCE) that scope every tenant-scoped table read/write to the current tenant. The app role is notBYPASSRLS, so isolation is enforced by the database itself. See data-layer. -
withTenant/withOwner— the two connection helpers (lib/infrastructure/db/types.ts).withTenant(tenantId, fn)runs a query inside a transaction with the tenant context set — the normal request path.withOwner(fn)runs on theBYPASSRLSowner connection for platform-global work (provisioning, lifecycle, audit); request paths must not use it. -
Control plane — the superadmin console (
app/control-plane, hostadmin.wahylabs.com). Tenant resolution does not run here. -
Superadmin — a platform operator, identified by
platform_role='superadmin'on their user row and gated by the helpers inlib/auth.ts. This is a positive role check, not a membership check. -
Impersonation — a superadmin's "log in as tenant" flow: a single-use cross-subdomain handoff token mints an owner-identity session on the tenant host, time-capped and audited. Implemented in
lib/impersonation. -
Module — an opt-in, billable capability a tenant can be entitled to (Website, Bookings, Journal, WhatsApp CRM). Defined in
lib/modules/registry.ts(billable: true). -
Primitive — an always-on capability that ships with every tenant regardless of entitlements (Settings, Availability, Billing). Also in
lib/modules/registry.ts(kind: "primitive",billable: false). -
Entitlement — the record that a given tenant has a given module enabled. Enforced in three layers (route guard,
assertEntitled()in each action, and RLS) — seelib/entitlements. -
Package — a sellable bundle of modules that a superadmin defines and assigns to a tenant (
lib/packages). The default Starter package is what a newly-provisioned tenant gets. -
Package version — an immutable snapshot of a package's module composition and prices. Editing a package copies forward into a new version and advances a pointer, so existing subscribers stay grandfathered on the version pinned to them. See
lib/packages. -
Block — a
{type, props}unit of the public site's page model. Blocks are assembled and rendered through the registry inlib/site/blocks/registry.tsx. -
Theme — a code-defined set of design tokens, a default block layout, and a copy catalog, selected per-tenant. Themes live in
lib/site/theme(default is the brand-neutralneutraltheme; Lasheyeisland has its own). -
Studio editor — the in-admin WYSIWYG editor for the public site: a three-pane route (
app/admin/(panel)/editor, reached in-product at/admin/editor) that edits a draft site config and publishes it. It materializes the code theme into an editable config, then renders "override mode". -
Proxy —
proxy.ts, the Node-runtime request interceptor that does tenant resolution, anti-spoof header handling, locale redirects, and control-plane/marketing routing. In Next.js 16 this replacesmiddleware.ts— do not add amiddleware.ts. -
Audit event — an append-only record of a platform action (tenant provisioned, transitioned, purged, impersonation started/ended, billing succeeded/failed, etc.). Written owner-side via
lib/auditinto theaudit_eventsplatform-global table. -
Subscription — a tenant's active plan record, pinned to one package version, tracking the billing period and status. Schema:
lib/db/schema/subscriptions.ts. -
Invoice — a billing document for a period or plan change (
open→paid/void). Schema:lib/db/schema/invoices.ts. A payment (lib/db/schema/payments.ts) is a single M-Pesa capture attempt that settles an invoice. -
Wallet — an append-only, signed per-tenant credit ledger (e.g. downgrade proration credits); the balance is the sum of its entries. Schema:
lib/db/schema/wallet-ledger.ts. -
Refund — a money-out reversal of a payment via M-Pesa (manual, superadmin-gated). Schema:
lib/db/schema/refunds.ts. -
WhatsApp CRM — the billable
crmmodule: a per-tenant WhatsApp Business Cloud API integration for contacts, conversations, a deterministic booking bot, and review/reminder sweeps. Lives inlib/integrations/whatsapp(transport) andlib/whatsapp(flows). See whatsapp-integration.
Where to go next
User guides (per surface):
- The public tenant site — blocks/themes, booking, i18n, SEO.
- The admin CMS (owner guide) — every admin surface and how to use it.
- Marketing & signup — the apex site and the public signup flow.
Engineering reference:
- tenancy — how a request resolves to a tenant, anti-spoofing, caching.
- data-layer — Postgres + Drizzle + RLS, the two connection roles, migrations.
- auth — Auth.js v5, memberships, the member-only login gate, superadmin.
- whatsapp-integration — the WhatsApp webhook + CRM ingestion.
- docs/README.md — the full documentation index.