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.md for 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 in proxy.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.com host. Stored in lib/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 not BYPASSRLS, 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 the BYPASSRLS owner connection for platform-global work (provisioning, lifecycle, audit); request paths must not use it.

  • Control plane — the superadmin console (app/control-plane, host admin.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 in lib/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) — see lib/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 in lib/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-neutral neutral theme; 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".

  • Proxyproxy.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 replaces middleware.tsdo not add a middleware.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/audit into the audit_events platform-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 (openpaid / 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 crm module: a per-tenant WhatsApp Business Cloud API integration for contacts, conversations, a deterministic booking bot, and review/reminder sweeps. Lives in lib/integrations/whatsapp (transport) and lib/whatsapp (flows). See whatsapp-integration.


Where to go next

User guides (per surface):

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.