nuxt-x-tenancy
nuxt-x-tenancy
Multi-tenant resolution layer for Nuxt 4. Bridges the better-auth active organization (via nuxt-x-auth-better) into a tenant vocabulary: URL/host resolution strategies (path, subdomain, header), X-Tenant-ID request injection, and per-tenant useXCrud cache scoping with a flush on switch.
ID contract: tenant id === better-auth organization id. No translation layer anywhere — see Backend requirements.
Installation
npm install @xenterprises/nuxt-x-tenancy
Peers (@xenterprises/nuxt-x-app ^1.1.0, @xenterprises/nuxt-x-auth-better ^1.2.0, nuxt ^4.0.0) are auto-installed by npm ≥ 7. The layer's own nuxt.config.ts extends both sibling layers, so the consumer extends only tenancy and gets the whole stack.
What the consumer writes
The batteries are included: the global tenant middleware, the $fetch header plugin, the useTenant composable, and both XT* components come from the layer. A minimal consumer writes three things:
extendsinnuxt.config.ts:
// nuxt.config.ts
export default defineNuxtConfig({
extends: ['@xenterprises/nuxt-x-tenancy'],
})
app/app.config.ts— it must live in the consuming project'sapp/directory (Nuxt 4srcDirconvention); at the project root your overrides are silently ignored. Everything is optional — these are the defaults:
// app/app.config.ts
export default defineAppConfig({
xTenancy: {
strategy: 'path', // 'path' | 'subdomain' | 'header'
pathPrefix: '/org', // path strategy: /org/:slug
// rootDomain: 'app.example.com', // required for the subdomain strategy
},
})
- The auth-better requirements — a better-auth server with the organization plugin enabled and
xAuth.organization.enabled: true(plusNUXT_PUBLIC_X_AUTH_BASE_URL). See Backend requirements.
Then pass the tenant scope to tenant-scoped CRUD calls:
<script setup lang="ts">
const { tenant, tenants, isResolving, scope, switchTenant } = useTenant()
const tasks = useXCrud<Task>('/api/tasks', { scope }).all()
</script>
<template>
<XTTenantSwitcher />
<XTTenantResolver>
<template #default="{ tenant: active }">
<p>Active: {{ active.name }}</p>
</template>
</XTTenantResolver>
</template>
That's it — the global middleware resolves /org/:slug (or the subdomain) into the active organization, the request plugin adds X-Tenant-ID to every $fetch/useXCrud call, and switchTenant() flushes the old tenant's CRUD cache.
Resolution strategies (xTenancy.strategy)
path(default) — thetenant.globalmiddleware reads the slug from${pathPrefix}/:slug(default/org/:slug, matching auth-better's org routes) and activates it. Unresolvable slugs redirect toxTenancy.fallbackRoutewhen set.subdomain— the middleware maps the request host againstxTenancy.rootDomain(acme.app.example.com→ slugacme). Requires wildcard DNS.header— no route effects; the tenant follows the session's active org and is carried to APIs via theX-Tenant-IDheader.
Full chain details, middleware ordering, and the upstream better-auth caveat: Resolution chain.
Request integration
$tenantHeaders()— plugin-provided factory returning{ 'X-Tenant-ID': <id> }(or{}); spread it into SSR/custom requests. Header injection is deliberately not done server-side:globalThis.$fetchis shared across SSR requests and a per-request closure would leak tenants.- Client
$fetchwrapper — every client-side request that doesn't already set the header gets it (per-call headers win, case-insensitive). CoversuseXCrud, which readsglobalThis.$fetchat call time. Opt out:xTenancy.injectHeader: false. - CRUD scope — pass
scopetouseXCrud; aRefscope means live lists refetch on switch into a fresh cache entry, andswitchTenant()callsuseXCrud.clearScope(oldScope)so the old tenant's cache is dropped. Opt out:xTenancy.crudScope: false.
What the layer provides
Components (auto-imported, XT* prefix):
XTTenantSwitcher—USelectMenuoftenants; selecting one callsswitchTenant(). Props:placeholder,hideWhenSingle.XTTenantResolver— gate for tenant-scoped content:loadingslot whileisResolving, default slot (scopedtenant) when active,emptyslot otherwise.
Composables:
useTenant()— tenant bridge:tenant,tenants,isResolving,scope,switchTenant(id),resolveBySlug(slug).
Middleware & plugins:
tenant.global— path/subdomain resolution (runs after auth-better'sauth.global/organization.global).tenantplugin — provides$tenantHeaders()and installs the client-side$fetchX-Tenant-IDwrapper.
Pages / server routes: none — the layer is client/middleware-only by design. Tenant pages are the consumer's or auth-better's /org/* routes; the backend contract is documented in Backend requirements.
App config options (xTenancy)
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Master switch (middleware + plugin). |
strategy | 'path' | 'subdomain' | 'header' | 'path' | Resolution strategy. |
pathPrefix | string | '/org' | Tenant URL prefix (path strategy). |
rootDomain | string | undefined | Root domain (subdomain strategy, required there). |
headerName | string | 'X-Tenant-ID' | Tenant header name. |
injectHeader | boolean | true | Client-side global $fetch header injection. |
crudScope | boolean | true | Scope flushing on switchTenant(). |
fallbackRoute | string | undefined | Redirect target for unresolvable slugs. |
Environment variables
None of its own. Inherits NUXT_PUBLIC_X_AUTH_BASE_URL / auth-path from nuxt-x-auth-better.
AI Context
package: "@xenterprises/nuxt-x-tenancy"
version: "0.1.0"
use-when: >
Building a multi-tenant Nuxt 4 SaaS on better-auth organizations. The
layer resolves the active tenant from the URL (path or subdomain) or
treats it as API-only (header), injects X-Tenant-ID into every client
request, and scopes useXCrud caches per tenant with a flush on switch.
tenant id === better-auth organization id. Extends nuxt-x-app and
nuxt-x-auth-better itself, so consumers write a single extends entry.
Release Notes
Detailed release history for @xenterprises/nuxt-x-affiliate — what shipped in v0.3.5, v0.4.0, v0.4.1, v0.5.0, and v0.6.0, plus the breaking dashboard-folder consolidation landing in the next minor.
useTenant
Tenant bridge composable for nuxt-x-tenancy — current tenant, tenant list, CRUD cache scope, and switch/resolve actions on top of the better-auth active organization.
