---
title: "Vercel Deployment"
description: "Deploy Ring on Vercel with RING_DEPLOY_TARGET, serverless limits, and vercel.json as the ProcessConductor cron schedule catalog (19 pipelines, CRON_SECRET)."
locale: "en"
---
# Vercel Deployment

> **Info**
> Use the **Founder** / **Developer** tabs in the docs sidebar to filter this page for your role. `audience` frontmatter controls content filtering within the page. Sidebar article visibility is controlled separately by `lib/docs/audience-curated-docs.ts`.

Vercel is a **first-class Ring deploy target** — equal in the backend matrix to Firebase Hosting / Cloud Run and k3s. The app runs as **Next.js serverless route handlers** (no custom `server.ts`). Platform Cron reads root **`vercel.json`** and invokes each ProcessConductor path on a UTC schedule.

| Concern | Vercel truth |
|---------|----------------|
| Deploy flags | `RING_DEPLOY_TARGET=vercel`, `NEXT_PUBLIC_RING_DEPLOY_TARGET=vercel` |
| Runtime | Serverless only — no custom Node `server.ts` |
| Tunnel | SSE + long-polling (no native WSS) |
| Cron catalog | `vercel.json` → `crons[]` — must list every ProcessConductor `cronPath` |
| Auth gate | `Authorization: Bearer $CRON_SECRET` on `/api/cron/*` |

### For founders

## Why Vercel Cron matters for clone health

ProcessConductor runs **19** background pipelines (email poll, cleanups, settlements, membership expiry, peer-game session expiry, Forgejo lab GC, and more). On Vercel, **Platform Cron** hits the same HTTP routes your Admin → **Background Processes** panel records. If `CRON_SECRET` is missing or `crons[]` is incomplete, those jobs silently never run.

### Operator checklist

1. Import the Ring repo in the Vercel dashboard and deploy the main branch.
2. Copy required secrets from [Environment variables](/docs/deployment/environment.md) — include a long random **`CRON_SECRET`**.
3. Set `RING_DEPLOY_TARGET=vercel` and `NEXT_PUBLIC_RING_DEPLOY_TARGET=vercel`.
4. Confirm root `vercel.json` `crons[]` lists **every** ProcessConductor path (schedule SSOT for all backends).
5. After deploy, open Admin → Background Processes and verify runs appear.
6. Expect Tunnel over **SSE + long-polling** only — native WebSocket is a self-hosted / custom-server concern.

> **Tip**
> Hosting on **Firebase** instead? Same cron URLs and schedules — trigger with **Google Cloud Scheduler**, using `vercel.json` as the catalog. See [Firebase Integration](/docs/backend/firebase.md).

### For developers

## Deploy on Vercel

Push the Ring repo to GitHub (or connect another Git provider Vercel supports).

Import the project in the Vercel dashboard. Framework preset: **Next.js**. Root directory: the Ring app root (where `package.json` and `vercel.json` live).

Set environment variables (see [Environment](/docs/deployment/environment.md)), including:

```bash
RING_DEPLOY_TARGET=vercel
NEXT_PUBLIC_RING_DEPLOY_TARGET=vercel
CRON_SECRET=generate-a-long-random-string
```

Plus your `DB_BACKEND_MODE` and backend-specific secrets (`AUTH_FIREBASE_*`, Postgres URL, payment keys, etc.).

Deploy the production branch. Confirm Platform Cron shows the paths from `vercel.json`.

> **Warning**
> Vercel runs **serverless route handlers only** — there is **no** custom `server.ts`. Tunnel uses **SSE + long-polling**; native WSS is unavailable on this target (`server.ts` logs SSE-only when `RING_DEPLOY_TARGET=vercel`).

## ProcessConductor + vercel.json

| Layer | Path |
|-------|------|
| Pipeline SSOT | `lib/processes/registry.ts` — `PIPELINE_IDS` + `PIPELINE_REGISTRY` (**19**) |
| HTTP routes | `app/api/cron//route.ts` — `GET`/`POST` |
| Schedule catalog | `ring-platform.org/vercel.json` → `crons[]` (UTC) |
| Ledger | `ProcessConductor.recordRun` |
| Admin copy | `locales/*/modules/admin.json` → `processes.pipelines.*` |
| Ops write-up | `AI-RING/scripts/PIPELINES.md` § ProcessConductor cron pipelines |

**Parity rule:** `vercel.json` **must list every** ProcessConductor `cronPath`. Firebase-hosted rings reuse this file as the schedule catalog for Cloud Scheduler. k3s uses per-job YAML under `k8s/` (e.g. `cronjob-email-processor.yaml`) curling an in-cluster URL — not a single inventory file.

### Full schedule catalog (UTC)

| Pipeline ID | cronPath | schedule |
|-------------|----------|----------|
| email-processor | `/api/cron/email-processor` | `*/5 * * * *` |
| email-analytics | `/api/cron/email-analytics` | `0 6 * * *` |
| refcodes-mint | `/api/cron/refcodes-mint` | `*/30 * * * *` |
| cleanup-reservations | `/api/cron/cleanup-reservations` | `*/15 * * * *` |
| cleanup-usernames | `/api/cron/cleanup-usernames` | `*/5 * * * *` |
| cleanup-news-deleted | `/api/cron/cleanup-news-deleted` | `0 3 * * 0` |
| cleanup-email-tokens | `/api/cron/cleanup-email-tokens` | `0 */6 * * *` |
| train | `/api/cron/train` | `0 2 * * *` |
| settlement-payout | `/api/cron/settlement-payout` | `0 1 * * *` |
| inventory-drift | `/api/cron/inventory-drift` | `0 */6 * * *` |
| subscription-expiry-check | `/api/cron/subscription-expiry` | `0 0 * * *` |
| credit-balance-monthly | `/api/cron/credit-balance-monthly` | `0 * * * *` |
| subscription-payment | `/api/cron/subscription-payment` | `0 2 * * *` |
| solana-batch-payment | `/api/cron/solana-batch-payment` | `0 3 * * *` |
| nft-gate-expiry | `/api/cron/nft-gate-expiry` | `0 4 * * *` |
| close-expired-polls | `/api/cron/close-expired-polls` | `*/5 * * * *` |
| peer-game-session-expiry | `/api/cron/peer-game-session-expiry` | `*/5 * * * *` |
| forgejo-robot-gc | `/api/cron/forgejo-robot-gc` | `0 4 * * 0` |
| forgejo-token-rotate | `/api/cron/forgejo-token-rotate` | `0 5 1 * *` |

Example `vercel.json` shape (every ProcessConductor path must appear):

```json
{
  "crons": [
    { "path": "/api/cron/email-processor", "schedule": "*/5 * * * *" },
    { "path": "/api/cron/forgejo-robot-gc", "schedule": "0 4 * * 0" }
  ]
}
```

### Auth smoke test

```bash
curl -sS -H "Authorization: Bearer $CRON_SECRET" \
  "https:///api/cron/email-processor"
```

Fail-closed: missing/wrong bearer → `401`.

### Adding a pipeline

1. Register in `lib/processes/registry.ts` (`PIPELINE_IDS` + `PIPELINE_REGISTRY` with `cronPath`).
2. Add `app/api/cron//route.ts` (Bearer gate + `ProcessConductor.recordRun`).
3. Add Admin labels under `locales/*/modules/admin.json` → `processes.pipelines.*`.
4. **Append** `{ path, schedule }` to `vercel.json` `crons[]`.
5. Optionally add `k8s/cronjob-.yaml` for cluster deploys.

Ops notes: `forgejo-robot-gc` supports `?dryRun=1`; `forgejo-token-rotate` uses `FORGEJO_TOKEN_ROTATE_MAX_AGE_DAYS` / `FORGEJO_TOKEN_ROTATE_LIMIT`.

## Related documentation

  
- [backend/firebase](/docs/backend/firebase.md) — Same-workflow: Firebase-hosted rings use Cloud Scheduler + this vercel.json catalog (firebase.json is not cron).

  
- [deployment/environment](/docs/deployment/environment.md) — Depends-on: CRON_SECRET, RING_DEPLOY_TARGET, and backend secrets.

  
- [features/admin](/docs/features/admin.md) — Next-step: Admin → Background Processes for recorded pipeline runs.

  
- [features/email-ai-crm](/docs/features/email-ai-crm.md) — Same-workflow: email-processor and email-analytics cron pipelines.

  
- [deployment/self-hosted](/docs/deployment/self-hosted.md) — See-also: k3s path with native WSS and per-job CronJob YAML.

  
- [architecture/backend-modes-and-databases](/docs/architecture/backend-modes-and-databases.md) — Prerequisite: pick DB_BACKEND_MODE before choosing a host.
