Sites
Overview
Section titled “Overview”Sites hosts your web applications. It is a composer over Functions and Storage—not a separate compute primitive. Static assets live in Storage; SSR and API routes run as Functions behind an origin router.
Sites speaks the Vercel Build Output API-shaped bundle format. Framework adapters for static, Hono, and Next.js compile your project into a known target. Deploy and promote are separate: every deployment gets a preview URL; promotion swaps production traffic atomically.
Features include incremental static regeneration (ISR), on-demand revalidation, custom domains with TLS, and per-site access policies (password-protected previews, IP allowlists).
Key features
Section titled “Key features”- Framework adapters — Static, Hono, and Next.js App Router at v0; more in v0.2.
- Preview deployments — Every build is live at
{deployment_id}.preview.{site}.reactor.app. - Atomic promote — Single-row swap of
current_deployment_id; in-flight requests finish gracefully. - ISR and stale-while-revalidate — Serve cached HTML; background revalidation via Jobs.
- Custom domains — DNS or HTTP verification; ACME TLS on self-hosted (
O2). - Route table — Static files, function dispatch, redirects, and prerender routes from manifest.
- Unified logs — Router and function logs merged in one SSE stream.
Quickstart
Section titled “Quickstart”Create a site, build a Hono app, deploy, promote, and visit the live URL.
src/index.ts (Hono):
import { Hono } from 'hono';
const app = new Hono();app.get('/', (c) => c.json({ hello: 'Reactor Sites' }));app.get('/api/health', (c) => c.json({ ok: true }));
export default app;reactor sites create my-app --framework honoreactor sites build ./my-hono-app --out .reactor-bundlereactor sites deploy my-app --bundle .reactor-bundlereactor sites promote my-appreactor sites open my-appimport { createClient } from '@reactor/client';
const reactor = createClient({ url: process.env.REACTOR_URL! });await reactor.auth.signInWithPassword({ email, password });reactor.auth.setOrg('acme');
await reactor.sites.create({ name: 'my-app', framework: 'hono' });
const bundle = await reactor.sites.build({ projectDir: './my-hono-app' });const deployment = await reactor.sites.deploy('my-app', { bundle });await reactor.sites.promote('my-app', { deploymentId: deployment.id });
console.log(`Live at https://my-app.acme.reactor.app`);# Create sitecurl -s -X POST "$REACTOR_URL/sites/v1/_admin/sites" \ -H "Authorization: Bearer $REACTOR_TOKEN" \ -H "X-Reactor-Org: acme" \ -H "Content-Type: application/json" \ -d '{"name":"my-app","framework":"hono"}'
# Deploy (multipart upload — use CLI for bundle packaging)# After deploy + promote, visit:curl -s -I "https://my-app.acme.reactor.app/"How-to guides
Section titled “How-to guides”Preview a deployment before promoting
Section titled “Preview a deployment before promoting”Every deployment is automatically available at a preview subdomain—no promotion required.
https://{deployment_id}.preview.my-app.acme.reactor.appreactor sites deploy my-app --bundle .reactor-bundle# CLI prints preview URL from deployment IDreactor sites deployments list my-appPreview deployments send X-Robots-Tag: noindex and disable ISR caching so you always see fresh output.
Add a custom domain
Section titled “Add a custom domain”reactor sites domains add my-app app.example.com# Follow DNS TXT verification instructionsreactor sites domains verify my-app app.example.comconst domain = await reactor.sites.domains.create('my-app', { host: 'app.example.com',});console.log(domain.verificationInstructions);await reactor.sites.domains.verify('my-app', 'app.example.com');curl -s -X POST "$REACTOR_URL/sites/v1/_admin/sites/my-app/domains" \ -H "Authorization: Bearer $REACTOR_TOKEN" \ -H "X-Reactor-Org: acme" \ -H "Content-Type: application/json" \ -d '{"host":"app.example.com"}'DNS verification record:
_reactor-verify.app.example.com TXT "reactor-site-verification={token}"On-demand ISR revalidation
Section titled “On-demand ISR revalidation”Purge cached pages after content changes.
reactor sites revalidate my-app --paths /blog/post-1,/blog/post-2reactor sites revalidate my-app --tags blogawait reactor.sites.revalidate('my-app', { paths: ['/blog/post-1', '/blog/post-2'],});// Or by cache tag:await reactor.sites.revalidate('my-app', { tags: ['blog'] });curl -s -X POST "$REACTOR_URL/sites/v1/_admin/sites/my-app/revalidate" \ -H "Authorization: Bearer $REACTOR_TOKEN" \ -H "X-Reactor-Org: acme" \ -H "Content-Type: application/json" \ -d '{"paths":["/blog/post-1"],"tags":["blog"]}'Configuration
Section titled “Configuration”[sites]workdir = "./.reactor/sites"static_max_files = 50000static_max_bytes = 536870912 # 512 MiB per deploymentisr_default_ttl_secs = 3600preview_subdomain = "preview"revalidation_secret = "internal-secret-for-functions"
[sites.functions]url = "http://localhost:8004"api_key = "internal-functions-api-key"
[sites.storage]url = "http://localhost:8003"api_key = "internal-storage-api-key"
[sites.jobs]url = "http://localhost:8005"api_key = "internal-jobs-api-key"
# Self-hosted TLS (O2)[sites.acme]email = "ops@example.com"Optional analytics snippet injection in site manifest:
{ "analytics": { "enabled": true, "project_key": "rapk_...", "auto_pageview": true, "auto_errors": true }}Limits and quotas
Section titled “Limits and quotas”| Limit | Default | Notes |
|---|---|---|
| Static files per deployment | 50,000 | |
| Total static size per deployment | 512 MiB | |
| Function bundle size | 50 MiB each | Same as Functions |
| ISR default revalidate | 1 hour | Per-route override in manifest |
| Invocation log sample rate | 1% | Router traffic; use metrics for billing |
| Preview ISR | Disabled | Always fresh on preview URLs |
| Edge router | v0.2 | Origin-based routing at v0 |
Route kinds in manifest:
| Kind | Behavior |
|---|---|
static | Serve from Storage with cache headers |
function | Dispatch to internal SSR/API function |
redirect | 301/302/307/308 with Location |
prerender | ISR cached HTML with optional fallback function |
Response headers:
| Header | Meaning |
|---|---|
X-Reactor-Site | {name}@{version} |
X-Reactor-Cache | HIT, MISS, STALE, or BYPASS |
X-Reactor-Duration-Ms | Total serve latency |
API and SDK links
Section titled “API and SDK links”- Serve plane:
/*(Host header resolves site) - Admin base path:
/sites/v1/_admin/ - OpenAPI reference: Sites API
- JavaScript SDK:
reactor.sites - CLI:
reactor sites
| Method | Path | Description |
|---|---|---|
POST | /sites/v1/_admin/sites | Create site |
POST | /sites/v1/_admin/sites/{name}/deployments | Upload bundle |
POST | /sites/v1/_admin/sites/{name}/promote | Swap production traffic |
POST | /sites/v1/_admin/sites/{name}/revalidate | Purge ISR cache |
POST | /sites/v1/_admin/sites/{name}/domains | Add custom domain |
GET | /sites/v1/_admin/sites/{name}/logs | Unified logs (SSE) |
Troubleshooting
Section titled “Troubleshooting”404 route_unmatched
Section titled “404 route_unmatched”No route in the deployment’s route table matched the path and method. Check the manifest routes array—first match wins, ordered by priority.
502 function_dispatch_failed
Section titled “502 function_dispatch_failed”The internal SSR function failed or timed out. Check function logs for _site-{name}-ssr. Verify the function deployment reached ready status before promote.
503 deployment_not_ready
Section titled “503 deployment_not_ready”Deployment is still uploading assets or waiting for functions. Poll deployment status before promoting.
reactor sites deployments get my-app dep_01HZ...reactor sites logs my-app --followCustom domain stuck in pending
Section titled “Custom domain stuck in pending”Run verification again after adding the DNS TXT or HTTP challenge file:
reactor sites domains verify my-app app.example.comISR serves stale content
Section titled “ISR serves stale content”Check X-Reactor-Cache: STALE—background revalidation may be queued. Force purge with on-demand revalidation. Ensure the ISR revalidation function deployed successfully for Next.js sites.