К основному содержанию
скилл CX Codex

multitenant — скилл для Claude Code: мультитенантность в Next.js

Скилл для Vovk.ts: разводит несколько тенантов по поддоменам в одном Next.js-приложении — прокси, сегменты, страницы.

Установить в Claude Code · Через Skills CLI (Vercel) Все способы и платформы ↓
npx skills add finom/vovk --skill multitenant -a claude-code
Открыть на GitHub

Что делает multitenant

Скилл описывает, как поднять несколько тенантов из одного Next.js-приложения на Vovk.ts. Схема такая: example.com — корневой тенант, admin.example.com — админка, customer.example.com — клиентский портал, <customer>.customer.example.com — инстанс конкретного клиента. Форму выбираете сами, скилл показывает обвязку под любую.

Работает в три слоя. src/proxy.ts ловит каждый запрос, зовёт multitenant() из vovk с URL, хостом и картой overrides и получает обратно { action, destination }. Дальше ветвление на NextResponse.rewrite, NextResponse.redirect или 404. Прокси именно переписывает путь, а не редиректит, поэтому admin.example.com/ остаётся в адресной строке, а Next.js внутри отдаёт src/app/admin/page.tsx. Бэкенд — именованный сегмент на тенанта, фронтенд — своя страница на тенанта.

В vovk.config.mjs нужно переключиться на сегментированный клиент: composedClient.enabled: false и segmentedClient.enabled: true. Плюс segmentNameOverride: '' для каждого тенанта, чтобы имя сегмента не попадало в URL клиентских вызовов. В targetHost подставляется корневой домен, и тут есть подвох: без ветки по process.env.VERCEL локальная разработка уйдёт на боевой хостнейм вместо localhost:3000. Создание отдельного сегмента, контроллеры внутри него и тонкости RPC скилл не разбирает — для этого есть соседние скиллы segment, procedure и rpc.

Какие задачи решает

  • развести несколько клиентов по поддоменам в одном Next.js-приложении
  • настроить переписывание путей через proxy.ts без редиректов
  • выдать каждому тенанту свой сегмент API и свою страницу
  • подключить wildcard-поддомены и локальные поддомены на localhost
  • переключить проект с composed-клиента на segmented

Примеры запросов агенту

  • Настрой мультитенантность: admin.example.com и customer.example.com должны обслуживаться одним приложением
  • Свяжи поддомен <customer>.customer.example.com с сегментом customer/[customer_name] через multitenant()
  • Переключи vovk.config.mjs на сегментированный клиент и проставь segmentNameOverride для тенантов

Как установить multitenant

1Через Skills CLI (Vercel)

npx skills add finom/vovk --skill multitenant -a claude-code

2Вручную: положить файлы

~/.claude/skills/multitenant/SKILL.md

Папка скилла целиком; для одного проекта — .claude/skills/multitenant/

По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов

Вопросы о multitenant

Как установить multitenant в Claude Code?

Выполните npx skills add finom/vovk --skill multitenant -a claude-code. Файл скилла появится по пути ~/.claude/skills/multitenant/SKILL.md.

Что делает скилл multitenant?

Показывает, как обслуживать несколько тенантов из одного Next.js-приложения на Vovk.ts: прокси с multitenant(), карта overrides, сегменты на бэкенде и страницы на фронтенде.

Чем этот скилл отличается от segment и procedure?

Здесь только обвязка мультитенантности. Создание и настройка отдельного сегмента — в скилле segment, написание контроллеров внутри сегмента — в procedure, детали сегментированного клиента — в rpc.

Исходный текст

SKILL.md · MIT
---
name: multitenant
description: Multi-tenant / multitenancy patterns in Vovk.ts — hosting multiple tenants from one Next.js app via subdomain routing. Covers the `multitenant()` helper from `vovk`, the Next.js `proxy.ts` file that wires it up, the `overrides` shape (static / dynamic / nested subdomain patterns), the `vovk.config.mjs` changes needed (switch to segmented client, `segmentNameOverride`), per-tenant frontend page routes, wildcard DNS, and local subdomains under `localhost`. Triggers on phrasings like "multi-tenant", "multitenancy", "per-tenant API", "per-tenant frontend", "host `admin.example.com`", "serve per subdomain", "tenant per customer", "wildcard subdomains", "route subdomain to segment". Does NOT cover creating or configuring an individual segment — hand off to the `segment` skill. Does NOT cover writing controllers inside a tenant segment — hand off to the `procedure` skill. Does NOT cover composed-vs-segmented client specifics beyond the flip — hand off to the `rpc` skill.
---

# Vovk.ts multi-tenant routing

Host multiple tenants from one Next.js app. Each tenant gets own **named segment** on backend, own **frontend page route** on frontend, wired together with subdomain-aware **proxy** calling `multitenant()` from `vovk`.

Typical shape:

- `example.com/` → root tenant (marketing home, or tenant picker).
- `admin.example.com/` → admin tenant — internal backend + UI.
- `customer.example.com/` → customer-facing portal (signup, landing, etc.).
- `<customer>.customer.example.com/` → per-customer instance.
- `pro.<customer>.customer.example.com/` → per-customer pro tier.

Pick any shape; this skill shows wiring for any.

## Architecture

Three layers, one request:

1. **Proxy (`src/proxy.ts`)** — intercepts every request, calls `multitenant()` with incoming URL, host, your `overrides` map, gets back `{ action, destination }`. Branches into `NextResponse.rewrite`, `NextResponse.redirect`, or 404.
2. **Named segment per tenant** on backend (`src/app/api/admin/[[...vovk]]/route.ts`, etc.). Each tenant's `api/*` calls land here after proxy rewrite.
3. **Per-tenant frontend page routes** on frontend (`src/app/admin/page.tsx`, etc.). Each tenant's non-api requests land here after proxy rewrite.

Proxy **rewrites** — does NOT redirect (except reserved-path guard, see below) — so `admin.example.com/` stays in user's address bar while Next.js internally serves `src/app/admin/page.tsx`.

## 1. Create a segment per tenant

```bash
npx vovk new segment admin
npx vovk new segment customer
npx vovk new segment customer/pro
```

Controllers go in `src/modules/...` as usual. See **`segment` skill** for segment shapes, `initSegment()`, nested paths; see **`procedure`** for controller authoring.

## 2. Switch to segmented client and override segment names

In `vovk.config.mjs`:

```js
/** @type {import('vovk').VovkConfig} */
const config = {
  composedClient: { enabled: false },
  segmentedClient: { enabled: true },
  outputConfig: {
    segments: {
      admin:          { segmentNameOverride: '' },
      customer:       { segmentNameOverride: '' },
      'customer/pro': { segmentNameOverride: '' },
    },
  },
};

export default config;
```

Why:

- `composedClient.enabled: false` + `segmentedClient.enabled: true` — switches from single all-segments RPC bundle to one client per segment. Each tenant's frontend imports only RPC it uses → lean bundles.
- `segmentNameOverride: ''` per tenant — leaves the segment name out of the URLs that segment's client calls (`/api/users`, not `/api/admin/users`), so the proxy's `{ from: 'api', to: 'api/<tenant>' }` rewrite routes them to the tenant's segment. The client folder keeps the segment name: `admin` pages import `@/client/admin`.

See **`rpc` skill** for full segmented-client reference.

## 3. Wire the proxy

`src/proxy.ts` (or `proxy.ts` at project root if `src/` not used):

```ts
import { NextRequest, NextResponse } from 'next/server';
import { multitenant } from 'vovk';

export default function proxy(request: NextRequest) {
  const { action, destination } = multitenant({
    requestUrl: request.url,
    requestHost: request.headers.get('host') ?? '',
    targetHost: process.env.VERCEL ? 'example.com' : 'localhost:3000',
    overrides: {
      admin: [
        { from: 'api', to: 'api/admin' },
        { from: '',    to: 'admin' },
      ],
      customer: [
        { from: 'api', to: 'api/customer' },
        { from: '',    to: 'customer' },
      ],
      '[customer_name].customer': [
        { from: 'api', to: 'api/customer' },
        { from: '',    to: 'customer/[customer_name]' },
      ],
      'pro.[customer_name].customer': [
        { from: 'api', to: 'api/customer/pro' },
        { from: '',    to: 'customer/[customer_name]/pro' },
      ],
    },
  });

  if (action === 'rewrite' && destination) return NextResponse.rewrite(new URL(destination));
  if (action === 'redirect' && destination) return NextResponse.redirect(new URL(destination));
  return NextResponse.next();
}

export const config = {
  matcher: ['/((?!static|.*\\.png|.*\\.svg|.*\\.ico|.well-known|_next/image|_next/static).*)'],
};
```

`targetHost` is your root domain — branch on `process.env.VERCEL` (or your own flag) so local dev uses `localhost:3000`, prod uses real hostname.

## 4. Per-tenant frontend pages

Each tenant override maps `from: ''` → `to: '<tenant>'`. Non-api requests to tenant subdomain rewrite to `/<tenant>` internally; Next.js serves from `src/app/<tenant>/page.tsx`.

File tree mirroring proxy example above:

```
src/app/
  page.tsx                                    ← root tenant (example.com/)
  admin/page.tsx                              ← admin tenant (admin.example.com/)
  customer/page.tsx                           ← customer landing (customer.example.com/)
  customer/[customer_name]/page.tsx           ← per-customer instance (<name>.customer.example.com/)
  customer/[customer_name]/pro/page.tsx       ← per-customer pro (pro.<name>.customer.example.com/)
```

`[customer_name]` is Next.js dynamic route segment. Populated from subdomain capture — `acme.customer.example.com` hits `[customer_name].customer` override → rewrites to `/customer/acme` → Next.js renders `src/app/customer/[customer_name]/page.tsx` with `params.customer_name === 'acme'`.

Mapping cheat sheet:

| Subdomain pattern | Override `to` (frontend) | Next.js page file |
|---|---|---|
| `admin.<root>` | `admin` | `src/app/admin/page.tsx` |
| `customer.<root>` | `customer` | `src/app/customer/page.tsx` |
| `<name>.customer.<root>` | `customer/[customer_name]` | `src/app/customer/[customer_name]/page.tsx` |
| `pro.<name>.customer.<root>` | `customer/[customer_name]/pro` | `src/app/customer/[customer_name]/pro/page.tsx` |

Inside per-tenant page:

- Import that tenant's segmented client from `@/client/<segment>` (`@/client/admin`, `@/client/customer`, `@/client/customer/pro`): `segmentedClient.outDir` (default `src/client`) plus the segment name. `segmentNameOverride` changes the URLs, not this path.
- Call RPC normally — `{ from: 'api', to: 'api/<tenant>' }` override rewrites every `fetch('/api/...')` so it lands in that tenant's segment on backend.

### Deeper frontend trees

Nest regular pages under tenant's folder. `src/app/admin/users/page.tsx` serves `admin.example.com/users` — proxy rewrite `to: 'admin'` becomes `to: 'admin'` for `/` and `admin/users` for `/users` (because `multitenant()` rewrites prefix, not whole path).

## The `multitenant()` helper

Signature (simplified):

```ts
multitenant({
  requestUrl: string,    // request.url
  requestHost: string,   // request.headers.get('host')
  targetHost: string,    // your root domain, e.g. 'example.com' or 'localhost:3000'
  overrides: Record<string, Array<{ from: string; to: string }>>,
}): {
  action: 'rewrite' | 'redirect' | null;
  destination: string | null;
  subdomains: Record<string, string> | null; // captured [placeholder] values, if any
  message: string;                            // human-readable summary for logs
}
```

### Overrides shape

| Key shape | Matches | Typical use |
|---|---|---|
| Static (`admin`) | `admin.<targetHost>` exactly | Fixed tenant (admin, customer, staff, …) |
| Dynamic (`[customer_name].customer`) | `<anything>.customer.<targetHost>` | Per-customer instance |
| Nested (`pro.[customer_name].customer`) | `pro.<anything>.customer.<targetHost>` | Tier-scoped per-customer |

Each value is list of `{ from, to }` path rewrites applied when host matches. Conventions:

- `{ from: 'api', to: 'api/<tenant>' }` — `/<api>/x` → `/<api>/<tenant>/x`, so RPC calls land in tenant segment.
- `{ from: '', to: '<tenant>' }` — `/` → `/<tenant>`, so frontend hits tenant's page tree.

`[placeholders]` in key are substituted into `to` paths: `'[customer_name].customer'` + `{ to: 'customer/[customer_name]' }` → `customer/acme` when host is `acme.customer.example.com`.

### Reserved-path guard

If request arrives on **root host** with path starting with reserved tenant name (e.g. `example.com/admin/...`), `multitenant()` returns `redirect` (not rewrite) to proper subdomain (`admin.example.com/...`). Keeps root tenant from accidentally serving tenant-scoped URLs and stops users from bookmarking wrong form of link.

"Reserved" = any `overrides` key without dynamic `[placeholders]`. Dynamic keys (`[customer_name].customer`) don't count as reserved paths.

### Return `action` values

| `action` | Proxy does | When |
|---|---|---|
| `'rewrite'` | `NextResponse.rewrite(destination)` | Normal tenant routing — host matches an override and path matches a `from`. |
| `'redirect'` | `NextResponse.redirect(destination)` | Root-host access to reserved tenant path (guard above). |
| `null` | `NextResponse.next()` | Pass-through — not a tenant request (schema endpoint, unmatched host, etc.). |

Some docs examples branch on `action === 'notfound'` and return 404. Current `multitenant()` helper never produces that value (only `'rewrite'`, `'redirect'`, or `null`), so `'notfound'` branch is dead code. Drop it unless handling a 404 yourself for some other reason.

`subdomains` carries captured dynamic values (e.g. `{ customer_name: 'acme' }`) — useful for structured logging, analytics, or feeding into request context.

Helper also short-circuits on `_schema_` paths (Vovk's dev schema endpoint), returning `action: null`, so `vovk dev` / client generation keeps working without extra proxy config.

## 5. DNS / local dev

- **Production**: wildcard DNS at right level. For `<customer>.customer.example.com` need `*.customer.example.com` → host. On Vercel, CNAME `*.multitenant` → `cname.vercel-dns.com.` covers one level; nested patterns need a record at each level you want to catch.
- **Local**: browsers resolve every name under `localhost` to the loopback address, so `admin.localhost:3000`, `acme.customer.localhost:3000`, etc. reach the dev server with no `/etc/hosts` change. A hosts file takes no wildcards, so a `*.localhost` line does nothing.

## Adding a new tenant

1. `npx vovk new segment <tenant>` — creates `src/app/api/<tenant>/[[...vovk]]/route.ts`.
2. Create frontend page tree under `src/app/<tenant>/...` (or `src/app/<parent>/[slug]/...` for dynamic tenants).
3. Add `overrides` entry in `src/proxy.ts` mapping subdomain pattern to both API path (`api` → `api/<tenant>`) and frontend path (`''` → `<tenant>`).
4. Add `segmentNameOverride: ''` under `outputConfig.segments.<tenant>` in `vovk.config.mjs`.
5. Run `vovk dev` / `vovk generate` to regenerate segmented client.
6. **Prod**: confirm wildcard DNS covers new subdomain. **Local**: nothing to do; every name under `localhost` works.

## Gotchas

- **Proxy filename is fixed**: must be `proxy.ts` (or `.js`) at `src/proxy.ts` or project root. Exported default function can be named anything. (On older Next.js versions same file is called `middleware.ts` — follow whatever convention your Next.js version expects.)
- **Matcher excludes static assets**: `config.matcher` regex skips `_next/*`, `*.png`, `.well-known/*`, etc. Keep those exclusions — without them, asset requests get rewritten and blow up.
- **Wildcard DNS is one level deep**: `*.example.com` covers `admin.example.com` but NOT `foo.admin.example.com`. For patterns like `pro.[customer_name].customer`, need wildcard at each level (`*.customer.example.com`, and potentially more). Check your DNS provider's wildcard semantics.
- **Rewrite vs. redirect**: `rewrite` keeps tenant URL visible in address bar; `redirect` sends 302 to destination. `multitenant()` picks the right one — don't swap them manually in branch.
- **Reserved names can't overlap with real URL paths**: if you have tenant key `admin` and root-tenant page at `src/app/admin/page.tsx`, reserved-path guard redirects `example.com/admin` → `admin.example.com/`, making that page unreachable from root host. Either rename tenant or root page.
- **Sharing controllers across tenants**: if tenants share logic, import same controller class into multiple tenant segments' `controllers` maps (see `segment` skill). Per-tenant customization belongs in controller (e.g. `req.vovk.meta()` for tenant context).

## Common flows

### "Add an admin subdomain"

Short version: `npx vovk new segment admin` → `src/app/admin/page.tsx` → `overrides.admin` in `proxy.ts` → `segmentNameOverride: ''` in `vovk.config.mjs` → done.

### "Each customer gets their own subdomain"

Use dynamic override: `'[customer_name].customer': [...]` with `customer/[customer_name]` on frontend. One segment (`customer`) serves them all; per-customer logic reads subdomain from `request` or `subdomains` field returned by `multitenant()`.

### "Self-hosted per-tenant database"

Vovk doesn't manage DB isolation — that's application-level. Resolve tenant in controller using `req.headers.get('host')` or middleware-injected request header, then route DB calls accordingly. Per-tenant connection pooling / sharding is your concern.

### "Migrating an existing single-tenant app to multi-tenant"

1. Move existing root-tenant pages under root `src/app/` tree (they already live there).
2. Add new tenant segment + page tree for each new tenant.
3. Flip to segmented client in `vovk.config.mjs`, add `segmentNameOverride: ''` for NEW tenants — root tenant keeps its default.
4. Add `proxy.ts` with new tenants' overrides; don't override anything for root.
5. Re-run `vovk dev` / `vovk generate`.

© finom · MIT · finom/vovk

Похожие скиллы

Все скиллы по теме «Бэкенд и API» →
mcp-builder официальный anthropics Скилл проводит через создание MCP-сервера на TypeScript или Python: от изучения протокола до тестов и оценки качества. 130 949установок claude-api официальный anthropics Справочник по Anthropic SDK и API Claude для генерации кода без устаревших параметров и выдуманных методов. 74 198установок lark-im larksuite Скилл подключает Lark IM к агенту: он отправляет сообщения, ищет историю, управляет чатами и файлами. Подойдёт для задач вокруг командных коммуникаций. CX Codex +1 472 900установок lark-event larksuite Слушает события Lark/Feishu и отдаёт их потоком NDJSON. Подходит для ботов, обработчиков сообщений и долгих подписчиков. CX Codex +1 466 800установок caveman-discover JuliusBrussee Находит точки вызова LLM в репозитории и размечает их воркфлоу для Caveman Cloud, чтобы расходы на токены не сливались в одну общую кучу. CX Codex +1 104 400установок migration JuliusBrussee Скилл задаёт правила безопасных миграций схем, данных и API с обязательным откатом. Пригодится при изменениях, где нельзя терять данные или ломать совместимость. CX Codex +1 103 900установок

Из того же набора5

Спонсоры проекта

Компании, которые помогают держать аналитику открытой