---
title: Overview
description: Welcome to Vercel for Platforms documentation
product: Vercel for Platforms
type: overview
summary: Choose between Multi-Tenant and Multi-Project approaches to build platforms that serve multiple customers on Vercel.
related:
  - /docs/multi-tenant-platforms/concepts
  - /docs/multi-project-platforms/concepts
  - /docs/examples/multi-tenant-template
  - /docs/examples/oss-coding-agent
---

# Overview



Vercel for Platforms helps you build platforms that serve multiple customers. Choose from two distinct approaches depending on your use case.

## Choose your approach

### Multi-Tenant

**Single codebase serving multiple tenants with their own domains.**

Routing, data, and configuration are tenant-aware within one deployment. Use this when the content is unique, but the codebase is the same.

**Examples**: Mintlify, Fern (documentation platforms), Hashnode, Dub (content platforms), Super (website builders)

**Best for**: Documentation platforms, content platforms, website builders, SaaS dashboards where all customers share the same application structure.

[Learn about Multi-Tenant →](/docs/multi-tenant-platforms/concepts)

***

### Multi-Project

**Multiple unique codebases, each with its own Vercel project.**

Each tenant has its own repository or deployment generated from a template. Use this when agents or users are deploying unique codebases.

**Examples**: Spawn, Orchids (AI coding platforms), v0 (generative UI), Replit (code environments)

**Best for**: AI coding platforms, per-customer isolated environments, user-generated applications, platforms where each tenant needs custom code.

[Learn about Multi-Project →](/docs/multi-project-platforms/concepts)

***

## What you can build

**Website builders**: Let customers create sites, connect domains, and publish
instantly.

**E-commerce platforms**: Spin up tenant stores from a template, connect
headless APIs, and route custom domains.

**AI coding platforms**: Create collaborative coding environments with
real-time features, custom domain support, and integrated deployment options.

**SaaS dashboards**: Serve tenant-aware dashboards from a single codebase with
strict data boundaries.

**Documentation and knowledge bases**: Multi-tenant docs with search and
custom domains per workspace.

**Developer platforms**: Provision apps from templates, enforce standards, and
give teams routing and domain controls.

## How it works

### Multi-Tenant architecture

With Multi-Tenant, you deploy a single Next.js application to Vercel. Your middleware identifies which tenant a request belongs to (by subdomain or custom domain), then loads that tenant's data and configuration. All tenants share the same codebase but see different content.

You can offer:

* Wildcard subdomains like `tenant1.yourapp.com`, `tenant2.yourapp.com`
* Custom domains like `tenant1.com`, `tenant2.com`
* Automatic SSL certificates for all domains

### Multi-Project architecture

With Multi-Project, you programmatically create separate Vercel projects for each tenant using the Vercel SDK. Each project has its own codebase, deployments, and domains. This gives complete isolation between tenants.

You can:

* Create projects from templates
* Deploy unique code per tenant
* Manage deployments programmatically
* Configure domains per project

## Getting started

The fastest way to get started depends on your use case:

**Multi-Tenant**: Clone the [Platforms Starter Kit](https://vercel.com/templates/next.js/platforms-starter-kit) and configure wildcard domains.

**Multi-Project**: Use the [Vercel SDK](/docs/sdk) to programmatically create and deploy projects.

## Comparison table

| Feature              | Multi-Tenant                          | Multi-Project                         |
| -------------------- | ------------------------------------- | ------------------------------------- |
| **Architecture**     | Single codebase, one deployment       | Multiple codebases, multiple projects |
| **Code sharing**     | All tenants share code                | Each tenant has unique code           |
| **Deployment**       | Deploy once, affects all tenants      | Deploy per tenant independently       |
| **Isolation**        | Application-level (database, routing) | Complete (separate projects)          |
| **Custom domains**   | ✓ Unlimited                           | ✓ Per project                         |
| **Wildcard domains** | ✓ `*.yourapp.com`                     | ✓ Per project                         |
| **Best for**         | Same app, different content           | Different code per tenant             |
| **Complexity**       | Lower (one codebase)                  | Higher (manage multiple projects)     |

## Which approach should I choose?

### Choose Multi-Tenant if:

* All tenants use the same application structure
* Content and branding differ, but functionality is the same
* You want to deploy once and update all tenants simultaneously
* You're building: documentation platforms, content platforms, website builders

### Choose Multi-Project if:

* Each tenant needs custom code or unique functionality
* Tenants deploy their own code (AI agents, user-generated apps)
* Complete isolation is required between tenants
* You're building: AI coding platforms, per-customer environments, template-based platforms

## Next steps

<Cards>
  <Card title="Multi-Domain Quickstart" href="/docs/multi-tenant-platforms/quickstart">
    Start letting your customers assign custom domains in minutes with our
    Ready-to-use components
  </Card>

  <Card title="Fluid Subhosting Quickstart" href="/docs/multi-project-platforms/quickstart">
    Quickly let your customers deploy code to an infinite amount of Vercel with
    our Actions
  </Card>

  <Card title="Multi-Tenant Platform Example" href="/docs/examples/multi-tenant-template">
    Quickly let your customers deploy code to an infinite amount of Vercel
    Functions
  </Card>

  <Card title="OSS Coding Agent Example" href="/docs/examples/oss-coding-agent">
    Start letting your customers assign custom domains in minutes
  </Card>
</Cards>


---
title: Overview
description: Welcome to Vercel for Platforms documentation
product: Vercel for Platforms
type: overview
summary: High-level introduction to platforms and multi-tenant architecture on Vercel.
related:
  - /docs/multi-tenant-platforms/concepts
  - /docs/multi-project-platforms/concepts
---

# Overview



### Platforms vs Multi-tenant

* **Platforms**: Apps that deploy many unique codebases. Each tenant often has its own repository or deployment generated from a template. Most common for usecases where Agents are deploying unique codebases to Vercels. [v0](https://v0.app) and [same.dev](https://same.dev) are examples of this.
* **Multi-tenant**: A single codebase serving multiple tenants and domains. Routing, data, and configuration are tenant-aware within one deployment. Common for usecases where the content is unique, but the codebase is the same. [Mintlify](https://mintlify.com) and [Fern](https://buildwithfern.com) are examples of this.

### What you can build

* **Website builders**: Let customers create sites, connect domains, and publish instantly.
* **E-commerce platforms**: Spin up tenant stores from a template; connect headless APIs; route custom domains.
* **SaaS dashboards**: Serve tenant-aware dashboards from a single codebase with strict data boundaries.
* **Documentation and knowledge bases**: Multi-tenant docs with search and custom domains per workspace.
* **Internal developer platforms**: Provision apps from templates; enforce standards; give teams routing and domain controls.
* **Integration marketplaces**: Host integration UIs and webhooks per tenant with safe rollout and observability.

<Cards>
  <Card title="Concepts" href="/docs/concepts/tenants">
    Learn the core concepts
  </Card>

  <Card title="Components" href="/docs/components/dns-table">
    Ready-to-use components
  </Card>

  <Card title="Components" href="/docs/components/dns-table">
    Ready-to-use components
  </Card>
</Cards>


---
title: Multi-Tenant Template
description: Build SaaS applications that serve multiple domains from a single Next.js codebase
product: Multi-Tenant
type: guide
summary: Build SaaS applications serving multiple domains from a single Next.js codebase using middleware and dynamic routing.
prerequisites:
  - /docs/multi-tenant-platforms/concepts
related:
  - /docs/multi-tenant-platforms/quickstart
  - /docs/multi-tenant-platforms/middleware-and-routing
---

# Multi-Tenant Template





Multi-tenant applications allow you to serve different customers with unique domains from a single codebase. This template demonstrates how to build a platform that dynamically routes requests based on the incoming domain, enabling you to create white-label solutions, SaaS platforms, and marketplace applications.

<img alt="Multi-Tenant Architecture" src={__img0} placeholder="blur" />

<Callout>
  Deploy the template directly: [vercel.com/templates/saas/platforms-starter-kit](https://vercel.com/templates/saas/platforms-starter-kit)
</Callout>

## What is Multi-Tenancy?

Multi-tenancy is an architecture pattern where a single instance of an application serves multiple tenants (customers or organizations). Each tenant gets their own subdomain or custom domain while sharing the same underlying infrastructure and codebase.

Common use cases include:

* **White-label platforms** - Rebrandable applications for different clients
* **SaaS marketplaces** - Multiple stores/sites under one platform
* **Content management systems** - Publishing platforms with custom domains
* **Agency portfolios** - Client websites managed from one codebase

## How It Works

The multi-tenant template uses Next.js middleware to intercept incoming requests and route them based on the hostname:

1. **Request arrives** - A user visits `customer1.example.com` or `customer1.com`
2. **Middleware intercepts** - Extracts the hostname from the request headers
3. **Dynamic routing** - Rewrites the URL internally to include tenant context
4. **Tenant-specific content** - Renders customized content for that domain
5. **Response served** - User sees their branded experience

## Getting Started

### Step 1: Clone the Template

Start by cloning the multi-tenant template:

```bash title="Terminal"
git clone https://github.com/vercel/platforms
cd platforms
pnpm install
```

The template includes:

* **Next.js App Router** for modern React architecture
* **Middleware** for domain-based routing
* **Tailwind CSS** for styling
* **TypeScript** for type safety

### Step 2: Set Up Local Development

For local development, you'll need to map custom domains to localhost. Add these entries to your hosts file:

```text title="/etc/hosts"
127.0.0.1 tenant1.localhost
127.0.0.1 tenant2.localhost
127.0.0.1 custom.localhost
```

Then start the development server:

```bash title="Terminal"
pnpm dev
```

Visit different domains to see tenant-specific content:

* [http://localhost:3002](http://localhost:3002) - Default home page
* [http://tenant1.localhost:3002](http://tenant1.localhost:3002) - Tenant 1's site
* [http://tenant2.localhost:3002](http://tenant2.localhost:3002) - Tenant 2's site

### Step 3: Configure Middleware

The middleware is the heart of the multi-tenant system. It intercepts all requests and routes them appropriately:

```typescript title="middleware.ts"
import { type NextRequest, NextResponse } from "next/server";

export function middleware(request: NextRequest) {
  const hostname = request.headers.get("host") || "";
  // Add tenant context to the request and rewrite to /[domain]

  // if it starts with /domains, pass through
  if (request.nextUrl.pathname.startsWith("/domains")) {
    return NextResponse.next();
  }

  const target = `${request.nextUrl.href}domains/${hostname}`;
  const response = NextResponse.rewrite(target);
  response.headers.set("x-tenant-domain", hostname);
  return response;
}

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};
```

### Step 4: Create Tenant-Specific Pages

Each tenant's content is served from a dynamic route that receives the domain as a parameter:

```tsx title="app/domains/[domain]/page.tsx"
export default async function TenantPage({
  params,
}: {
  params: Promise<{ domain: string }>;
}) {
  const { domain } = await params;

  // Fetch tenant-specific data
  const tenant = await getTenantByDomain(domain);

  // Render customized content
  return (
    <div className="min-h-screen">
      <header className="p-6" style={{ backgroundColor: tenant.brandColor }}>
        <h1 className="text-3xl font-bold text-white">
          {tenant.name}
        </h1>
      </header>

      <main className="p-6">
        {/* Tenant-specific content */}
        <TenantContent data={tenant.content} />
      </main>
    </div>
  );
}
```

## Core Components

### Domain Resolution

The system identifies tenants through their domain:

```typescript title="middleware.ts"
function extractSubdomain(request: NextRequest): string | null {
  const url = request.url;
  const host = request.headers.get('host') || '';
  const hostname = host.split(':')[0];

  // Local development environment
  if (url.includes('localhost') || url.includes('127.0.0.1')) {
    // Try to extract subdomain from the full URL
    const fullUrlMatch = url.match(/http:\/\/([^.]+)\.localhost/);
    if (fullUrlMatch && fullUrlMatch[1]) {
      return fullUrlMatch[1];
    }

    // Fallback to host header approach
    if (hostname.includes('.localhost')) {
      return hostname.split('.')[0];
    }

    return null;
  }

  // Production environment
  const rootDomainFormatted = rootDomain.split(':')[0];

  // Handle preview deployment URLs (tenant---branch-name.vercel.app)
  if (hostname.includes('---') && hostname.endsWith('.vercel.app')) {
    const parts = hostname.split('---');
    return parts.length > 0 ? parts[0] : null;
  }

  // Regular subdomain detection
  const isSubdomain =
    hostname !== rootDomainFormatted &&
    hostname !== `www.${rootDomainFormatted}` &&
    hostname.endsWith(`.${rootDomainFormatted}`);

  return isSubdomain ? hostname.replace(`.${rootDomainFormatted}`, '') : null;
}
```

### Domain Configuration

Configure domains in your Vercel dashboard:

1. **Wildcard subdomain** - `*.yourdomain.com`
   * Captures all subdomains automatically
   * Perfect for user-generated sites

2. **Custom domains** - Add individually
   * Each tenant's custom domain
   * Requires DNS configuration per domain

## Use Cases

### SaaS Platforms

Build software that serves multiple organizations with isolated data and custom branding.

### E-commerce Marketplaces

Create platforms where vendors get their own storefronts with unique domains.

### Content Publishing

Enable creators to publish content on their custom domains while using your platform.

### Agency Solutions

Manage multiple client websites from a single codebase with individual customizations.

***

The multi-tenant template provides a solid foundation for building platforms that serve multiple customers from a single codebase. By leveraging Next.js middleware and dynamic routing, you can create scalable SaaS applications that offer custom domains, personalized experiences, and efficient resource utilization.

Whether you're building a white-label solution, a marketplace platform, or a content management system, this architecture pattern enables you to grow from one to thousands of tenants without infrastructure complexity.


---
title: OSS AI Vibe Coding Platform
description: Build and deploy your own AI-powered coding platform with Vercel Sandboxes
product: Multi-Project
type: guide
summary: Build an AI-powered coding platform with Vercel Sandboxes that generates, executes, and deploys applications.
prerequisites:
  - /docs/multi-project-platforms/concepts
related:
  - /docs/multi-project-platforms/quickstart
  - /docs/platform-elements/actions/deploy-files
---

# OSS AI Vibe Coding Platform





The AI Vibe Coding Platform represents a new paradigm in development environments where AI doesn't just assist with code completion, but actively generates entire applications based on natural language descriptions. This open-source platform enables developers to create their own AI-powered coding environments that can generate, execute, and deploy full applications in isolated, secure sandboxes.

<img alt="OSS AI Vibe Coding Platform" src={__img0} placeholder="blur" />

<Callout>
  Check out the live demo at [oss-vibe-coding-platform.vercel.app](https://oss-vibe-coding-platform.vercel.app/)
</Callout>

## What Makes It a "Vibe" Platform?

The term "vibe" captures the essence of natural, conversational development. Instead of writing code line by line, developers describe the vibe of what they want to build, and AI translates that vision into working code. It's about capturing intent and transforming it into implementation through the power of modern language models.

## How It Works

When you describe what you want to build, the platform:

1. **Creates an isolated sandbox** - A secure container for your application
2. **Generates application code** - AI writes all necessary files and configuration
3. **Executes the build** - Installs dependencies and runs your application
4. **Provides instant preview** - See your running application immediately
5. **Enables deployment** - Deploy to Vercel with a single click

## Getting Started

### Step 1: Clone and Set Up the Platform

Start by [cloning the project](https://vercel.com/new/clone?demo-description=A+full-stack+coding+platform+built+with+Vercel%27s+AI+Cloud%2C+AI+SDK%2C+and+Next.js.\&demo-image=https%3A%2F%2Fassets.vercel.com%2Fimage%2Fupload%2Fv1754588832%2FOSSvibecodingplatform%2Fscreenshot.png\&demo-title=Vibe+Coding+Platform\&demo-url=https%3A%2F%2Fvercel.fyi%2Fvibes\&project-name=Vibe+Coding+Platform\&repository-name=vibe-coding-platform\&repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fexamples%2Ftree%2Fmain%2Fapps%2Fvibe-coding-platform\&from=vibe-coding-platform-app) and installing dependencies. The platform is built with Next.js and uses modern tools like:

* **Vercel AI SDK** for streaming AI responses
* **Vercel Sandboxes** for secure code execution
* **SWR** for data fetching and caching
* **Tailwind CSS** for styling

### Step 2: Configure Your Environment Variables

You'll only need one environment variable locally to run the project:

```text title=".env.local"
AI_GATEWAY_API_KEY=""
```

This variable is used to authenticate with the AI Gateway and is sorted out for you when deployed to Vercel.

### Step 3: Run the Development Server

Launch your local vibe coding platform locally with:

```bash title="Terminal"
pnpm dev
```

Open [http://localhost:3000](http://localhost:3000) to see your platform running. You'll see:

* **Chat Interface** - Where you describe what you want to build
* **File Explorer** - Browse generated files in real-time
* **Live Preview** - See your application as it's being built
* **Logs Console** - Monitor build output and debugging information

### Step 4: Build Your First Application

Now for the fun part - let's build something! Try these example prompts:

**Simple React App:**

```text title="Prompt"
"Create a todo list app with React that lets me add, complete, and delete tasks. Make it look modern with nice styling."
```

**API with Database:**

```text title="Prompt"
"Build a REST API with Express that manages a book library. Include endpoints for CRUD operations and use SQLite for storage."
```

**Interactive Game:**

```text title="Prompt"
"Make a simple memory card game where players match pairs of cards. Include a timer and score tracking."
```

The platform will:

1. Generate all necessary files
2. Set up the project structure
3. Install dependencies
4. Run the development server
5. Provide you with a live preview URL

## Core Components

### The Sandbox System

Each user session gets its own isolated sandbox:

```typescript title="ai/tools/create-sandbox.ts"
export const createSandbox = async ({ timeout, ports }) => {
  // ...

  const sandbox = await Sandbox.create({
    timeout: timeout ?? 600000,
    ports,
  });

  return (
    `Sandbox created with ID: ${sandbox.sandboxId}.` +
    `\nYou can now upload files, run commands, and access services on the exposed ports.`
  )
}
```

### File Generation

The AI generates files directly into the sandbox:

```typescript title="ai/tools/generate-files.ts"
export const generateFiles = tool({
  description: 'Generate multiple files for the application',
  execute: async ({ files, sandboxId }) => {
    const sandbox = getSandbox(sandboxId);

    for (const file of files) {
      await sandbox.writeFile(file.path, file.content)
    }

    return `Generated ${files.length} files`
  }
})
```

### Command Execution

Run build commands and start servers in the sandbox:

```typescript title="ai/tools/run-command.ts"
export const runCommand = tool({
  description: 'Execute shell commands in the sandbox',
  execute: async ({ command, sandboxId }) => {
    const sandbox = getSandbox(sandboxId)
    const result = await sandbox.runCommand({ cmd: command })

    return result.stdout;
  }
})
```

## Key Features

### Real-time Streaming

Watch as AI generates your application in real-time:

```tsx title="app/chat.tsx"
export function Chat() {
  const { messages, append, isLoading } = useChat({
    api: '/api/chat',
    onResponse: (response) => {
      // Stream updates to UI as code is generated
    }
  })

  return (
    <Conversation className="relative w-full">
      <ConversationContent className="space-y-4">
        {messages.map((message) => (
          <Message key={message.id} message={message} />
        ))}
      </ConversationContent>
      <ConversationScrollButton />
    </Conversation>
  )
}
```

### Multi-Model Support

The platform supports all models supported by the AI Gateway.

```typescript title="app/api/models/route.tsx"
import { SUPPORTED_MODELS } from '@/ai/constants'
import { getAvailableModels } from '@/ai/gateway'
import { NextResponse } from 'next/server'

export async function GET() {
  const allModels = await getAvailableModels()
  return NextResponse.json({
    models: allModels.filter((model) => SUPPORTED_MODELS.includes(model.id)),
  })
}
```

***

The AI Vibe Coding Platform democratizes application development by allowing anyone to build software through natural conversation. Whether you're a seasoned developer looking to prototype quickly or someone new to coding wanting to bring ideas to life, this platform provides the tools to make it happen.

By combining the power of modern AI models with secure sandbox environments, we're entering a new era where the barrier between idea and implementation is just a conversation.


---
title: Platform Template
description: The wall-to-wall reference for building an AI app builder on Vercel. Sandboxes, AI Gateway, deployments, Sign in With Vercel, and project transfers
---

# Platform Template



The Platform Template is the wall-to-wall reference for building an AI app builder on Vercel. It covers every layer of the stack: running AI agents in sandboxes, routing LLM calls through AI Gateway, previewing apps live, deploying to Vercel, and transferring project ownership to users via Vercel Apps and claim deployments.

<Callout>
  Check out the live demo at [platform-template.labs.vercel.dev](https://platform-template.labs.vercel.dev/) and the [GitHub source](https://github.com/vercel-labs/platform-template)
</Callout>

## What This Covers

This template shows every Vercel platform primitive working together end-to-end:

* **Vercel Sandbox** - agents write code and run dev servers allowing for app previews
* **AI Gateway** - secure LLM access via OIDC, proxied from sandboxes that can't hold credentials
* **Vercel Deployments** - deploy sandbox contents to production with the Vercel SDK
* **Vercel Apps** - OAuth flow that installs an app in the users Vercel account during the claim flow allowing for continued updating
* **Project Transfers** - allow users to get their websites deployed and then later take ownership of them

## Architecture Overview

The template is a Next.js application with five major subsystems:

<Mermaid
  chart="flowchart TD
    Browser[&#x22;User Browser&#x22;]

    subgraph App[&#x22;Next.js App&#x22;]
        Chat[&#x22;Chat UI (React)&#x22;]
        Preview[&#x22;Preview UI (iframe)&#x22;]
        RPC[&#x22;oRPC Router&#x22;]
        Proxy[&#x22;AI Proxy Route&#x22;]
        Sandbox[&#x22;Sandbox\nAgent CLI + Dev Server&#x22;]

        Chat --> RPC
        RPC --> Sandbox
        Sandbox --> Preview
        Sandbox --> Proxy
    end

    Gateway[&#x22;AI Gateway\n(OIDC auth)&#x22;]
    VercelAPI[&#x22;Vercel API\n(Deploy / Claim)&#x22;]

    Browser --> Chat
    Browser --> Preview
    Proxy --> Gateway
    RPC --> VercelAPI"
/>

## How It Works

### 1. User sends a prompt

The user types a prompt in the chat interface. The frontend calls `rpc.chat.send` - a streaming oRPC procedure that orchestrates the entire flow.

### 2. Sandbox is created and set up

If this is a new session, a Vercel Sandbox is provisioned. The setup process installs bun, scaffolds the chosen template (Next.js, Vite, or TanStack Start), installs the agent CLI, and starts the dev server:

```typescript title="lib/sandbox/setup.ts"
export async function* setupSandbox(
  sandbox: Sandbox,
  options: SetupOptions,
): AsyncGenerator<SetupProgress> {
  yield { stage: "installing-bun", message: "Installing bun..." };
  await run(sandbox, {
    cmd: "sh",
    args: ["-c", "curl -fsSL https://bun.sh/install | bash && ..."],
    sudo: true,
  }, "bun install");

  // Run template-specific setup (create-next-app, create-vite, etc.)
  for await (const progress of template.setup(sandbox)) {
    yield { stage: progress.stage, message: progress.message };
  }

  // Install agent CLI and wait for dev server in parallel
  yield { stage: "installing-agent", message: "Installing agent..." };
  await Promise.all([agentInstallPromise, devServerPromise]);

  yield { stage: "ready", message: "Sandbox ready" };
}
```

### 3. Agent executes inside the sandbox

Native agent coding CLIs like Codex and Claude Codex are run inside of the sandbox

```typescript title="lib/agents/claude-agent.ts"
const env = {
  ANTHROPIC_BASE_URL: proxyConfig.baseUrl,  // Points to our proxy
  ANTHROPIC_API_KEY: proxyConfig.sessionId,  // Short-lived session ID
};

const cmd = await sandbox.runCommand({
  cmd: "sh",
  args: ["-c", `claude --print --verbose --output-format stream-json ...`],
  cwd: SANDBOX_BASE_PATH,
  env,
  detached: true,
});

for await (const log of cmd.logs()) {
  // Parse NDJSON and convert to unified StreamChunk format
}
```

### 4. LLM calls go through AI Gateway

The agent CLI's base URL is configured to the platform's proxy route, which acts as a provider endpoint. The proxy validates the session, attaches a Vercel OIDC token, and forwards the request to AI Gateway.

If you wanted to implement things like tracking token spend per user this is where you're able to implement it:

```typescript title="app/api/ai/proxy/[[...path]]/route.ts"
// 1. Extract session ID from x-api-key or Authorization header
const data = await redis.get(`session:${sessionId}`);
if (!session) return new Response("Invalid session", { status: 401 });

// 2. Get OIDC token from the Vercel deployment
const gatewayToken = await getVercelOidcToken();

// 3. Forward to AI Gateway with only allowed headers
headers.set("authorization", `Bearer ${gatewayToken}`);
const response = await fetch(`${AI_GATEWAY_URL}${apiPath}`, {
  method: request.method,
  headers,
  body: await request.arrayBuffer(),
});
```

### 5. User previews and deploys

The sandbox dev server is exposed via an iframe. When the user clicks Deploy, the platform reads all files from the sandbox and creates a Vercel deployment:

```typescript title="lib/rpc/procedures/deploy.ts"
const { client, teamId, ownership } = await getDeploymentClient(projectId);
const files = await readFilesForDeploy(sandbox, filePaths);

await vercel.deployments.createDeployment({
  name,
  files,
  target: "production",
  project: projectId ?? undefined,
});
```

### 6. User claims the deployment

For signed-out users, the deployment lives on the partner team. The user can sign in with Vercel to claim it:

1. Platform creates a transfer request via the Vercel API, getting a `transferCode`
2. OAuth URL includes `transfer_code` - Vercel transfers the project during authorization
3. Post-OAuth callback stores per-project tokens (JWE-encrypted) in Redis
4. Future deploys use the stored tokens to deploy directly to the user's account

## AI Gateway Proxy Pattern

Sandboxes cannot hold secrets. The proxy pattern solves this with short-lived capability tokens:

<Mermaid
  chart="sequenceDiagram
    participant S as Sandbox (Agent CLI)
    participant P as Platform (/api/ai/proxy)
    participant R as Redis
    participant G as AI Gateway

    S->>P: LLM request with sessionId
    P->>R: Validate session:{sessionId}
    R-->>P: Session data (sandboxId)
    P->>P: getVercelOidcToken()
    P->>G: Forward request with OIDC token
    G-->>P: LLM response
    P-->>S: LLM response"
/>

**Security properties:**

* Sandbox never sees the real OIDC token or any API key
* Proxy sessions are stored in Redis with a 1-hour TTL
* Only `accept`, `content-type`, and `anthropic-version` headers are forwarded
* CORS checks reject cross-origin requests

## Deployment Authorization Model

The template implements three tiers of deployment authorization:

| Tier        | When            | Token Source                      | Deploys To     |
| ----------- | --------------- | --------------------------------- | -------------- |
| **Partner** | Signed-out user | `VERCEL_PARTNER_TOKEN` env var    | Partner team   |
| **User**    | Signed-in user  | OAuth session cookie              | User's account |
| **Project** | Post-claim      | Stored per-project tokens (Redis) | User's account |

Priority order: project tokens > user session > partner token. This means once a user claims a project, all subsequent deploys go to their account even if they're on a different browser session, because the platform stored the tokens during the claim flow.

## Sign in With Vercel + Claim Flow

The claim flow combines OAuth authorization with project transfer in a single redirect:

1. User clicks "Claim" on a partner-owned deployment
2. Platform calls `projects.createProjectTransferRequest()` to get a `transferCode`
3. User is redirected to Vercel OAuth with `transfer_code` in the URL
4. User authorizes - Vercel transfers the project and returns an auth code
5. Callback exchanges the code for tokens, stores them (JWE-encrypted) in Redis
6. User is redirected back with `?sandboxId=xxx` to restore their session

State recovery is seamless: the sandbox ID is encoded in the redirect URL, and `usePersistedChat()` restores messages, preview URL, and deployment state from Redis.

## Getting Started

### Step 1: Clone and Set Up

```bash title="Terminal"
git clone https://github.com/vercel/platform-template
cd platform-template
pnpm install
```

The template is built with:

* **Next.js 16** with App Router
* **oRPC** for type-safe streaming RPC
* **Vercel Sandbox** for secure code execution
* **Vercel SDK** for deployments and project management
* **Zustand + SWR** for state management
* **Upstash Redis** for session persistence

### Step 2: Configure Environment Variables

```text title=".env.local"
# Required for AI Gateway proxy
PROXY_BASE_URL="http://localhost:3000/api/ai/proxy"

# Required for deployments (partner-tier)
VERCEL_PARTNER_TOKEN=""
VERCEL_PARTNER_TEAM_ID=""

# Required for Sign in With Vercel
VERCEL_CLIENT_ID=""
VERCEL_CLIENT_SECRET=""
SESSION_SECRET=""

# Required for session persistence
UPSTASH_REDIS_REST_URL=""
UPSTASH_REDIS_REST_TOKEN=""
```

### Step 3: Run the Development Server

```bash title="Terminal"
pnpm dev
```

Open [http://localhost:3000](http://localhost:3000) to see the platform. You'll see:

* **Chat interface** - describe what you want to build
* **Agent selector** - choose between Claude Code and Codex
* **Template selector** - pick Next.js, Vite, or TanStack Start
* **Live preview** - watch your app being built in real-time
* **File explorer** - browse generated files
* **Deploy button** - deploy to Vercel with one click

### Step 4: Build Something

Try a prompt like:

```text title="Prompt"
"Build a dashboard with a sidebar navigation, a main content area showing some charts, and a dark mode toggle. Use shadcn/ui components."
```

The platform will create a sandbox, set up the template, run the agent, and show you a live preview as the code is being written.

## Key Patterns

### Unified Stream Protocol

Both Claude Code and Codex produce vastly different output formats. The harness layer normalizes them into a single `StreamChunk` protocol that the UI consumes. This means adding a new agent (e.g., Gemini CLI, OpenCode) requires no UI changes - just implement the `AgentProvider` interface and parse the CLI output.

### oRPC Generator Streaming

The RPC layer uses `async function*` generators for streaming. The client gets typed chunks as they're yielded:

```typescript title="lib/rpc/procedures/chat.ts"
export const sendMessage = os.input(schema).handler(async function* ({ input }) {
  yield { type: "sandbox-id", sandboxId: sandbox.sandboxId };

  for await (const progress of setupSandbox(sandbox, options)) {
    yield events.sandboxStatus(sandbox.sandboxId, ...);
  }

  for await (const chunk of agent.execute({ prompt, sandboxContext, proxyConfig })) {
    yield chunk;
  }
});
```

### Template System

Each template implements a setup generator and provides framework-specific agent instructions:

```typescript title="lib/templates/types.ts"
interface Template {
  id: TemplateId;
  name: string;
  devPort: number;
  instructions: string;   // System prompt for the agent
  setup(sandbox: Sandbox): AsyncGenerator<SetupProgress>;
}
```

Templates handle all scaffolding: `create-next-app`, `create-vite`, installing shadcn, configuring Tailwind, and starting the dev server. The agent receives framework-specific instructions so it generates idiomatic code.

***

The Platform Template demonstrates what's possible when you combine Vercel's infrastructure primitives - sandboxes, AI Gateway, deployments, and project transfers - into a cohesive product experience. It's the reference architecture for building platforms where AI writes code, users preview it live, and deploy it to production with a single click.


---
title: Domain connect
description: How to implement domain connect
product: Multi-Tenant
type: guide
summary: Implement domain connect for your multi-tenant platform.
prerequisites:
  - /docs/multi-tenant-platforms/concepts
related:
  - /docs/multi-tenant-platforms/configuring-domains
  - /docs/platform-elements/blocks/custom-domain
---

# Domain connect



# Domain connect

Content coming soon...


---
title: Next.js App Router
description: Using Next.js App Router with Vercel Platforms
product: Multi-Tenant
type: guide
summary: Use Next.js App Router with Vercel Platforms for multi-tenant applications.
prerequisites:
  - /docs/multi-tenant-platforms/concepts
related:
  - /docs/multi-tenant-platforms/middleware-and-routing
  - /docs/examples/multi-tenant-template
---

# Next.js App Router



# Next.js App Router

Content coming soon...


---
title: Concepts
description: Understanding the core concepts of multi-project architecture helps you build scalable platforms with isolated tenant environments.
product: Multi-Project
type: conceptual
summary: Core concepts of multi-project architecture including projects, deployments, domains, and use cases.
related:
  - /docs/multi-project-platforms/quickstart
  - /docs/multi-project-platforms/reference
---

# Concepts



## Projects

### What is a project

A Vercel project represents a single application with its own Git repository, environment variables, and deployment history. In multi-project architecture, each tenant gets their own dedicated Vercel project.

**Key characteristics**:

* Unique project ID
* Independent configuration
* Separate deployment history
* Isolated builds and functions

### Programmatic creation

Create projects using the Vercel SDK:

```ts title="create-project.ts"
import { Vercel } from "@vercel/sdk";

const vercel = new Vercel({
  bearerToken: "<YOUR_BEARER_TOKEN_HERE>",
});

const { value: project } = await vercel.projects.createProject({
  teamId: "team_1234",
  requestBody: {
    name: `tenant-${tenantId}`,
    framework: "nextjs",
  },
});
```

### Project templates

Generate projects from templates to ensure consistency:

```ts title="create-project-from-template.ts"
import { Vercel } from "@vercel/sdk";

const vercel = new Vercel({
  bearerToken: "<YOUR_BEARER_TOKEN_HERE>",
});

const { value: project } = await vercel.projects.createProject({
  teamId: "team_1234",
  requestBody: {
    name: `tenant-${tenantId}`,
    gitRepository: {
      type: "github",
      repo: "your-org/tenant-template",
    },
  },
});
```

## Deployments

### Isolated deployments

Each project has independent deployments:

* Separate build processes
* Independent function execution
* Isolated environment variables
* Individual deployment URLs

### Deployment lifecycle

1. **Create**: Deploy code to project
2. **Build**: Vercel builds the application
3. **Preview**: Test deployment before promoting
4. **Production**: Promote deployment to production
5. **Rollback**: Revert to previous deployment if needed

### Preview vs production

**Preview deployments**:

* Generated for every Git push
* Unique URL per deployment
* Test changes before production

**Production deployments**:

* Live on project domains
* Promoted from preview or direct deploy
* Serves end users

## Domains

### Per-project domains

Each tenant project can have its own domains:

```ts title="add-domain.ts"
import { Vercel } from "@vercel/sdk";

const vercel = new Vercel({
  bearerToken: "<YOUR_BEARER_TOKEN_HERE>",
});

await vercel.projects.addProjectDomain({
  idOrName: project.id,
  requestBody: {
    name: "tenant1.com",
  },
});
```

### Automatic URLs

Every project gets automatic Vercel URLs:

* Production: `project-name.vercel.app`
* Preview: `project-name-git-branch.vercel.app`

### Custom domains per project

Configure custom domains independently for each tenant:

* Add domain via SDK
* Tenant configures DNS
* Verify ownership
* SSL certificate issues automatically

## Architecture

### Multiple projects vs single project

**Multi-Project**:

* Each tenant = separate Vercel project
* Complete isolation
* Independent scaling
* Higher management overhead

**Multi-Tenant** (single project):

* All tenants = one Vercel project
* Shared infrastructure
* Lower management overhead
* Application-level isolation

## Use cases

### When to use Multi-Project vs Multi-Tenant

**Use Multi-Project when**:

* Tenants deploy their own code
* Each tenant needs custom functionality
* Complete isolation is required (security, compliance)
* AI agents are generating and deploying code
* Users create applications from templates

**Use Multi-Tenant when**:

* All tenants use the same application
* Content differs but code is the same
* You want to deploy once, update all tenants
* Lower operational overhead preferred

### AI coding platforms

Perfect for platforms where:

* AI generates unique code per tenant
* Each tenant's app is different
* Users can deploy custom logic
* Examples: Spawn, Orchids, v0

### Template-based platforms

Ideal for:

* Creating projects from templates
* Customizing per tenant
* Maintaining consistency
* Rapid tenant onboarding

### User-generated applications

Well-suited for:

* Users creating their own apps
* Hosting user-generated content
* Platform controls infrastructure
* Users focus on their code

## Next steps

* [Quickstart](/docs/multi-project-platforms/quickstart): Get started with multi-project platforms
* [Reference](/docs/multi-project-platforms/reference): API reference and troubleshooting


---
title: Quickstart
description: Programmically host code for user or AI generated applications
product: Multi-Project
type: guide
summary: Integrate user-generated sites with Vercel through project creation, deployment management, and domain aliasing.
prerequisites:
  - /docs/multi-project-platforms/concepts
related:
  - /docs/multi-project-platforms/reference
  - /docs/platform-elements/actions/deploy-files
---

# Quickstart



Let's explore integrating user generated sites with Vercel, providing a system for creating preview environments for your customers. The integration involves project creation, deployment management, domain aliasing (with custom suffixes), and SSO protection to secure all deployments.

Vercel's API endpoints handle project creation, deployment configuration, domain assignment, and security settings, enabling a seamless experience for your users to access and share their generated sites.

The integration would involve the following integrations:

1. Creating a Project for each chat
2. Deploying each version of the Artifact to the Project on demand when the user wants to "Publish"
3. Auto-aliasing the `*.CUSTOM_SUFFIX.com` domain that you want, or a user provided domain.
4. **Optional** Pretecting all the deployments through a central proxy
5. **Optional** Configuring the sites as subpaths on a single domain

## Project Creation

* We recommend one project per user chat, the user can subsequently deploy multiple versions of the artifact to the same project (while still allowing all versions to stay active). The project becomes the organizational primitive for a single chat however
* [Create a project for each chat](https://vercel.com/docs/rest-api/reference/endpoints/projects/create-a-new-project)
  * Configure the name of the project to help yourself come back to it and keep your Vercel account organized.
* Most common options to customize the behaviour of each tenant project:
  * `framework`: The framework the application is using (ie. Next.js, React ect.)
  * `serverlessFunctionRegion`: The region any of your compute runs in. (It will default to iad1 if not)
  * `passwordProtection`: Configure a password customers can use to protect their generations

```jsx title="create-project.ts"
async function createProject() {
  const result = await vercel.projects.createProject({
    teamId: "team_1a2b3c4d5e6f7g8h9i0j1k2l",
    slug: "my-team-url-slug",
    requestBody: {
      name: "a-project-name",
    },
  });

  console.log(result);
}
```

## Deployment

* [Every project has one "Production" domain that points to the most recent version (while keeping previous versions) and infinite "Preview" deployments](https://vercel.com/docs/deployments/environments), for most applications it makes most sense to always deploy the version as a new "Production" deployment under the same project
* [Create a new Deployment with this endpoint](https://vercel.com/docs/rest-api/reference/endpoints/deployments/create-a-new-deployment)
* Most common options to customize the behaviour of each tenant project:
  * `files`: The set of AI Generated files you want to deploy
  * `target`: preview or production. Both are the same underlying infrastructure but this is a semantic identifier you can add

Leverage our pre-configured [file deploy action](/docs/platform-elements/actions/deploy-files):

<Installer path="deploy-files" />

```jsx title="deploy-files.ts"
import { deployFiles } from "@/actions/deploy-files"
import type { InlinedFile } from "@vercel/sdk/models/createdeploymentop"

// Example: Deploy a simple HTML site
const files: InlinedFile[] = [
  {
    file: "index.html",
    data: "<html><body><h1>Hello from my platform!</h1></body></html>"
  },
  {
    file: "package.json",
    data: JSON.stringify({
      name: "my-deployment",
      version: "1.0.0"
    })
  }
]

await deployFiles(files, {
  domain: "customer-site.com",
  deploymentName: "customer-deployment-1",
  projectId: "existing-project-id", // Optional: use existing project
  config: {
    framework: "nextjs",
    buildCommand: "npm run build",
    outputDirectory: ".next"
  }
})
```

## Domain Aliasing

* If you want deployments to have a custom suffix: `*.CUSTOM_SUFFIX.com`, you can:
  * Setup a [wildcard domain for your domain](https://vercel.com/docs/multi-tenant/domain-management#using-wildcard-domains)
  * Use either of these to auto-assign the `*.CUSTOM_SUFFIX.com` domain:
    * [Assign a single domain that maps to the most recent production domain of the Project](https://vercel.com/docs/rest-api/reference/endpoints/projects/add-a-domain-to-a-project)
    * [Alias a specific URL to only that one deployment in a project](https://vercel.com/docs/rest-api/reference/endpoints/aliases/assign-an-alias)
  * [Alternatively allow customers to bring in a domain they own elsewhere to assign to their sites](/docs/multi-tenant-platforms/configuring-domains)

## Protecting all deployments behind SSO or authentication, if you want central authentication (optional)

1. Create a Proxy site hosted in Vercel that all traffic will flow through that:
   1. Checks x.ai SSO and asks the user to log in if not
   2. Proxies back to the actual deployed application once authenticated
   3. Includes a [Deployment Bypass](https://vercel.com/docs/deployment-protection/methods-to-bypass-deployment-protection/protection-bypass-automation) header, that will lock down direct access to the Vercel applications
2. Alias all domains to the proxy site instead so all traffic goes to it first
3. Ensure `ssoProtection` is configured to `all` when creating the project to lock down access
   1. Configuring a bypass option on the deployment so the Proxy can still access it: [https://vercel.com/docs/rest-api/reference/endpoints/projects/update-protection-bypass-for-automation](https://vercel.com/docs/rest-api/reference/endpoints/projects/update-protection-bypass-for-automation)

## Configuring the sites to be subpaths instead of custom domains (optional)

* To ensure all these deployments can only be accessed as subpaths `domain.com/customer_a` you would first configure the same Proxy setup as in 4. (with or without authentication, depending on your product requirements.)
* This proxy would be [assigned the domain you want to share in it's Vercel project](https://vercel.com/docs/domains/working-with-domains/add-a-domain)
* You can then create a [routing middleware](https://vercel.com/docs/routing-middleware) in the Proxy, to be able to rewrite the paths to the domains they are hosted in Vercel.
  * It can dynamically pull the data from your backend or you can make it faster by caching it in [Edge Config](https://vercel.com/docs/edge-config)


---
title: Reference
description: Common questions related to Multi Projet Platforms
product: Multi-Project
type: reference
summary: Projects and deployments API reference, error codes, troubleshooting, and FAQ for multi-project platforms.
prerequisites:
  - /docs/multi-project-platforms/concepts
related:
  - /docs/multi-project-platforms/quickstart
  - /docs/platform-elements/actions/deploy-files
---

# Reference



## Custom blocks

Start with our Custom [Blocks](/docs/platform-elements/blocks/deploy-popover) and [Actions](/docs/platform-elements/actions/deploy-files) that speed up your usage of the Vercel API.

## Projects & Deployments API reference

### Create project

Create a new Vercel project using the [create project API](https://vercel.com/docs/rest-api/reference/endpoints/projects/create-a-new-project).

**SDK**:

```ts title="create-project.ts"
import { Vercel } from "@vercel/sdk";

const vercel = new Vercel({
  bearerToken: "<YOUR_BEARER_TOKEN_HERE>",
});

async function run() {
  const result = await vercel.projects.createProject({
    teamId: "team_1a2b3c4d5e6f7g8h9i0j1k2l",
    slug: "my-team-url-slug",
    requestBody: {
      name: "a-project-name",
    },
  });

  console.log(result);
}

run();
```

### Deploy to project

Create a deployment for a project using the [create deployment API](https://vercel.com/docs/rest-api/reference/endpoints/deployments/create-a-new-deployment).

**SDK**:

```ts title="deploy-files.ts"
import { Vercel } from "@vercel/sdk";

const vercel = new Vercel({
  bearerToken: "<YOUR_BEARER_TOKEN_HERE>",
});

async function run() {
  const result = await vercel.deployments.createDeployment({
    teamId: "team_1a2b3c4d5e6f7g8h9i0j1k2l",
    slug: "my-team-url-slug",
    requestBody: {
      deploymentId: "dpl_2qn7PZrx89yxY34vEZPD31Y9XVj6",
      files: [
        {
          data: "<value>",
          file: "folder/file.js",
        },
      ],
      gitMetadata: {
        remoteUrl: "https://github.com/vercel/next.js",
        commitAuthorName: "kyliau",
        commitAuthorEmail: "kyliau@example.com",
        commitMessage:
          "add method to measure Interaction to Next Paint (INP) (#36490)",
        commitRef: "main",
        commitSha: "dc36199b2234c6586ebe05ec94078a895c707e29",
        dirty: true,
        ci: true,
        ciType: "github-actions",
        ciGitProviderUsername: "rauchg",
        ciGitRepoVisibility: "private",
      },
      gitSource: {
        projectId: 987654321,
        ref: "main",
        sha: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0",
        type: "gitlab",
      },
      meta: {
        foo: "bar",
      },
      name: "my-instant-deployment",
      project: "my-deployment-project",
      projectSettings: {
        buildCommand: "next build",
        installCommand: "pnpm install",
      },
      target: "production",
    },
  });

  console.log(result);
}

run();
```

### List deployments

Get deployments for a project using the [list deployments API](https://vercel.com/docs/rest-api/reference/endpoints/deployments/list-deployments).

**SDK**:

```ts title="list-deployments.ts"
import { Vercel } from "@vercel/sdk";

const vercel = new Vercel({
  bearerToken: "<YOUR_BEARER_TOKEN_HERE>",
});

async function run() {
  const result = await vercel.deployments.getDeployments({
    app: "docs",
    from: 1612948664566,
    limit: 10,
    projectId: "QmXGTs7mvAMMC7WW5ebrM33qKG32QK3h4vmQMjmY",
    projectIds: ["prj_123", "prj_456"],
    target: "production",
    to: 1612948664566,
    users: "kr1PsOIzqEL5Xg6M4VZcZosf,K4amb7K9dAt5R2vBJWF32bmY",
    since: 1540095775941,
    until: 1540095775951,
    state: "BUILDING,READY",
    teamId: "team_1a2b3c4d5e6f7g8h9i0j1k2l",
    slug: "my-team-url-slug",
  });

  console.log(result);
}

run();
```

### Delete project

Remove a project and all its deployments using the delete project API.

**SDK**:

```ts title="delete-project.ts"
import { Vercel } from "@vercel/sdk";

const vercel = new Vercel({
  bearerToken: "<YOUR_BEARER_TOKEN_HERE>",
});

async function run() {
  const result = await vercel.deployments.deleteDeployment({
    id: "dpl_5WJWYSyB7BpgTj3EuwF37WMRBXBtPQ2iTMJHJBJyRfd",
    url: "https://files-orcin-xi.vercel.app/",
    teamId: "team_1a2b3c4d5e6f7g8h9i0j1k2l",
    slug: "my-team-url-slug",
  });

  console.log(result);
}

run();
```

### Error codes

| Code                     | Description                | Solution                                   |
| ------------------------ | -------------------------- | ------------------------------------------ |
| `project_limit_exceeded` | Team project limit reached | Upgrade plan or clean up unused projects   |
| `invalid_name`           | Project name is invalid    | Use alphanumeric characters and hyphens    |
| `forbidden`              | Insufficient permissions   | Check API token has project creation scope |
| `rate_limit_exceeded`    | Too many requests          | Implement exponential backoff              |
| `build_failed`           | Deployment build failed    | Check build logs for errors                |

## Troubleshooting

### Deployment Failures

**Problem**: Deployments failing with build errors.

**Solution**:

* Check build logs via SDK
* Verify `package.json` dependencies
* Ensure build command is correct
* Check for environment variable issues
* Verify Node.js version compatibility

### Project Creation Limits

**Problem**: Cannot create more projects.

**Solution**:

* Check current project count against plan limit
* Delete unused projects
* Upgrade to higher tier plan
* Contact sales for enterprise limits

### Domain Conflicts

**Problem**: Domain already in use error.

**Solution**:

* Domain must be unique across Vercel
* Remove domain from other project first
* Use subdomain instead (`tenant1.yourdomain.com`)
* Verify domain ownership if domain exists elsewhere

### Build Errors

**Problem**: Builds timing out or failing.

**Solution**:

* Optimize build process
* Check build time limits for your plan
* Reduce dependencies
* Use build caching
* Split large builds into stages

### Resource Quota Issues

**Problem**: Hitting function size or execution limits.

**Solution**:

* Review function sizes
* Optimize bundle size
* Check execution time limits
* Consider upgrading plan
* Split large functions

## FAQ

### What's the difference between Multi-Project and Multi-Tenant?

**Multi-Project**: Multiple Vercel projects, each with unique code and isolated deployments. Complete separation between tenants.

**Multi-Tenant**: Single project serving multiple tenants with different content. All tenants share the same codebase.

Use Multi-Project when tenants need custom code. Use Multi-Tenant when tenants share functionality but have different content.

### How many projects can I create?

Project limits depend on your plan:

* **Hobby**: Limited projects per account
* **Pro**: Higher limits
* **Enterprise**: Custom limits based on needs

Contact [sales](/contact/sales) for specific limits.

### How is pricing calculated per tenant?

Each project is billed based on:

* **Build minutes**: Time spent building deployments
* **Function invocations**: Number of function calls
* **Bandwidth**: Data transferred
* **Edge requests**: CDN requests

All usage follows standard Vercel pricing. See [pricing documentation](/pricing).

### Are there isolation guarantees?

Yes, Multi-Project provides complete isolation:

* **Build isolation**: Separate build environments
* **Runtime isolation**: Independent function execution
* **Data isolation**: No shared state between projects
* **Configuration isolation**: Separate environment variables

### How does data retention work?

When you delete a project:

* Deployments are deleted immediately
* Build logs are retained for 30 days
* Environment variables are deleted
* Domains are released

Export any data you need before deleting projects.

### How can I monitor multiple projects?

Monitor projects using:

* **Vercel Dashboard**: View all projects per team
* **SDK**: Query project and deployment status
* **Webhooks**: Get real-time deployment notifications
* **Analytics**: View usage and performance metrics

### Can I migrate from Multi-Tenant to Multi-Project?

Yes, but it requires architectural changes:

1. Export tenant data from shared database
2. Create separate projects per tenant
3. Deploy tenant code to each project
4. Configure domains for each project
5. Update tenant routing in your application

This is a significant migration. Consider carefully before switching.

### What are the rate limits?

API rate limits for project operations:

* **Create project**: 100 requests per hour
* **Create deployment**: 1000 requests per hour
* **Delete project**: 100 requests per hour
* **Other operations**: Standard API limits

See [API rate limits documentation](/docs/rest-api#rate-limits).

### How do I handle CI/CD per tenant?

Each project can have its own CI/CD:

* Connect to tenant's Git repository
* Configure build settings per project
* Set up deployment webhooks
* Use GitHub Actions or other CI tools
* Test and deploy independently

### Can tenants manage their own projects?

No, programmatically created projects are managed by your team. Tenants cannot access the Vercel dashboard for their projects unless you add them as team members (not recommended for platforms).

Instead, build your own interface for tenants to:

* Trigger deployments
* View deployment status
* Configure environment variables
* Monitor usage

## Next steps

* [Concepts](/docs/multi-project-platforms/concepts): Understand multi-project architecture
* [Quickstart](/docs/multi-project-platforms/quickstart): Get started with multi-project platforms
* [Vercel SDK](https://vercel.com/docs/sdk): Complete SDK documentation


---
title: Concepts
description: Understanding the core concepts of multi-tenant architecture helps you build scalable platforms on Vercel.
product: Multi-Tenant
type: conceptual
summary: Core concepts of multi-tenant architecture including tenants, domains, routing, and tenant context.
related:
  - /docs/multi-tenant-platforms/quickstart
  - /docs/multi-tenant-platforms/configuring-domains
  - /docs/multi-tenant-platforms/middleware-and-routing
---

# Concepts



## Tenants

### What is a tenant

A tenant represents a customer, workspace, or organization within your multi-tenant application. Each tenant has its own data, configuration, and branding, but all tenants share the same codebase and deployment.

**Examples**:

* Blog platform: Each writer with their own blog is a tenant
* Documentation platform: Each company with its own docs site is a tenant
* E-commerce platform: Each store owner is a tenant

### Tenant identification strategies

You can identify tenants using three approaches:

**Subdomain-based**: Extract the tenant from the subdomain (`tenant1.yourapp.com`)

```ts title="middleware.ts"
const hostname = request.headers.get("host");
const subdomain = hostname.split(".")[0]; // "tenant1"
```

**Custom domain-based**: Map custom domains to tenants (`tenant1.com` → Tenant 1)

```ts title="middleware.ts"
// Map custom domain to tenant in database
const tenant = await db.tenant.findFirst({
  where: { customDomain: hostname },
});
```

**Path-based**: Extract tenant from URL path (`/tenant1/dashboard`)

```ts title="middleware.ts"
const pathname = request.nextUrl.pathname;
const tenantSlug = pathname.split("/")[1]; // "tenant1"
```

### Tenant data isolation

Multi-tenant applications must isolate data between tenants:

**Database-level**: Use tenant ID in all queries

```ts title="database.ts"
const posts = await db.post.findMany({
  where: { tenantId: tenant.id },
});
```

**Application-level**: Middleware ensures requests can only access their tenant's data

**Edge Config**: Store tenant configuration for fast lookups at the edge

## Domains

### Wildcard domains

Wildcard domains let you automatically serve all subdomains from a single Vercel project:

* Add `*.yourapp.com` to your project
* Point your domain to Vercel's nameservers
* Any subdomain (`tenant1.yourapp.com`, `tenant2.yourapp.com`) automatically routes to your app
* Vercel issues SSL certificates for each subdomain on the fly

**Requirements**: Must use Vercel's nameservers (`ns1.vercel-dns.com`, `ns2.vercel-dns.com`)

### Custom domains

Custom domains let tenants bring their own domain:

* Add `tenant1.com` to your Vercel project via SDK
* Tenant configures DNS (CNAME or nameservers)
* Verify domain ownership (TXT record)
* Vercel issues SSL certificate automatically

### SSL certificate issuance

Vercel automatically issues SSL certificates for all domains using Let's Encrypt:

* Wildcard domains: Single wildcard certificate covers all subdomains
* Custom domains: Individual certificate per domain
* Automatic renewal before expiration
* No configuration required

### Domain verification

For domains already in use on Vercel, ownership verification is required:

1. Add domain to your project
2. Vercel generates a unique TXT record
3. Tenant adds TXT record to their DNS
4. Verify ownership via SDK or dashboard
5. Certificate issues once verified

## Routing

### How middleware resolves tenants

Next.js middleware runs on every request before your pages render:

```ts title="middleware.ts"
export async function middleware(request: NextRequest) {
  const hostname = request.headers.get("host");

  // Get tenant from subdomain or custom domain
  const tenant = await resolveTenant(hostname);

  // Add tenant to request headers
  const response = NextResponse.next();
  response.headers.set("x-tenant-id", tenant.id);

  return response;
}
```

### Request handling flow

1. User visits `tenant1.yourapp.com`
2. Request hits Vercel's edge network
3. Middleware extracts subdomain (`tenant1`)
4. Middleware looks up tenant in database or Edge Config
5. Middleware adds tenant context to request headers
6. Page component reads tenant from headers
7. Page renders with tenant-specific data

### Performance considerations

**Edge Config**: Store tenant configuration at the edge for sub-10ms lookups

```ts title="edge-config.ts"
import { get } from "@vercel/edge-config";

const tenant = await get(`tenant_${hostname}`);
```

**Caching**: Cache tenant lookups in middleware to reduce database queries

**Connection pooling**: Use connection pooling for database queries to handle multiple tenants efficiently

## Architecture

### Single deployment serving multiple domains

Multi-tenant architecture means:

* One Next.js codebase
* One Vercel deployment
* Multiple domains (subdomains + custom domains)
* Shared infrastructure and resources
* Tenant-aware routing and data access

### Tenant context

Pass tenant information through your application:

**In middleware**: Set headers

```ts title="middleware.ts"
response.headers.set("x-tenant-id", tenant.id);
```

**In server components**: Read headers

```ts title="server-component.ts"
import { headers } from "next/headers";

const tenantId = headers().get("x-tenant-id");
```

**In API routes**: Access request headers

```ts title="api-route.ts"
const tenantId = request.headers.get("x-tenant-id");
```


---
title: Configuring Custom Domains
description: How to make deployments & projects use a custom domain
product: Multi-Tenant
type: guide
summary: Configure wildcard domains, custom domains, domain verification, and redirects for multi-tenant deployments.
prerequisites:
  - /docs/multi-tenant-platforms/concepts
related:
  - /docs/multi-tenant-platforms/quickstart
  - /docs/platform-elements/blocks/custom-domain
  - /docs/platform-elements/blocks/dns-table
---

# Configuring Custom Domains



## Using wildcard domains

If you plan on offering subdomains like `*.acme.com`, add a wildcard domain to your Vercel project. This requires using [Vercel's nameservers](https://vercel.com/docs/projects/domains/working-with-nameservers) so that Vercel can manage the DNS challenges necessary for generating wildcard SSL certificates.

1. Point your domain to Vercel's nameservers (`ns1.vercel-dns.com` and `ns2.vercel-dns.com`).
2. In your Vercel project settings, add the apex domain (e.g., `acme.com`).
3. Add a wildcard domain: `.acme.com`.

Now, any `tenant.acme.com` you create—whether it's `tenant1.acme.com` or `docs.tenant1.acme.com`—automatically resolves to your Vercel deployment. Vercel issues individual certificates for each subdomain on the fly.

## Offering custom domains

You can also give tenants the option to bring their own domain. In that case, you'll want your code to:

1. Provision and assign the tenant's domain to your Vercel project.
2. Verify the domain (to ensure the tenant truly owns it).
3. Automatically generate an SSL certificate.

## Adding a domain programmatically

You can add a new domain through the [Vercel SDK](https://vercel.com/docs/sdk). For example:

```ts title="add-domain.ts"
import { VercelCore as Vercel } from "@vercel/sdk/core.js";
import { projectsAddProjectDomain } from "@vercel/sdk/funcs/projectsAddProjectDomain.js";

const vercel = new Vercel({
  bearerToken: process.env.VERCEL_TOKEN,
});

// The 'idOrName' is your project name in Vercel, for example: 'multi-tenant-app'
await projectsAddProjectDomain(vercel, {
  idOrName: "my-multi-tenant-app",
  teamId: "team_1234",
  requestBody: {
    // The tenant's custom domain
    name: "customacmesite.com",
  },
});
```

Once the domain is added, Vercel attempts to issue an SSL certificate automatically.

## Verifying domain ownership

If the domain is already in use on Vercel, the user needs to set a TXT record to prove ownership of it.

You can check the verification status and trigger manual verification:

```ts title="verify-domain.ts"
import { VercelCore as Vercel } from "@vercel/sdk/core.js";
import { projectsGetProjectDomain } from "@vercel/sdk/funcs/projectsGetProjectDomain.js";
import { projectsVerifyProjectDomain } from "@vercel/sdk/funcs/projectsVerifyProjectDomain.js";

const vercel = new Vercel({
  bearerToken: process.env.VERCEL_TOKEN,
});

const domain = "customacmesite.com";

const [domainResponse, verifyResponse] = await Promise.all([
  projectsGetProjectDomain(vercel, {
    idOrName: "my-multi-tenant-app",
    teamId: "team_1234",
    domain,
  }),
  projectsVerifyProjectDomain(vercel, {
    idOrName: "my-multi-tenant-app",
    teamId: "team_1234",
    domain,
  }),
]);

const { value: result } = verifyResponse;

if (!result?.verified) {
  console.log(`Domain verification required for ${domain}.`);
  // You can prompt the tenant to add a TXT record or switch nameservers.
}
```

## Handling redirects and apex domains

### Redirecting between apex and "www"

Some tenants might want `www.customacmesite.com` to redirect automatically to their apex domain `customacmesite.com`, or the other way around.

1. Add both `customacmesite.com` and `www.customacmesite.com` to your Vercel project.
2. Configure a redirect for `www.customacmesite.com` to the apex domain by setting `redirect: customacmesite.com` through the API or your Vercel dashboard.

This ensures a consistent user experience and prevents issues with duplicate content.

### Avoiding duplicate content across subdomains

If you offer both `tenant.acme.com` and `customacmesite.com` for the same tenant, you may want to redirect the subdomain to the custom domain (or vice versa) to avoid search engine duplicate content. Alternatively, set a canonical URL in your HTML `<head>` to indicate which domain is the "official" one.

## Deleting or removing domains

If a tenant cancels or no longer needs their custom domain, you can remove it from your Vercel account using the SDK:

```ts title="remove-domain.ts"
import { VercelCore as Vercel } from "@vercel/sdk/core.js";
import { projectsRemoveProjectDomain } from "@vercel/sdk/funcs/projectsRemoveProjectDomain.js";
import { domainsDeleteDomain } from "@vercel/sdk/funcs/domainsDeleteDomain.js";

const vercel = new Vercel({
  bearerToken: process.env.VERCEL_TOKEN,
});

await Promise.all([
  projectsRemoveProjectDomain(vercel, {
    idOrName: "my-multi-tenant-app",
    teamId: "team_1234",
    domain: "customacmesite.com",
  }),
  domainsDeleteDomain(vercel, {
    domain: "customacmesite.com",
  }),
]);
```

The first call disassociates the domain from your project, and the second removes it from your account entirely.

## Troubleshooting common issues

Here are a few common issues you might run into and how to solve them:

### DNS propagation delays

After pointing your nameservers to Vercel or adding CNAME records, changes can take 24–48 hours to propagate. Use [WhatsMyDNS](https://www.whatsmydns.net/) to confirm updates worldwide.

### Forgetting to verify domain ownership

If you add a tenant's domain but never verify it (e.g., by adding a `TXT` record or using Vercel nameservers), SSL certificates won't be issued. Always check the domain's status in your Vercel project or with the SDK.

### Wildcard domain requires Vercel nameservers

If you try to add `.acme.com` without pointing to `ns1.vercel-dns.com` and `ns2.vercel-dns.com`, wildcard SSL won't work. Make sure the apex domain's nameservers are correctly set.

### Exceeding subdomain length for preview URLs

Each DNS label has a [63-character limit](https://vercel.com/guides/why-is-my-vercel-deployment-url-being-shortened#rfc-1035). If you have a very long branch name plus a tenant subdomain, the fully generated preview URL might fail to resolve. Keep branch names concise.

### Duplicate content SEO issues

If the same site is served from both subdomain and custom domain, consider using [canonical](https://nextjs.org/docs/app/api-reference/functions/generate-metadata#alternates) tags or auto-redirecting to the primary domain.

### Misspelled domain

A small typo can block domain verification or routing, so double-check your domain spelling.

Last updated on June 12, 2025


---
title: Configuring Custom Subpaths
description: How to support custom subpaths for customers using a custom domain
product: Multi-Tenant
type: guide
summary: Host platform content on custom subpaths of customer domains while maintaining a single Next.js application.
prerequisites:
  - /docs/multi-tenant-platforms/concepts
  - /docs/multi-tenant-platforms/middleware-and-routing
related:
  - /docs/multi-tenant-platforms/configuring-domains
---

# Configuring Custom Subpaths



Custom subpaths let customers host your platform content on any path of their existing domain, like `company.com/docs` or `startup.com/help`, while you maintain a single Next.js application and they host the rest of their site separately.

## Single app, multiple subpaths

Use a catch-all route to handle all customer requests in one application:

```tsx title="app/sites/[...slug]/page.tsx"
export default async function CustomerSite({
  params,
}: {
  params: { slug: string[] };
}) {
  const [customerSlug, ...contentPath] = params.slug;

  // Load customer config
  const customer = await getCustomer(customerSlug);
  if (!customer) return notFound();

  // Load customer-specific content
  const content = await getContent(customer.id, contentPath.join("/"));

  return (
    <div>
      <h1>{customer.name}</h1>
      <div>{content}</div>
    </div>
  );
}
```

This handles requests like:

* `yourapp.com/sites/acme/getting-started`
* `yourapp.com/sites/startup/api-reference`

## Redirect subdomains to paths

Redirect customer subdomains to path-based routes:

```ts title="proxy.ts"
import { NextRequest, NextResponse } from "next/server";

export async function proxy(request: NextRequest) {
  const hostname = request.headers.get("host") || "";
  const { pathname } = request.nextUrl;

  // Check if it's a customer subdomain
  const subdomain = hostname.split(".")[0];

  if (
    subdomain !== "www" &&
    subdomain !== "app" &&
    hostname.includes("yourapp.com")
  ) {
    // Rewrite vercel.yourapp.com/guide -> yourapp.com/sites/vercel/guide
    const rewriteUrl = new URL(`/sites/${subdomain}${pathname}`, request.url);
    rewriteUrl.host = "yourapp.com";

    return NextResponse.rewrite(rewriteUrl);
  }

  return NextResponse.next();
}
```

## Set unique asset prefix

Configure Next.js to use a unique asset prefix to avoid conflicts:

```js title="next.config.js"
/** @type {import('next').NextConfig} */
const nextConfig = {
  assetPrefix: "/your-platform-assets",

  async rewrites() {
    return [
      {
        source: "/your-platform-assets/_next/:path*",
        destination: "/_next/:path*",
      },
    ];
  },
};

module.exports = nextConfig;
```

This ensures your CSS, JS, and images load from `/your-platform-assets/_next/...` instead of `/_next/...`.

## Customer domain setup

Customers map two paths on their domain to your platform:

**Content mapping:**

```
/docs/:path* -> https://yourapp.com/sites/customer-slug/:path*
```

**Asset mapping:**

```
/your-platform-assets/:path* -> https://yourapp.com/your-platform-assets/:path*
```

Example with Vercel [Proxy](https://nextjs.org/docs/app/getting-started/proxy):

```ts title="proxy.ts"
import { NextRequest, NextResponse } from "next/server";

export async function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // Route content requests
  if (pathname.startsWith("/docs/")) {
    const targetPath = pathname.replace("/docs/", "/sites/customer-slug/");
    const targetUrl = `https://yourapp.com${targetPath}`;

    return NextResponse.rewrite(new URL(targetUrl));
  }

  // Route asset requests
  if (pathname.startsWith("/your-platform-assets/")) {
    const targetUrl = `https://yourapp.com${pathname}`;
    return NextResponse.rewrite(new URL(targetUrl));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/docs/:path*", "/your-platform-assets/:path*"],
};
```

## Handle customer configuration

Store customer settings and customize the experience:

```tsx title="app/sites/[...slug]/layout.tsx"
export default async function CustomerLayout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: { slug: string[] };
}) {
  const customerSlug = params.slug[0];
  const customer = await getCustomer(customerSlug);

  return (
    <html>
      <head>
        <title>{customer.siteTitle}</title>
        <style
          dangerouslySetInnerHTML={{
            __html: `
            :root {
              --primary-color: ${customer.primaryColor};
              --font-family: ${customer.fontFamily};
            }
          `,
          }}
        />
      </head>
      <body>
        <nav style={{ backgroundColor: customer.primaryColor }}>
          <img src={customer.logo} alt={customer.name} />
        </nav>
        {children}
      </body>
    </html>
  );
}
```


---
title: Middleware and Routing
description: Middleware runs at the edge before requests reach your application, making it ideal for tenant resolution, routing decisions, and access control in multi-tenant platforms.
product: Multi-Tenant
type: guide
summary: Resolve tenants and route requests using Next.js middleware with subdomain, custom domain, and path-based strategies.
prerequisites:
  - /docs/multi-tenant-platforms/concepts
related:
  - /docs/multi-tenant-platforms/configuring-domains
  - /docs/multi-tenant-platforms/custom-subpaths
---

# Middleware and Routing



## Resolve tenants with middleware

### Subdomain-based tenant resolution

Extract tenant identity from subdomains like `tenant1.yourapp.com`:

```ts
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

export async function middleware(request: NextRequest) {
  const url = request.nextUrl;
  const hostname = request.headers.get('host') || '';
  
  // Extract subdomain (tenant identifier)
  const subdomain = hostname.split('.')[0];
  
  // Skip processing for main app domains
  if (subdomain === 'www' || subdomain === 'app' || subdomain === 'admin') {
    return NextResponse.next();
  }
  
  // Validate tenant exists (you might cache this for performance)
  const tenant = await validateTenant(subdomain);
  
  if (!tenant) {
    // Redirect to main app or show 404
    return NextResponse.redirect(new URL('/not-found', request.url));
  }
  
  // Add tenant context to the request
  const response = NextResponse.next();
  response.headers.set('x-tenant-id', tenant.id);
  response.headers.set('x-tenant-subdomain', subdomain);
  
  return response;
}

export const config = {
  matcher: [
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
};
```

This example:

* Extracts `tenant1` from `tenant1.yourapp.com`
* Validates the tenant exists in your database
* Adds tenant context to request headers for your app to use
* Redirects invalid tenants to a 404 page

### Custom domain tenant resolution

Handle custom domains like `tenant.com` mapping to tenants:

```ts
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { get } from '@vercel/edge-config';

export async function middleware(request: NextRequest) {
  const hostname = request.headers.get('host') || '';
  
  // Check if this is a custom domain
  const customDomainTenant = await get(`domain_${hostname}`);
  
  if (customDomainTenant) {
    // Custom domain found, set tenant context
    const response = NextResponse.next();
    response.headers.set('x-tenant-id', customDomainTenant.id);
    response.headers.set('x-tenant-type', 'custom-domain');
    return response;
  }
  
  // Fall back to subdomain resolution
  const subdomain = hostname.split('.')[0];
  const subdomainTenant = await get(`subdomain_${subdomain}`);
  
  if (subdomainTenant) {
    const response = NextResponse.next();
    response.headers.set('x-tenant-id', subdomainTenant.id);
    response.headers.set('x-tenant-type', 'subdomain');
    return response;
  }
  
  // No tenant found
  return NextResponse.redirect(new URL('/not-found', request.url));
}
```

This example:

* First checks if the hostname is a registered custom domain
* Falls back to subdomain parsing if not a custom domain
* Uses Edge Config for fast tenant lookups
* Sets tenant type so your app knows how the tenant was resolved

### Path-based tenant resolution

Extract tenant from URL paths like `/tenant1/dashboard`:

```ts
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  
  // Extract tenant from first path segment: /tenant_name_1/dashboard
  const pathSegments = pathname.split('/');
  const tenantSlug = pathSegments[1];
  
  if (!tenantSlug || tenantSlug.startsWith('_')) {
    return NextResponse.next();
  }
  
  // Validate tenant exists
  const tenant = await validateTenant(tenantSlug);
  
  if (!tenant) {
    return NextResponse.redirect(new URL('/not-found', request.url));
  }
  
  // Rewrite URL to remove tenant from path
  const newPath = `/${pathSegments.slice(2).join('/')}`;
  const rewriteUrl = new URL(newPath, request.url);
  
  const response = NextResponse.rewrite(rewriteUrl);
  response.headers.set('x-tenant-id', tenant.id);
  response.headers.set('x-tenant-slug', tenantSlug);
  
  return response;
}
```

This example:

* Extracts `tenant1` from `/tenant1/dashboard`
* Rewrites the URL to `/dashboard` (removes tenant from path)
* Your app receives `/dashboard` with tenant context in headers
* Skips processing for Next.js internal routes (starting with `_`)

## Route tenant-specific requests

### Tenant-aware page routing

Route different URLs to tenant-specific pages:

```ts
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const hostname = request.headers.get('host') || '';
  const tenantId = await resolveTenantId(hostname);
  
  if (!tenantId) {
    return NextResponse.redirect(new URL('/not-found', request.url));
  }
  
  // Route to tenant-specific pages
  if (pathname === '/') {
    // Rewrite homepage to tenant-specific version
    const url = request.nextUrl.clone();
    url.pathname = `/tenant/${tenantId}`;
    return NextResponse.rewrite(url);
  }
  
  if (pathname.startsWith('/blog')) {
    // Route blog requests to tenant-specific blog
    const url = request.nextUrl.clone();
    url.pathname = `/tenant/${tenantId}/blog${pathname.replace('/blog', '')}`;
    return NextResponse.rewrite(url);
  }
  
  // Add tenant context to all other requests
  const response = NextResponse.next();
  response.headers.set('x-tenant-id', tenantId);
  return response;
}
```

This example:

* Routes `tenant1.app.com/` to `/tenant/tenant1` page
* Routes `tenant1.app.com/blog/post-1` to `/tenant/tenant1/blog/post-1` page
* Other routes get tenant context but keep the same URL
* Your page components can access tenant ID from headers


---
title: Multi-tenant preview URLs
description: Test per tenant changes in preview deployments by adding dynamic prefixes to preview URLs.
product: Multi-Tenant
type: guide
summary: Test tenant-specific experiences in preview deployments using dynamic URL prefixes.
prerequisites:
  - /docs/multi-tenant-platforms/concepts
related:
  - /docs/multi-tenant-platforms/middleware-and-routing
---

# Multi-tenant preview URLs



Multi-tenant preview URLs let you test tenant-specific experiences in preview deployments without configuring additional domains. Add any prefix before `---` in a preview URL, and Vercel routes the request to the same deployment while passing the full hostname to your code.

<Callout>
  This feature requires a [custom preview suffix
  suffix](https://vercel.com/docs/deployments/preview-deployment-suffix). It
  does not work with the default `.vercel.app` suffix.
</Callout>

## The problem

Standard preview URLs like `my-app-git-feature.vercel.dev` work well for single-tenant applications, but multi-tenant apps need to test changes for each tenant separately.

Without tenant-aware previews, you would need to:

* Manually switch tenant context in your application
* Deploy separate preview environments per tenant
* Manually assign domains to each preview deployment

## The solution

You can add any **dynamic prefix** before `---` in your preview URL:

```
{prefix}---{preview-url}
```

Vercel routes the request to the same deployment as `{preview-url}`, but your code receives the full hostname including the prefix. This lets you extract the prefix and handle tenant routing however you want.

**Examples**:

| URL                                      | Routes to                       | Your code receives                             |
| ---------------------------------------- | ------------------------------- | ---------------------------------------------- |
| `acme---preview-xyz.vercel.dev`          | `preview-xyz.vercel.dev`        | `host: acme---preview-xyz.vercel.dev`          |
| `globex---my-app-git-feature.vercel.dev` | `my-app-git-feature.vercel.dev` | `host: globex---my-app-git-feature.vercel.dev` |
| `tenant-123---my-app-abc123.vercel.dev`  | `my-app-abc123.vercel.dev`      | `host: tenant-123---my-app-abc123.vercel.dev`  |

## How it works

1. User visits `{tenant}---{preview-url}`
2. Vercel routes the request to the deployment at `{preview-url}`
3. Your code receives the full hostname: `{tenant}---{preview-url}`
4. Your code extracts the prefix and handles routing

The prefix can be anything—a tenant ID, workspace name, feature flag, or anything else your application needs.

## Reference implementation

Below if a reference implementation of extracting a tenant prefix from the hostname and routing to the `/[domain]/page.tsx` path.

```ts title="middleware.ts"
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { getSubdomain } from "tldts";

export async function middleware(request: NextRequest) {
  const hostname = request.headers.get("host") || request.nextUrl.hostname;
  const subdomain = getSubdomain(hostname) || "";
  const [tenantPart] = subdomain.includes("---") ? subdomain.split("---") : [];

  if (!tenantPart) {
    return NextResponse.next();
  }
  const url = request.nextUrl.clone();
  const pathname = url.pathname;
  // Rewrite to tenant-prefixed path
  url.pathname = `/${tenantPart}${pathname}`;
  return NextResponse.rewrite(url);
}

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};
```

<Callout type="info">
  Checkout multi-tenant preview URLs in action with this
  [demo](https://multi-tenant-preview-urls-k6oodlv4w.vercel.rocks/all).
</Callout>

## Limitations

* Preview URL prefixes only work with custom deployment URL suffixes, not the default `.vercel.app`
* The prefix must appear before `---` in the preview URL path
* Total hostname length must not exceed DNS limits (253 characters)


---
title: Quickstart
description: Create an application to allow your customers to assign custom domains
product: Multi-Tenant
type: guide
summary: Set up wildcard domains, custom domains, domain verification, and redirects for a multi-tenant application.
prerequisites:
  - /docs/multi-tenant-platforms/concepts
related:
  - /docs/multi-tenant-platforms/configuring-domains
  - /docs/multi-tenant-platforms/middleware-and-routing
  - /docs/platform-elements/blocks/custom-domain
---

# Quickstart



<Video src="https://www.youtube.com/embed/vVYlCnNjEWA?si=FlkfRF7ktnKCS_VH" />

## Types of domains

This guide walks you through setting up domains for your multi-tenant application. There are two main approaches:

### Wildcard Domains (\*.acme.com)

If you want to offer subdomains like `tenant.acme.com`, you'll need to:

1. Configure wildcard domains for subdomains like `*.acme.com`

### Custom Domains (tenant.com)

If you want to allow tenants to use their own domains, you'll need to:

1. Enable custom domain support for your tenants
2. Add domains programmatically using the Vercel SDK
3. Verify domain ownership with TXT records
4. Configure domain redirects and handle SEO
5. Clean up domains when tenants leave

You can implement either approach or both depending on your needs.

## Wildcard Domain Setup

If you plan on offering subdomains like `*.acme.com`, add a wildcard domain to your Vercel project. This requires using [Vercel's nameservers](https://vercel.com/docs/projects/domains/working-with-nameservers) so that Vercel can manage the DNS challenges necessary for generating wildcard SSL certificates.

1. Point your domain to Vercel's nameservers (`ns1.vercel-dns.com` and `ns2.vercel-dns.com`).
2. In your Vercel project settings, add the apex domain (e.g., `acme.com`).
3. Add a wildcard domain: `.acme.com`.

Now, any `tenant.acme.com` you create—whether it's `tenant1.acme.com` or `docs.tenant1.acme.com`—automatically resolves to your Vercel deployment. Vercel issues individual certificates for each subdomain on the fly.

## Custom Domain Setup

The following steps guide you through setting up custom domain support for your tenants. This allows them to use their own domains (e.g., `customacmesite.com`) instead of subdomains.

### Step 1: Enable Custom Domains

You can also give tenants the option to bring their own domain. In that case, you'll want your code to:

1. Provision and assign the tenant's domain to your Vercel project.
2. Verify the domain (to ensure the tenant truly owns it).
3. Automatically generate an SSL certificate.

### Step 2: Add Domains Programmatically

You can add a new domain through the [Vercel SDK](https://vercel.com/docs/sdk). For example:

```ts title="add-domain.ts"
import { VercelCore as Vercel } from "@vercel/sdk/core.js";
import { projectsAddProjectDomain } from "@vercel/sdk/funcs/projectsAddProjectDomain.js";

const vercel = new Vercel({
  bearerToken: process.env.VERCEL_TOKEN,
});

// The 'idOrName' is your project name in Vercel, for example: 'multi-tenant-app'
await projectsAddProjectDomain(vercel, {
  idOrName: "my-multi-tenant-app",
  teamId: "team_1234",
  requestBody: {
    // The tenant's custom domain
    name: "customacmesite.com",
  },
});
```

Once the domain is added, Vercel attempts to issue an SSL certificate automatically.

### Step 3: Verify Domain Ownership

If the domain is already in use on Vercel, the user needs to set a TXT record to prove ownership of it.

You can check the verification status and trigger manual verification:

```ts title="verify-domain.ts"
import { VercelCore as Vercel } from "@vercel/sdk/core.js";
import { projectsGetProjectDomain } from "@vercel/sdk/funcs/projectsGetProjectDomain.js";
import { projectsVerifyProjectDomain } from "@vercel/sdk/funcs/projectsVerifyProjectDomain.js";

const vercel = new Vercel({
  bearerToken: process.env.VERCEL_TOKEN,
});

const domain = "customacmesite.com";

const [domainResponse, verifyResponse] = await Promise.all([
  projectsGetProjectDomain(vercel, {
    idOrName: "my-multi-tenant-app",
    teamId: "team_1234",
    domain,
  }),
  projectsVerifyProjectDomain(vercel, {
    idOrName: "my-multi-tenant-app",
    teamId: "team_1234",
    domain,
  }),
]);

const { value: result } = verifyResponse;

if (!result?.verified) {
  console.log(`Domain verification required for ${domain}.`);
  // You can prompt the tenant to add a TXT record or switch nameservers.
}
```

### Step 4: Configure Redirects

#### Redirecting between apex and "www"

Some tenants might want `www.customacmesite.com` to redirect automatically to their apex domain `customacmesite.com`, or the other way around.

1. Add both `customacmesite.com` and `www.customacmesite.com` to your Vercel project.
2. Configure a redirect for `www.customacmesite.com` to the apex domain by setting `redirect: customacmesite.com` through the API or your Vercel dashboard.

This ensures a consistent user experience and prevents issues with duplicate content.

#### Avoiding duplicate content across subdomains

If you offer both `tenant.acme.com` and `customacmesite.com` for the same tenant, you may want to redirect the subdomain to the custom domain (or vice versa) to avoid search engine duplicate content. Alternatively, set a canonical URL in your HTML `<head>` to indicate which domain is the "official" one.

### Step 5: Clean Up Domains

If a tenant cancels or no longer needs their custom domain, you can remove it from your Vercel account using the SDK:

```ts title="remove-domain.ts"
import { VercelCore as Vercel } from "@vercel/sdk/core.js";
import { projectsRemoveProjectDomain } from "@vercel/sdk/funcs/projectsRemoveProjectDomain.js";
import { domainsDeleteDomain } from "@vercel/sdk/funcs/domainsDeleteDomain.js";

const vercel = new Vercel({
  bearerToken: process.env.VERCEL_TOKEN,
});

await Promise.all([
  projectsRemoveProjectDomain(vercel, {
    idOrName: "my-multi-tenant-app",
    teamId: "team_1234",
    domain: "customacmesite.com",
  }),
  domainsDeleteDomain(vercel, {
    domain: "customacmesite.com",
  }),
]);
```

The first call disassociates the domain from your project, and the second removes it from your account entirely.


---
title: Reference
description: API reference related to Multi Tenant Projects
product: Multi-Tenant
type: reference
summary: Domain API reference, error codes, troubleshooting, and FAQ for multi-tenant platforms.
prerequisites:
  - /docs/multi-tenant-platforms/concepts
related:
  - /docs/multi-tenant-platforms/configuring-domains
  - /docs/platform-elements/blocks/custom-domain
  - /docs/platform-elements/blocks/dns-table
---

# Reference



## Custom blocks

Start with our Custom [Blocks](/docs/platform-elements/blocks/custom-domain) and [Actions](/docs/platform-elements/actions/add-custom-domain) that speed up your usage of the Vercel API.

## Domain API reference

### Add domain

Add a domain to your Vercel project programmatically using the [create or transfer domain API](https://vercel.com/docs/rest-api/reference/endpoints/domains/add-an-existing-domain-to-the-vercel-platform).

**SDK**:

```ts title="add-domain.ts"
import { Vercel } from "@vercel/sdk";

const vercel = new Vercel({
  bearerToken: "<YOUR_BEARER_TOKEN_HERE>",
});

async function run() {
  const result = await vercel.domains.createOrTransferDomain({
    teamId: "team_1a2b3c4d5e6f7g8h9i0j1k2l",
    slug: "my-team-url-slug",
    requestBody: {
      name: "example.com",
      method: "add",
      token: "fdhfr820ad#@FAdlj$$",
    },
  });

  console.log(result);
}

run();
```

### Get domain status

Check domain configuration and verification status using the [check domain API](https://vercel.com/docs/rest-api/reference/endpoints/domains/get-information-for-a-single-domain).

**SDK**:

```ts title="get-domain-status.ts"
import { Vercel } from "@vercel/sdk";

const vercel = new Vercel({
  bearerToken: "<YOUR_BEARER_TOKEN_HERE>",
});

async function run() {
  const result = await vercel.domains.getDomain({
    domain: "example.com",
    teamId: "team_1a2b3c4d5e6f7g8h9i0j1k2l",
    slug: "my-team-url-slug",
  });

  console.log(result);
}

run();
```

### Verify domain

Trigger domain ownership verification using the [domain configuration API](https://vercel.com/docs/rest-api/reference/endpoints/domains/get-a-domains-configuration).

**SDK**:

```ts title="verify-domain.ts"
import { Vercel } from "@vercel/sdk";

const vercel = new Vercel({
  bearerToken: "<YOUR_BEARER_TOKEN_HERE>",
});

async function run() {
  const result = await vercel.domains.getDomainConfig({
    domain: "example.com",
    teamId: "team_1a2b3c4d5e6f7g8h9i0j1k2l",
    slug: "my-team-url-slug",
  });

  console.log(result);
}

run();
```

### Remove domain

Remove a domain from your project using the [remove domain API](https://vercel.com/docs/rest-api/reference/endpoints/domains/remove-a-domain-by-name).

**SDK**:

```ts title="remove-domain.ts"
import { Vercel } from "@vercel/sdk";

const vercel = new Vercel({
  bearerToken: "<YOUR_BEARER_TOKEN_HERE>",
});

async function run() {
  const result = await vercel.domains.deleteDomain({
    domain: "example.com",
    teamId: "team_1a2b3c4d5e6f7g8h9i0j1k2l",
    slug: "my-team-url-slug",
  });

  console.log(result);
}

run();
```

### List domains

Get all domains for a project using the [list domains API](https://vercel.com/docs/rest-api/reference/endpoints/domains/list-all-the-domains).

**REST API**:

```ts title="list-domains.ts"
import { Vercel } from "@vercel/sdk";

const vercel = new Vercel({
  bearerToken: "<YOUR_BEARER_TOKEN_HERE>",
});

async function run() {
  const result = await vercel.domains.getDomains({
    limit: 20,
    since: 1609499532000,
    until: 1612264332000,
    teamId: "team_1a2b3c4d5e6f7g8h9i0j1k2l",
    slug: "my-team-url-slug",
  });

  console.log(result);
}

run();
```

### Error codes

| Code                    | Description                       | Solution                                |
| ----------------------- | --------------------------------- | --------------------------------------- |
| `domain_already_in_use` | Domain is used by another project | Verify domain ownership with TXT record |
| `invalid_domain`        | Domain format is invalid          | Check domain spelling and format        |
| `forbidden`             | Insufficient permissions          | Check API token permissions             |
| `rate_limit_exceeded`   | Too many requests                 | Wait and retry with exponential backoff |

## Troubleshooting

### DNS Propagation Delays

**Problem**: Domain not resolving after adding to project.

**Solution**:

* DNS changes take 24-48 hours to propagate globally
* Use [WhatsMyDNS](https://www.whatsmydns.net/) to check propagation
* Verify nameservers are set correctly
* Clear your local DNS cache: `sudo dscacheutil -flushcache` (macOS)

### Domain Verification Failures

**Problem**: Domain verification failing with TXT record added.

**Solution**:

* Wait 5-10 minutes after adding TXT record
* Verify TXT record is set correctly: `dig TXT _vercel.tenant1.com`
* Ensure no trailing dots in TXT value
* Check for duplicate TXT records
* Try verification again via SDK

### Wildcard Domain Not Working

**Problem**: Subdomains not routing to your application.

**Solution**:

* Verify nameservers point to `ns1.vercel-dns.com` and `ns2.vercel-dns.com`
* Confirm wildcard domain (`.yourapp.com`) is added to project
* Wait for DNS propagation (up to 48 hours)
* Check wildcard certificate status in project settings
* Ensure apex domain is also added to project

### SSL Certificate Not Issued

**Problem**: Domain shows "Certificate Error" in browser.

**Solution**:

* Complete domain verification first
* Wait 5-10 minutes for certificate issuance
* Check domain status in Vercel dashboard
* Ensure no CAA records blocking Let's Encrypt
* Verify domain is not on SSL blacklist

### Preview URL Not Resolving

**Problem**: Preview deployment URLs not working with custom domains.

**Solution**:

* Preview URLs with custom domains require Enterprise plan
* Use subdomain-based preview URLs: `branch-name---project.vercel.app`
* Contact sales to upgrade for multi-tenant preview URLs
* Keep branch names under 63 characters (DNS label limit)

### SEO Duplicate Content

**Problem**: Same content served on multiple domains.

**Solution**:

* Set canonical URLs pointing to primary domain
* Redirect subdomain to custom domain (or vice versa)
* Use consistent domain in sitemaps
* Configure 301 redirects in middleware

```tsx title="app/layout.tsx"
// app/layout.tsx
export async function generateMetadata() {
  return {
    alternates: {
      canonical: "https://primary-domain.com",
    },
  };
}
```

## FAQ

### What's the difference between Multi-Tenant and Multi-Project?

**Multi-Tenant**: Single codebase serving multiple tenants with their own domains. All tenants share the same deployment.

**Multi-Project**: Multiple projects, each with unique code and isolated deployments. Each tenant has their own Vercel project.

Use Multi-Tenant when all tenants need the same functionality but different content. Use Multi-Project when tenants need custom code.

### How many domains can I add per project?

* **Hobby**: 50 domains
* **Pro**: Unlimited (soft limit: 100,000)
* **Enterprise**: Unlimited (soft limit: 1,000,000)

Soft limits can be increased by [contacting support](/help).

### How do I get unlimited domains?

Upgrade to the Pro plan for unlimited custom domains. [View pricing](/pricing).

### What are multi-tenant preview URLs?

Multi-tenant preview URLs let you test changes for specific tenants before deploying to production. They follow the format: `tenant1---project-git-branch.vercel.app`.

This feature is **Enterprise only**. Contact your sales representative to enable it.

### How is pricing calculated?

Multi-tenant applications are priced based on:

* **Team plan**: Hobby, Pro, or Enterprise
* **Usage**: Bandwidth, function invocations, build minutes
* **Domains**: No additional cost for domains (within plan limits)

See [pricing documentation](/pricing) for details.

### What security features are available?

All Vercel applications include:

* **Firewall**: DDoS protection and rate limiting
* **WAF**: Web Application Firewall
* **SSL certificates**: Automatic HTTPS for all domains
* **Edge network**: Global CDN with low latency

### How can I monitor domain operations?

* **Vercel Dashboard**: View domain status and SSL certificates
* **API**: Query domain status programmatically
* **Webhooks**: Get notified of domain events (Enterprise)
* **Logs**: View domain resolution and errors

### How do I handle DNS propagation delays?

DNS changes take 24-48 hours to propagate. Use [WhatsMyDNS](https://www.whatsmydns.net/) to monitor propagation across global nameservers.

### Why isn't my SSL certificate being issued?

SSL certificates require domain verification. Add the TXT record provided by Vercel, wait 5-10 minutes, then trigger verification via the SDK or dashboard.

### How do I handle SEO with multiple domains?

Set canonical URLs to indicate the primary domain for each page. This prevents duplicate content issues.


---
title: Serving static files
description: Serve tenant-specific robots.txt, sitemap.xml, and llms.txt files using route handlers.
product: Multi-Tenant
type: guide
summary: Serve tenant-specific robots.txt, sitemap.xml, and llms.txt files using Next.js route handlers.
prerequisites:
  - /docs/multi-tenant-platforms/concepts
  - /docs/multi-tenant-platforms/middleware-and-routing
related:
  - /docs/multi-tenant-platforms/configuring-domains
---

# Serving static files



Multi-tenant applications need tenant-specific versions of static files like `robots.txt`, `sitemap.xml`, and agent discovery files like `llms.txt`. You can use route handlers to serve these files dynamically per tenant.

Next.js provides [built-in metadata file conventions](https://nextjs.org/docs/app/api-reference/file-conventions/metadata) for `robots.txt`, `sitemap.xml`, and other common files. Use route handlers when you need to serve files not covered by the metadata API, like `llms.txt` or custom discovery files.

## Route handler

Create a route handler that resolves the tenant and returns the appropriate content:

```ts title="app/[domain]/robots.txt/route.ts"
import { NextRequest, NextResponse } from "next/server";

export async function GET(
  request: NextRequest,
  { params }: { params: { domain: string } }
) {
  const { domain } = params;
  const tenant = await getTenant(domain);

  if (!tenant) {
    return new NextResponse("Not found", { status: 404 });
  }

  const content = `User-agent: *
Allow: /
Sitemap: https://${tenant.domain}/sitemap.xml`;

  return new NextResponse(content, {
    headers: {
      "Content-Type": "text/plain",
      "Cache-Control": "public, max-age=3600",
    },
  });
}
```

## Proxy integration

Your [Proxy](https://nextjs.org/docs/app/getting-started/proxy) must allow static file paths to reach route handlers instead of rewriting them:

```ts title="proxy.ts"
const STATIC_FILE_PATHS = [
  "/robots.txt",
  "/sitemap.xml",
  "/llms.txt",
  "/.well-known",
];

export function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl;

  if (STATIC_FILE_PATHS.some((path) => pathname.startsWith(path))) {
    return NextResponse.next();
  }

  // Your other logic
}
```

<Callout type="info">
  In Next.js 16+, the `middleware.ts` file was renamed to `proxy.ts`. See the [proxy.ts documentation](https://nextjs.org/docs/app/api-reference/file-conventions/proxy) for more details.
</Callout>

## Content types

Set the correct `Content-Type` header for each file type:

| Extension | Content-Type      |
| --------- | ----------------- |
| `.txt`    | `text/plain`      |
| `.xml`    | `application/xml` |
| `.html`   | `text/html`       |

## Caching

Use the `Cache-Control` or `CDN-Cache-Control` header to cache responses in the Vercel CDN cache. [Invalidate your resource in the CDN cache](https://vercel.com/docs/cdn-cache/purge) when tenant content changes.

```ts
return new NextResponse(content, {
  headers: {
    "Content-Type": "text/plain",
    "CDN-Cache-Control": "s-maxage=3600",
  },
});
```

The main difference between these headers is that `CDN-Cache-Control` allows you to control cache behavior separately from browser cache behavior. For a more thorough explanation, read more about [cache control options on Vercel](https://vercel.com/docs/cdn-cache#cache-control-options).

## When not to use this pattern

* **Truly static assets**: Use `/public` for files that don't change per tenant
* **Large files or media**: Use dedicated [blob storage](https://vercel.com/docs/vercel-blob)


---
title: Claim Deployment
description: A ready-to-use component that allows users to claim ownership of Vercel deployments created on their behalf
product: Multi-Project
type: reference
summary: A component for users to claim ownership of Vercel deployments created on their behalf.
related:
  - /docs/platform-elements/actions/deploy-files
  - /docs/platform-elements/blocks/deploy-popover
---

# Claim Deployment



<Preview example="claim-deployment" source="blocks/claim-deployment" />

## Overview

The Claim Deployment block provides a polished interface for platforms that deploy sites to Vercel on behalf of their users. When you create a deployment programmatically (e.g., through Mintlify, Hashnode, or similar platforms), users can claim ownership to manage updates and settings directly from their own Vercel account.

## Installation

<Installer path="claim-deployment" />

## Features

* **Visual deployment preview**: Shows a preview image of the deployed site
* **One-click URL copying**: Users can easily copy their deployment URL with the external link button
* **Vercel branding**: Includes the official Vercel logo for authenticity
* **Responsive design**: Works seamlessly across desktop and mobile devices

## Usage

```tsx title="claim-deployment.tsx"
import { ClaimDeployment } from '@/components/blocks/claim-deployment'

export default function DeploymentReady() {
  const handleClaim = () => {
    // Redirect to Vercel OAuth flow or handle claim logic
    window.location.href = `https://vercel.com/oauth/authorize?...`
  }

  return (
    <ClaimDeployment
      url="https://my-app.vercel.app"
      onClaimClick={handleClaim}
    />
  )
}
```

## Props

<TypeTable
  type={{
  url: {
    description: 'The deployment URL to display and allow copying',
    type: 'string',
    required: true,
  },
  onClaimClick: {
    description: 'Callback function triggered when the "Claim Deployment" button is clicked',
    type: '() => void',
    required: true,
  },
}}
/>

## Customization

The component uses shadcn/ui components internally, allowing you to customize the appearance through your existing theme configuration. The preview image placeholder (`https://placehold.co/1440x700`) should be replaced with an actual screenshot of the deployment.

## Integration Flow

1. **Deploy via API**: Your platform creates a deployment using Vercel's API
2. **Show claim interface**: Present this component to the user with their deployment URL
3. **Handle claim action**: When clicked, redirect to Vercel's OAuth flow or your custom claim process
4. **Transfer ownership**: Complete the transfer so users can manage the deployment from their Vercel dashboard


---
title: Custom Domain
description: Complete domain management interface with DNS verification and status tracking
product: Multi-Tenant
type: reference
summary: A complete domain management interface with DNS verification and real-time status tracking.
related:
  - /docs/platform-elements/blocks/dns-table
  - /docs/platform-elements/actions/add-custom-domain
  - /docs/multi-tenant-platforms/configuring-domains
---

# Custom Domain



<Preview example="custom-domain" source="blocks/custom-domain" />

## Overview

The Custom Domain block provides a comprehensive solution for platforms that need to offer custom domain functionality to their users. It handles the entire domain configuration flow including DNS verification, real-time status updates, and clear configuration instructions. This is essential for platforms like Mintlify and Hashnode that allow users to serve content from their own domains.

## Installation

<Installer path="custom-domain" />

## Features

* **Real-time DNS verification**: Automatically checks domain configuration every 20 seconds
* **Smart DNS record detection**: Determines whether TXT verification, CNAME, or A records are needed
* **Visual status indicators**: Clear icons show pending, invalid, or successful configuration
* **One-click copying**: Users can easily copy DNS values with confirmation feedback
* **Responsive DNS table**: Clean presentation of required DNS records
* **Automatic domain validation**: Validates domain format and converts to lowercase

## Usage

```tsx title="custom-domain.tsx"
import { CustomDomain } from "@/components/blocks/custom-domain";

export default function DomainSettings() {
  return <CustomDomain defaultDomain="docs.example.com" />;
}
```

## Server Actions

The component relies on two server actions that you need to implement:

### `addDomain(domain: string)`

Adds a domain to your Vercel project:

```ts title="vercel.ts"
import { vercel } from "@/lib/vercel";

export async function addDomain(domain: string) {
  const response = await vercel.projects.addProjectDomain({
    idOrName: process.env.PROJECT_ID,
    requestBody: {
      name: domain,
      redirect: null,
      gitBranch: null,
    },
  });
  return response;
}
```

### `getDomainStatus(domain: string)`

Checks the current DNS configuration status:

```ts title="vercel.ts"
export async function getDomainStatus(domain: string) {
  const response = await vercel.domains.getDomainConfig({
    domain,
  });

  return {
    status: response.verified
      ? "Valid Configuration"
      : response.verification
        ? "Pending Verification"
        : "Invalid Configuration",
    dnsRecordsToSet: response.verification?.dns || response.dns,
  };
}
```

## Props

<TypeTable
  type={{
  defaultDomain: {
    description: "Optional pre-filled domain value",
    type: "string",
    optional: true,
  },
}}
/>

## Subcomponents

### DomainConfiguration

Handles the DNS record display and refresh logic:

```tsx title="custom-domain.tsx"
<DomainConfiguration domain="example.com" className="custom-class" />
```

### DomainStatusIcon

Shows visual status of domain configuration:

```tsx title="custom-domain.tsx"
<DomainStatusIcon domain="example.com" />
```

### InlineSnippet

Styled inline code display for domain values:

```tsx title="custom-domain.tsx"
<InlineSnippet>_vercel.example.com</InlineSnippet>
```

## DNS Record Types

The component supports all common DNS record types:

* **TXT**: Domain ownership verification
* **CNAME**: Subdomain pointing
* **A**: IPv4 address mapping
* **AAAA**: IPv6 address mapping
* **MX**: Mail server configuration
* **NS**: Nameserver delegation

## Integration Example

```tsx title="app/settings/domains/page.tsx"
// app/settings/domains/page.tsx
import { CustomDomain } from "@/components/blocks/custom-domain";
import { getCurrentUserDomain } from "@/lib/db";

export default async function DomainsPage() {
  const currentDomain = await getCurrentUserDomain();

  return (
    <div className="container mx-auto py-8">
      <h1 className="text-2xl font-bold mb-6">Domain Settings</h1>
      <CustomDomain defaultDomain={currentDomain} />
    </div>
  );
}
```

## Best Practices

* **Rate limiting**: Implement rate limiting on domain addition to prevent abuse
* **Domain validation**: Validate domain ownership before allowing configuration
* **Error handling**: Provide clear error messages for invalid domains
* **SSL certificates**: Vercel automatically provisions SSL certificates once configured
* **Monitoring**: Track domain configuration success rates and common issues


---
title: Deploy Popover
description: A popover interface for deploying files to Vercel with real-time status tracking
product: Multi-Project
type: reference
summary: A popover interface for deploying files to Vercel with real-time status tracking.
related:
  - /docs/platform-elements/actions/deploy-files
  - /docs/platform-elements/blocks/claim-deployment
---

# Deploy Popover



<Preview example="deploy-popover" source="blocks/deploy-popover" />

## Overview

The Deploy Popover component provides a user-friendly popover interface for deploying files to Vercel. It includes real-time deployment status tracking, error handling, and the ability to inspect or visit deployments once they're ready.

## Installation

<Installer path="deploy-popover" />

## Features

* **One-click deployment**: Simple button interface to trigger deployments
* **Real-time status tracking**: Monitor deployment progress with live updates
* **Deployment states**: Clear visual feedback for deploying, building, ready, and error states
* **Direct deployment access**: Links to inspect deployments during build or visit when ready
* **Error handling**: Graceful error state management with user feedback
* **Automatic polling**: Built-in SWR polling for deployment status updates

## Usage

```tsx title="deploy-popover.tsx"
import { DeployPopover } from "@/components/blocks/deploy-popover"

export default function MyComponent() {
  return (
    <div className="flex items-center justify-center p-8">
      <DeployPopover />
    </div>
  )
}
```

## Component States

The component manages several deployment states:

### Idle

Initial state before any deployment action. Shows "Deploy to Vercel" button.

### Deploying

Active deployment in progress. Shows "Inspect Deployment" link.

### Polling

Checking deployment status after initial deployment. Shows progress messages like "Building application..." or "Initializing deployment..."

### Ready

Deployment successfully completed. Shows "Visit Deployment" link.

### Error

Deployment failed. Shows error message with option to inspect the failed deployment.

## Customization

### Custom Files

You can customize the files being deployed by modifying the `files` array:

```tsx title="deploy-popover.tsx"
const files = [
  {
    file: "index.html",
    data: "<h1>Your custom content</h1>"
  },
  {
    file: "style.css",
    data: "body { font-family: system-ui; }"
  }
] satisfies InlinedFile[]
```

### Project Configuration

Set a specific Vercel project ID for deployments:

```tsx title="deploy-popover.tsx"
const [projectId, setProjectId] = useState<string | null>(
  "your-project-id-here"
)
```

### Deployment Name

Customize the deployment name in the `deployFiles` call:

```tsx title="deploy-popover.tsx"
deployFiles(arg.paths, {
  projectId: arg.projectId ?? undefined,
  deploymentName: "your-custom-deployment-name",
})
```

## Integration with Deploy Files Action

This component works seamlessly with the Deploy Files server action:

```tsx title="deploy-popover.tsx"
import { deployFiles, getDeploymentStatus } from "@/actions/deploy-files"

// Deploy files to Vercel
const result = await deployFiles(files, {
  projectId: projectId ?? undefined,
  deploymentName: "platforms-deploy-test",
})

// Check deployment status
const status = await getDeploymentStatus(deploymentId)
```

## Polling Configuration

The component uses SWR for automatic status polling with these defaults:

* **Refresh interval**: 10 seconds
* **Error retry count**: 3 attempts
* **Error retry interval**: 2 seconds

You can adjust these in the `useSWR` configuration:

```tsx title="deploy-popover.tsx"
const REFRESH_INTERVAL = 10_000 // 10 seconds

useSWR(
  deploymentId,
  getDeploymentStatus,
  {
    refreshInterval: REFRESH_INTERVAL,
    errorRetryCount: 3,
    errorRetryInterval: 2000,
  }
)
```


---
title: DNS Table
description: A professional DNS record display component with one-click copying and support for all standard record types
product: Multi-Tenant
type: reference
summary: A DNS record display component with one-click copying for guiding users through domain configuration.
related:
  - /docs/platform-elements/blocks/custom-domain
  - /docs/platform-elements/actions/add-custom-domain
  - /docs/multi-tenant-platforms/configuring-domains
---

# DNS Table



<Preview example="dns-table" source="blocks/dns-table" />

## Overview

The DNS Table block provides a clean, user-friendly interface for displaying DNS records that users need to configure. It's designed for platforms that guide users through domain configuration, offering one-click copying of values and clear visual organization. This component is essential for domain management interfaces in platforms like Mintlify and Hashnode.

## Installation

<Installer path="dns-table" />

## Features

* **One-click copying**: Each DNS value has a copy button with visual confirmation
* **All record types**: Supports A, AAAA, CNAME, MX, NS, SOA, PTR, SRV, TXT, and CAA records
* **Responsive design**: Table adapts gracefully to different screen sizes
* **Monospace formatting**: DNS values displayed in monospace font for clarity
* **TTL support**: Optional TTL values with "Auto" as default
* **Empty state handling**: Clean message when no records are present
* **Accessibility**: Full keyboard navigation and screen reader support

## Usage

### Basic Example

```tsx title="dns-table.tsx"
import { DNSTable } from '@/components/blocks/dns-table'

const records = [
  {
    type: "CNAME",
    name: "docs",
    value: "cname.vercel-dns.com",
    ttl: "3600"
  },
  {
    type: "TXT",
    name: "_vercel",
    value: "vc-domain-verify=abc123def456..."
  }
]

export default function DNSConfiguration() {
  return <DNSTable records={records} />
}
```

### With Custom Domain Component

```tsx title="dns-table.tsx"
import { DNSTable } from '@/components/blocks/dns-table'
import { CustomDomain } from '@/components/blocks/custom-domain'

export default function DomainSettings() {
  // The CustomDomain component internally uses DNSTable
  // to display required DNS records
  return <CustomDomain defaultDomain="example.com" />
}
```

## Props

### DNSTable Props

<TypeTable
  type={{
  records: {
    description: 'Array of DNS records to display',
    type: 'DNSRecord[]',
    required: true,
  },
  copyTimeout: {
    description: 'Duration to show copy confirmation in milliseconds',
    type: 'number',
    default: 2000,
    optional: true,
  },
}}
/>

### DNSRecord Type

```ts
type DNSRecord = {
  type: "A" | "AAAA" | "CNAME" | "MX" | "NS" |
        "SOA" | "PTR" | "SRV" | "TXT" | "CAA"
  name: string
  value: string
  ttl?: string
}
```

### Record Type Descriptions

* **A**: Maps domain to IPv4 address (e.g., 192.0.2.1)
* **AAAA**: Maps domain to IPv6 address (e.g., 2001:db8::1)
* **CNAME**: Creates alias to another domain
* **MX**: Specifies mail servers for domain
* **NS**: Identifies authoritative nameservers
* **SOA**: Contains zone administrative information
* **PTR**: Reverse DNS lookup (IP to domain)
* **SRV**: Specifies service locations (port/host)
* **TXT**: Stores text data (verification, SPF, etc.)
* **CAA**: Controls SSL certificate authorities

## Advanced Examples

### Domain Verification Flow

```tsx title="dns-table.tsx"
import { DNSTable } from '@/components/blocks/dns-table'
import { useState } from 'react'

export function DomainVerification({ domain }) {
  const [verificationRecord] = useState({
    type: "TXT",
    name: `_vercel.${domain}`,
    value: `vc-domain-verify=${generateVerificationCode()}`
  })

  return (
    <div className="space-y-4">
      <h3>Add this TXT record to verify ownership:</h3>
      <DNSTable records={[verificationRecord]} />
      <Button onClick={checkVerification}>
        Check Verification
      </Button>
    </div>
  )
}
```

### Multiple Record Types

```tsx
import { DNSTable } from '@/components/blocks/dns-table'

const complexDNSSetup = [
  // Root domain A records
  {
    type: "A",
    name: "@",
    value: "76.76.21.21"
  },
  // Subdomain CNAME
  {
    type: "CNAME",
    name: "www",
    value: "cname.vercel-dns.com"
  },
  // Mail configuration
  {
    type: "MX",
    name: "@",
    value: "10 mail.example.com",
    ttl: "3600"
  },
  // SPF record for email
  {
    type: "TXT",
    name: "@",
    value: "v=spf1 include:_spf.example.com ~all"
  }
]

export default function CompleteDNSSetup() {
  return (
    <Card>
      <CardHeader>
        <CardTitle>Required DNS Records</CardTitle>
        <CardDescription>
          Add all these records to complete your setup
        </CardDescription>
      </CardHeader>
      <CardContent>
        <DNSTable records={complexDNSSetup} />
      </CardContent>
    </Card>
  )
}
```

## Subcomponents

### DNSCopyButton

The copy button component used for each DNS value:

```tsx title="dns-table.tsx"
import { DNSCopyButton } from '@/components/blocks/dns-table'

<DNSCopyButton
  text="76.76.21.21"
  copyTimeout={3000} // Optional: customize confirmation duration
/>
```

## Styling

The component uses Tailwind classes and can be customized through your theme configuration:

```css
/* Customize table appearance */
.dns-table {
  @apply border-2 border-primary;
}

/* Customize copy button */
.dns-copy-button {
  @apply hover:bg-primary/10;
}
```

## Best Practices

* **Value validation**: Validate DNS values before displaying to prevent copy errors
* **Record grouping**: Group related records together (e.g., all A records)
* **Clear instructions**: Provide context about where users should add these records
* **Progress tracking**: Consider showing which records have been configured
* **Help links**: Include links to DNS provider documentation when possible

## Integration with DNS Providers

```tsx title="dns-table.tsx"
// Example: Provider-specific instructions
const DNSInstructions = ({ provider, records }) => {
  const getProviderInstructions = (provider) => {
    switch(provider) {
      case 'cloudflare':
        return 'Log in to Cloudflare and navigate to DNS settings'
      case 'godaddy':
        return 'Access your GoDaddy DNS Management panel'
      default:
        return 'Add these records in your DNS provider'
    }
  }

  return (
    <div className="space-y-4">
      <Alert>
        <AlertDescription>
          {getProviderInstructions(provider)}
        </AlertDescription>
      </Alert>
      <DNSTable records={records} />
    </div>
  )
}
```


---
title: Report Abuse
description: A comprehensive abuse reporting system with categorization, validation, and privacy-focused design
product: Vercel for Platforms
type: reference
summary: A content moderation interface for reporting abuse with categorization, validation, and privacy-focused design.
related:
  - /docs/platform-elements/blocks/custom-domain
---

# Report Abuse



<Preview example="report-abuse" source="blocks/report-abuse" />

## Overview

The Report Abuse block provides platforms with a professional content moderation interface. Essential for platforms like Mintlify and Hashnode that host user-generated content, this component enables visitors to report inappropriate content while maintaining user privacy and preventing false reports. It features a clean dialog interface with comprehensive form validation and clear submission flow.

## Installation

<Installer path="report-abuse" />

## Features

* **Categorized reporting**: Customizable abuse categories for accurate classification
* **Optional URL field**: Users can specify the exact content location
* **Detailed descriptions**: Textarea for comprehensive issue reporting
* **Email validation**: Collects reporter contact for follow-up if needed
* **Privacy notice**: Built-in privacy disclosure for transparency
* **Form validation**: Required fields and input validation
* **Clean dialog UI**: Non-intrusive modal interface
* **Responsive design**: Works seamlessly across all devices
* **Visual indicators**: Icons and colors enhance user understanding

## Usage

### Basic Implementation

```tsx title="report-abuse.tsx"
import { ReportAbuse } from '@/components/blocks/report-abuse'

export default function ContentPage() {
  const abuseTypes = [
    { value: "spam", label: "Spam or Advertising" },
    { value: "inappropriate", label: "Inappropriate Content" },
    { value: "copyright", label: "Copyright Violation" },
    { value: "misinformation", label: "False Information" },
    { value: "harassment", label: "Harassment or Hate Speech" },
    { value: "other", label: "Other" }
  ]

  return (
    <div className="content-footer">
      <ReportAbuse types={abuseTypes} />
    </div>
  )
}
```

### With Server Action

```tsx title="report-abuse.tsx"
import { ReportAbuse } from '@/components/blocks/report-abuse'
import { reportContent } from '@/actions/moderation'

export default function BlogPost({ postId }) {
  const handleReport = async (data) => {
    await reportContent({
      ...data,
      postId,
      reportedAt: new Date()
    })
  }

  const types = [
    { value: "spam", label: "Spam" },
    { value: "inappropriate", label: "Inappropriate" },
    { value: "copyright", label: "Copyright" }
  ]

  return (
    <>
      <article>...</article>
      <ReportAbuse
        types={types}
        onSubmit={handleReport}
      />
    </>
  )
}
```

## Props

<TypeTable
  type={{
  types: {
    description: 'Categories of abuse to report',
    type: 'Array<{value: string, label: string}>',
    required: true,
  },
}}
/>

## Form Fields

### Category Selection

* **Required**: Yes
* **Type**: Select dropdown
* **Purpose**: Classifies the type of abuse for proper routing

### Content URL

* **Required**: No
* **Type**: URL input
* **Purpose**: Links directly to problematic content
* **Validation**: Must be valid URL format

### Description

* **Required**: Yes
* **Type**: Textarea
* **Purpose**: Detailed explanation of the issue
* **Min length**: Recommended 20+ characters

### Reporter Email

* **Required**: Yes
* **Type**: Email input
* **Purpose**: Enables follow-up communication
* **Validation**: Standard email format

## Advanced Examples

### Platform-Specific Categories

```tsx title="report-abuse.tsx"
// Documentation platform
const docAbuseTypes = [
  { value: "outdated", label: "Outdated Information" },
  { value: "broken-code", label: "Non-functional Code Examples" },
  { value: "security-issue", label: "Security Vulnerability" },
  { value: "plagiarism", label: "Plagiarized Content" },
  { value: "spam", label: "Spam or Promotional" }
]

// Blog platform
const blogAbuseTypes = [
  { value: "hate-speech", label: "Hate Speech" },
  { value: "misinformation", label: "Misinformation" },
  { value: "adult-content", label: "Adult Content" },
  { value: "harassment", label: "Harassment" },
  { value: "copyright", label: "Copyright Infringement" }
]

// E-commerce platform
const shopAbuseTypes = [
  { value: "counterfeit", label: "Counterfeit Product" },
  { value: "misleading", label: "Misleading Description" },
  { value: "prohibited", label: "Prohibited Item" },
  { value: "scam", label: "Potential Scam" }
]
```

### Integration with Moderation System

```tsx title="report-abuse.tsx"
import { ReportAbuse } from '@/components/blocks/report-abuse'
import { createModerationTicket } from '@/lib/moderation'
import { sendAlertEmail } from '@/lib/email'

export function ModeratedContent({ content }) {
  const handleAbuseReport = async (report) => {
    // Create moderation ticket
    const ticket = await createModerationTicket({
      contentId: content.id,
      reportType: report.category,
      description: report.description,
      reporterEmail: report.email,
      contentUrl: report.contentUrl || content.url,
      status: 'pending'
    })

    // Send alert for high-priority categories
    const highPriority = ['hate-speech', 'illegal', 'csam']
    if (highPriority.includes(report.category)) {
      await sendAlertEmail({
        to: 'moderation@platform.com',
        subject: `Urgent: ${report.category} reported`,
        ticketId: ticket.id
      })
    }

    return { success: true, ticketId: ticket.id }
  }

  const types = [
    { value: "spam", label: "Spam" },
    { value: "hate-speech", label: "Hate Speech" },
    { value: "illegal", label: "Illegal Content" }
  ]

  return (
    <div>
      {content.body}
      <ReportAbuse
        types={types}
        onSubmit={handleAbuseReport}
      />
    </div>
  )
}
```

### With Rate Limiting

```tsx title="report-abuse.tsx"
import { ReportAbuse } from '@/components/blocks/report-abuse'
import { rateLimit } from '@/lib/rate-limit'

const limiter = rateLimit({
  interval: 60 * 1000, // 1 minute
  uniqueTokenPerInterval: 500,
  maxReports: 3
})

export function RateLimitedReporting() {
  const handleSubmit = async (data) => {
    try {
      await limiter.check(data.email, 3)
      // Process report
    } catch {
      throw new Error('Too many reports. Please try again later.')
    }
  }

  return <ReportAbuse types={types} onSubmit={handleSubmit} />
}
```

## Customization

### Styling the Dialog

```css
/* Custom dialog styles */
[data-report-abuse-dialog] {
  @apply bg-background/95 backdrop-blur-md;
}

/* Custom button styling */
[data-report-abuse-trigger] {
  @apply bg-red-600 hover:bg-red-700;
}
```

### Custom Privacy Notice

```tsx title="report-abuse.tsx"
const CustomReportAbuse = ({ types }) => {
  return (
    <ReportAbuse
      types={types}
      privacyNotice={
        <div className="text-sm text-muted-foreground">
          <p>Your report is confidential and will be reviewed within 24 hours.</p>
          <a href="/privacy" className="underline">
            Learn more about our moderation process
          </a>
        </div>
      }
    />
  )
}
```

## Security Considerations

* **Rate limiting**: Implement rate limits to prevent spam reports
* **Authentication**: Consider requiring login for reporting
* **IP logging**: Log IP addresses for abuse prevention
* **Validation**: Sanitize all inputs before processing
* **CAPTCHA**: Add CAPTCHA for anonymous reporting

## Best Practices

* **Clear categories**: Use specific, non-overlapping categories
* **Quick response**: Acknowledge reports immediately
* **Follow-up**: Send confirmation emails when appropriate
* **Transparency**: Publish moderation guidelines
* **Appeals process**: Provide a way to contest false reports
* **Analytics**: Track report patterns to identify issues

## Moderation Workflow

```tsx title="report-abuse.tsx"
// Example moderation workflow
const ModerationWorkflow = {
  // 1. Receive report
  receiveReport: async (report) => {
    const ticket = await createTicket(report)
    await notifyModerators(ticket)
    return ticket.id
  },

  // 2. Review content
  reviewContent: async (ticketId) => {
    const ticket = await getTicket(ticketId)
    const content = await getContent(ticket.contentId)
    return { ticket, content }
  },

  // 3. Take action
  takeAction: async (ticketId, action) => {
    switch(action) {
      case 'remove':
        await removeContent(ticketId)
        break
      case 'warning':
        await sendWarning(ticketId)
        break
      case 'dismiss':
        await dismissReport(ticketId)
        break
    }
  },

  // 4. Notify reporter
  notifyReporter: async (ticketId, outcome) => {
    const ticket = await getTicket(ticketId)
    await sendEmail(ticket.reporterEmail, outcome)
  }
}
```


---
title: Add Custom Domain
description: Server action for programmatically adding a custom domain to a Vercel project
product: Multi-Tenant
type: reference
summary: Server action for programmatically adding custom domains to Vercel projects.
related:
  - /docs/platform-elements/blocks/custom-domain
  - /docs/platform-elements/blocks/dns-table
  - /docs/multi-tenant-platforms/configuring-domains
---

# Add Custom Domain



<Preview example="add-custom-domain" source="actions/add-custom-domain" />

## Overview

The Add Custom Domain action is a server-side utility that allows platforms to programmatically add custom domains to Vercel projects and check their configuration status. This enables platforms to manage domain verification and DNS configuration on behalf of their users, providing a seamless experience for connecting custom domains to deployments.

## Installation

<Installer path="add-custom-domain" />


---
title: Deploy Files
description: Server action for programmatically deploying files to Vercel, enabling platforms to create deployments on behalf of users
product: Multi-Project
type: reference
summary: Server action for programmatically deploying files to Vercel on behalf of platform users.
related:
  - /docs/platform-elements/blocks/claim-deployment
  - /docs/platform-elements/blocks/deploy-popover
  - /docs/multi-project-platforms/quickstart
---

# Deploy Files



<Preview example="deploy-files" source="actions/deploy-files" />

## Overview

The Deploy Files action is a server-side utility that allows platforms to programmatically deploy files to Vercel. This is the core functionality behind platforms like Mintlify and Hashnode that create Vercel deployments for their users without requiring direct Vercel account access.

## Installation

<Installer path="deploy-files" />

## Features

* **Programmatic deployment**: Deploy files directly to Vercel using the SDK
* **Custom domain support**: Automatically configure custom domains for deployments
* **Project configuration**: Pass custom build settings and environment variables
* **SSO protection handling**: Optionally make preview deployments public
* **Unique deployment naming**: Automatic UUID generation for deployment identification

## Usage

```ts title="deploy-files.ts"
import { deployFiles } from "@/actions/deploy-files"
import type { InlinedFile } from "@vercel/sdk/models/createdeploymentop"

// Example: Deploy a simple HTML site
const files: InlinedFile[] = [
  {
    file: "index.html",
    data: "<html><body><h1>Hello from my platform!</h1></body></html>"
  },
  {
    file: "package.json",
    data: JSON.stringify({
      name: "my-deployment",
      version: "1.0.0"
    })
  }
]

await deployFiles(files, {
  domain: "customer-site.com",
  deploymentName: "customer-deployment-1",
  projectId: "existing-project-id", // Optional: use existing project
  config: {
    framework: "nextjs",
    buildCommand: "npm run build",
    outputDirectory: ".next"
  }
})
```

## Parameters

### `files`

Array of files to deploy. Can be either:

* `InlinedFile`: File content provided directly as a string
* `UploadedFile`: File content uploaded separately and referenced by SHA

### `args`

Configuration object with the following options:

<TypeTable
  type={{
  projectId: {
    description: 'Optional existing Vercel project ID. If not provided, creates a new project',
    type: 'string',
    optional: true,
  },
  deploymentName: {
    description: 'Custom deployment name. Defaults to a UUID',
    type: 'string',
    optional: true,
  },
  config: {
    description: 'Build configuration including framework, commands, and environment',
    type: 'ProjectSettings',
    optional: true,
  },
  domain: {
    description: 'Custom domain to add to the project after deployment',
    type: 'string',
    optional: true,
  },
}}
/>

## Advanced Example

```ts title="deploy-files.ts"
import { deployFiles } from "@/actions/deploy-files"
import type { InlinedFile, ProjectSettings } from "@vercel/sdk/models/createdeploymentop"

// Deploy a Next.js application with custom configuration
const files: InlinedFile[] = [
  // Your application files here
]

const config: ProjectSettings = {
  framework: "nextjs",
  buildCommand: "npm run build",
  outputDirectory: ".next",
  installCommand: "npm install",
  devCommand: "npm run dev",
  env: {
    API_KEY: "your-api-key",
    DATABASE_URL: "your-database-url"
  },
  buildEnv: {
    NODE_ENV: "production"
  }
}

const deployment = await deployFiles(files, {
  deploymentName: `deployment-${Date.now()}`,
  config,
  domain: "app.customer-domain.com"
})
```

## Integration with Claim Deployment

After creating a deployment with this action, you typically show the Claim Deployment component to allow users to take ownership:

```tsx title="deploy-files.tsx"
// 1. Deploy files server-side
const deployment = await deployFiles(files, { domain })

// 2. Show claim interface client-side
<ClaimDeployment
  url={deployment.url}
  onClaimClick={handleTransferOwnership}
/>
```

## Security Considerations

* This action requires Vercel API credentials with deployment permissions
* Always validate and sanitize file contents before deployment
* Consider implementing rate limiting to prevent abuse
* Store API credentials securely using environment variables
