Skip to main content

Deploy Safely

Every destructive operation on a SpiderIQ project runs through a two-step preview → confirm flow. No more accidental deletions. No more "I thought I was on staging." No more wrong-tenant publishes.

The flow

  1. Preview — call the endpoint with ?dry_run=true. Nothing mutates. You get back a description of what the change would do, plus a one-time confirm_token (valid 7 days).
  2. Confirm — call the same endpoint with ?confirm_token=cft_TOKEN where TOKEN is from step 1. The token consumes (single-use) and the mutation runs.

The MCP tools default to preview mode. The CLI prompts interactively by default. Agents have to explicitly opt into mutating.

Which operations are gated?

OperationEndpoint
Delete pageDELETE /dashboard/projects/{pid}/content/pages/{page_id}
Publish pagePOST /dashboard/projects/{pid}/content/pages/{page_id}/publish
Unpublish pagePOST /dashboard/projects/{pid}/content/pages/{page_id}/unpublish
Update settingsPATCH /dashboard/projects/{pid}/content/settings
Apply themePOST /dashboard/projects/{pid}/templates/apply-theme
Delete componentDELETE /dashboard/projects/{pid}/content/components/{id}
Publish componentPOST /dashboard/projects/{pid}/content/components/{id}/publish
Archive componentPOST /dashboard/projects/{pid}/content/components/{id}/archive
Deploy site (preview)POST /dashboard/projects/{pid}/content/deploy/preview
Deploy site (production)POST /dashboard/projects/{pid}/content/deploy/production

Deploying a site

Deploy is the canonical two-step flow:

Step 1 — Preview

curl -X POST \
-H "Authorization: Bearer $SPIDERIQ_PAT" \
"https://spideriq.ai/api/v1/dashboard/projects/$PID/content/deploy/preview"

Response:

{
"project": "SMS Chemicals",
"preview_url": "https://preview-ov5f-c3b75c9b.sites.spideriq.ai",
"diff_summary": "3 pages modified, 1 new blog post, theme unchanged",
"confirm_token": "cft_2m9xkl3p7r4v5w8q6a1b2c3d4e5f6g7h",
"expires_at": "2026-04-21T12:00:00Z",
"snapshot_hash": "8f2c91a0b3d4e5f6..."
}

Open preview_url in a browser. You're looking at the exact snapshot that would go live — same templates, same content, same settings — served from a staging Cloudflare KV. No DNS flip yet.

Step 2 — Confirm

Review, then:

curl -X POST \
-H "Authorization: Bearer $SPIDERIQ_PAT" \
"https://spideriq.ai/api/v1/dashboard/projects/$PID/content/deploy/production?confirm_token=cft_2m9xkl3p7r4v5w8q6a1b2c3d4e5f6g7h"

Response:

{
"project": "SMS Chemicals",
"status": "live",
"version_id": 48,
"deployed_at": "2026-04-14T12:34:56Z"
}

The preview token consumes. The real deploy runs. Your site goes live at the mapped custom domain.

CLI flow

The CLI wraps the dance for you:

# Interactive (default): preview → table diff → [y/N] → production
spideriq content deploy

# Non-interactive (CI/agent):
spideriq content deploy --json
# emits the preview envelope; then:
spideriq content deploy --confirm cft_2m9xkl3p…

# Skip preview entirely (legacy mode; audit event emitted):
spideriq content deploy --yolo

For other destructive commands:

spideriq content components delete abc-123 --dry-run      # returns confirm_token
spideriq content components delete abc-123 --confirm cft_…
spideriq templates apply-theme default --dry-run

MCP flow

Destructive MCP tools default to dry_run=true when neither flag is passed. Agents get a preview + token on the first call; they must call again with confirm_token to actually mutate.

content_publish_page({ page_id: "abc-123" })
# → { dry_run: true, preview: {...}, confirm_token: "cft_..." }

content_publish_page({ page_id: "abc-123", confirm_token: "cft_..." })
# → the real publish result

For the deploy flow specifically, use the split tools:

content_deploy_site_preview()
# → { preview_url, confirm_token, preview: {...} }

content_deploy_site_production({ confirm_token: "cft_..." })
# → the real deploy

Error responses

StatusReasonWhen
403TokenInvalidToken string doesn't exist in DB
403TokenClientMismatchToken belongs to a different project
403TokenActionMismatchToken was issued for a different action
403TokenResourceMismatchToken was issued for a different resource
409TokenConsumedToken was already used once (single-use)
410TokenExpiredPast expires_at (default 7 days)

On 410, just call the preview endpoint again to get a fresh token. The snapshot will reflect current state.

What the preview captures

The preview envelope's preview field tells you exactly what will change — no surprises:

Page publish:

{
"action": "publish_page",
"page_id": "abc-123",
"slug": "pricing",
"title": "Pricing",
"current_status": "draft",
"will_become": "published"
}

Settings update:

{
"action": "update_settings",
"fields_changing": ["site_name", "primary_color"],
"current": { "site_name": "SMS", "primary_color": "#FF0000" },
"proposed": { "site_name": "SMS Chemicals", "primary_color": "#0088FF" }
}

Apply theme:

{
"action": "apply_theme",
"theme_name": "default",
"file_count": 29,
"files": ["layout/theme.liquid", "sections/header.liquid", ...],
"warning": "Applying a theme overwrites any per-file template edits for these paths."
}

A snapshot_hash (SHA-256) is stored on the token. Future tightening will reject a confirm if the underlying DB state has drifted since the preview — for now it's informational.

CI / automation

For automation where interactive confirmation isn't possible:

  • Preferred: two-step with explicit confirm_token — you still get a diff in the logs, and the token proves the preview ran.
  • Escape hatch: --yolo (CLI) or dry_run: false without confirm_token (MCP/API). Works, but every call writes an audit event to content_tenant_audit flagged as bypassed. Reserve this for trusted CI jobs.

Why this exists

Stage 0 of Phase 11+12 was triggered by an incident: an agent running in one Antigravity window picked up a token issued for a different client and overwrote a production site with the wrong content. The preview → confirm flow (Lock 4) is the last line of defense — even if Locks 1, 2, 3, and 5 somehow all failed simultaneously, the operator still has to eyeball the preview URL and type "y" (or copy the cft_… token) before anything mutates.

See also