---
title: "Tenancy"
description: "Resolve tenants before panel access and scope every resource operation."
image: "https://adonia.pages.dev/og.png"
version: "next"
---

> Documentation Index
> Fetch the complete documentation index at: https://adonia.pages.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Tenancy

Adonia tenancy is fail-closed. A tenant panel needs both a resolver and an address source: a dynamic `domain(':tenant.example.com')` or `tenantParam('tenant')`. The tenant middleware runs before panel access and every controller. Returning `null` or throwing produces the panel 404.

## Resolve the tenant

```ts
import { Panel } from '@adonia/core'

interface Tenant {
  id: number
  slug: string
  name: string
}

declare function findTenant(slug: string): Promise<Tenant | null>

export const admin = Panel.make('admin')
  .path('/admin')
  .tenantParam('tenant')
  .tenant((ctx) => findTenant(String(ctx.params.tenant)))
  .tenantSwitcher((_ctx, tenant) => {
const current = tenant as Tenant
return [
  {
    id: current.id,
    label: current.name,
    url: `https://${current.slug}.example.com/admin`,
    current: true,
  },
]
  })
```

For a host-mounted panel, use `.domain(':tenant.example.com')` and read the dynamic route/domain parameter in the resolver. Switcher destinations must be absolute HTTP(S) URLs; Adonia rejects duplicate ids, unsafe protocols, and non-absolute destinations.

The resolved value is `ctx.adonia.tenant`. It reaches resource queries, descriptor compilation, brand/navigation callbacks, search, widgets, authorization, and validator metadata.

## Scope every resource

On a tenant panel, every reachable resource must declare `static tenantScope`:

```ts
import { BaseResource, type SchemaBuilder, type SchemaComponent } from '@adonia/core'
import Post from '#models/post'

export default class PostResource extends BaseResource<typeof Post> {
  static override model = Post
  static override slug = 'posts'
  static override tenantScope = 'tenantId'

  override schema(_schema: SchemaBuilder): SchemaComponent[] {
return []
  }
}
```

The string form adds `where('tenantId', tenant.id)`, stamps the key on create, and verifies ownership before update, delete, restore, and action execution. A function form is available for composite or indirect ownership:

```ts
import {
  BaseResource,
  type SchemaBuilder,
  type SchemaComponent,
  type TenantScope,
} from '@adonia/core'
import Post from '#models/post'

interface Tenant {
  id: number
}

const scopePosts: TenantScope = (query, tenant) => {
  query.where('tenantId', (tenant as Tenant).id)
}

export default class ScopedPostResource extends BaseResource<typeof Post> {
  static override model = Post
  static override slug = 'scoped-posts'
  static override tenantScope = scopePosts

  override schema(_schema: SchemaBuilder): SchemaComponent[] {
return []
  }
}
```

`static tenantScope = false` is an explicit global-resource opt-out and is logged at boot. Omitting the declaration is `E_ADONIA_UNSCOPED_RESOURCE`; do not use `false` to silence the doctor for tenant-owned data.

## Validation and ownership

Framework unique/exists checks receive `tenantId` and edit `recordId` through validator metadata. Validator cache identity remains resource slug plus mode; never compile a tenant id into a cached Vine schema. Custom validation that reads the database must use the same resolved tenant and explicit allowlists.

Tenant scoping applies to lists, detail reads, relations, global search, widgets, actions, and JSON endpoints. Application policies still authorize the actor inside the tenant; scoping and authorization solve different problems.

## Routing and cookies

Path example: `/admin/acme/posts`. Host example: `https://acme.example.com/admin/posts`. Cross-host links and switcher options must be absolute.

Host-only session cookies isolate subdomains by default. Configure an apex cookie such as `.example.com` only when cross-subdomain SSO is intended, and configure the XSRF cookie consistently. Dynamic local domains also need matching Vite `server.allowedHosts`; HMR may need an explicit public host. Run:

```sh
node ace adonia:doctor --only=cookie-domain --only=vite-allowed-hosts --only=shield-xsrf --only=unscoped-resources
```

See [Deployment and security](/guide/deployment-security) for proxy, cookie, cache, upload, queue, and secret requirements, and [Nested resources](/guide/nested-resources) for composing parent and tenant scopes.

Source: https://adonia.pages.dev/guide/tenancy/index.mdx
