# DevStack first-party hosting (`*.devstack.bid`)

Status: **product path** (this PR). Deploy/Retry default to DevStack hosting.
User Cloudflare / Vercel remain **optional** targets for people who want their own account.

## Hard rule: no per-project GitHub activity on the hosting path

DevStack hosting **must never** create GitHub repositories, forks, pushes, branches, or
webhooks for a user project. Creating repos under load previously caused GitHub spam bans.

| Source of truth | Where |
|---|---|
| Project files / templates | D1 `project_files` + R2 `sources/<projectId>/latest.tar` |
| Build artifacts | R2 `artifacts/<jobId>.tar` + `sites/<deploymentId>/…` |
| SSR apps | Workers for Platforms **dispatch namespace** (one namespace, many scripts) |
| Static sites | R2 served by `devstack-sites` |

Import of an **existing** user GitHub repo (read-only clone with the user's OAuth) remains
an optional path. New projects / templates / chat `github_create_repo` / MCP `create_project`
create **hosted** projects only (`createHostedProject`) — zero GitHub write traffic.

### GitHub-activity audit (create / fork / push / branch / webhook)

Paths that **created** repos or wrote to GitHub, and how they relate to hosting:

| File:line (approx) | Activity | Hosting default |
|---|---|---|
| `src/lib/auth/github-api.ts:234` `createRepo` | Create repository | API kept for tests only; **not** called from hosting |
| `src/lib/connections/github-service.ts:48` `createProjectRepo` | Was: create repo + seed | **Disabled** — throws `forbidden` (403), no GitHub call |
| `src/lib/connections/github-service.ts:68` `createHostedProject` | D1 + R2 sources | **Default** create / template path |
| `src/app/api/projects/route.ts` POST | Was: `createProjectRepo` | Now: `createHostedProject` |
| `src/lib/chat/customer-tools.ts:91` `github_create_repo` | Was: create repo | Now: `createHostedProject` |
| `src/lib/mcp/platform-tools.ts` `create_project` | Was: create repo | Now: `createHostedProject` (no GitHub token) |
| `src/lib/connections/github-service.ts:110`/`127` `proposeChanges` / `commitToBranch` | Branch + commit + PR | **Opt-in** on existing imported repos only (prepare / chat); never for hosted projects |
| `src/lib/aisite/repo-turn.ts` `syncAiBranch` + `ai-turn` `commitFiles` | Branch + push | Requires `repo_full_name`; hosted projects have none |
| `src/lib/chat/customer-tools.ts` `github_create_branch` / commit tools | Branch / push | Existing-repo tools only |
| `src/lib/github/autodeploy.ts` + webhook routes | Push → build | Only for imported repos with webhooks; not created by hosting |
| `src/app/api/projects/import/route.ts` | Import existing repo | Read-only register + optional container clone; **does not create** a repo |
| `src/lib/hosting/deploy.ts` Deploy / Retry | Hosting entry | Archive build (R2) or clone **existing** repo; never createRepo |
| Fork APIs | — | **None** in tree |

## Goal

A signed-in user can create a hosted project (template / chat / MCP), upload sources, or
optionally import an existing GitHub repo, and get a live URL on DevStack —
`<slug>.devstack.bid` (production) and `<slug>-<shortid>.devstack.bid` (preview) —
**without** connecting Cloudflare at `/settings/connections`.

## Architecture

```
                 ┌──────────────────────────────────────────────┐
  UI / chat      │  POST /api/projects/:id/deploy                │
  Retry deploy ──┤    target=devstack (default) | cloudflare    │
  Redeploy       └──────────────┬───────────────────────────────┘
                                │
              ┌─────────────────┴──────────────────┐
              ▼                                    ▼
   target=devstack                          target=cloudflare
   (first-party)                            (optional)
              │                                    │
   ┌──────────┴──────────┐                  startDeploy()
   │ Hosted R2 sources?  │                  → customer CF token
   │  → archive container│                  → *.workers.dev
   │ Imported GitHub?    │
   │  → clone (OAuth)    │
   │ Static zip ready?   │
   │  → R2 publish       │
   └──────────┬──────────┘
              ▼
   R2 `devstack-sites`
     sources/<projectId>/latest.tar
     sites/<deploymentId>/…
     hosts/<label>.json   { projectId, deploymentId, worker? }
              │
              ▼
   Worker `devstack-sites`  (route *.devstack.bid)
     static → R2 object
     server → WfP dispatcher → user Worker script in SITES_DISPATCH_NAMESPACE
              (legacy workers.dev proxy only if allowLegacyWorkers — not default)
```

### Build pipeline

1. `startContainerBuild` creates `build_jobs` (`executor=cf-container`) + `deployments`
   (`target=devstack-sites`) and enqueues `BUILD_QUEUE`.
2. **Archive mode** (hosted): executor reads `sources/<id>/latest.tar` from R2, POSTs it to
   container `/source`, then `/build` with `source: "archive"` — **no git, no token**.
3. **GitHub mode** (imported existing repo only): container clones with the user's OAuth
   token (http.extraheader; scrubbed before install/build). Public repos may omit the token.
4. Package manager from lockfile; install env forces public npm registry.
5. Static output → tar → R2 sites; OpenNext → `deployServerWorker` into the WfP dispatch
   namespace (`SITES_DISPATCH_NAMESPACE` required) + `publishServer`.
6. Logs stream into `build_events` + R2 (secrets redacted).

### Zip / static upload

`publishUploadedSite` for ready static files. Profiles with `path=cf-build` go through
archive container via `startHostingDeploy` (sources uploaded to R2 first).

## Subdomains, rollback, records

| Item | Behaviour |
|---|---|
| Production host | `<slug>.devstack.bid` — first project to claim a host owns it |
| Preview host | `<slug>-<shortid>.devstack.bid` — immutable per deployment |
| Rollback | `POST …/rollback` points production at a previous **live** deployment |
| Reserved labels | `RESERVED` / `RESERVED_SUBSTRINGS` in `hostnames.ts` |
| Abuse badge | HTML footer → report form → `site_reports` |

## Abuse safeguards

- Caps: concurrent container builds, per-user daily/hourly builds, deploy quota.
- Size: `MAX_SITE_FILES` / `MAX_SITE_BYTES`; zip/file limits in `upload.ts`.
- Secrets: never logged; sanitizeLog; zip secret scanners; no secrets in chat.
- Egress allowlist on the build container; no platform D1/R2 bindings in customer Workers.
- **No GitHub create/push from hosting** (this document's hard rule).
- Takedown: site report + host ownership; preview Worker cleanup cron.
- Blocklist: reserved DNS labels; platform denylist for server Worker names.

## What this PR does **not** do (needs owner GO)

Production GO checklist (owner-approved 2026-11-01):

1. ~~Create WfP dispatch namespace `devstack-sites`~~ (done; do **not** reuse `one-origin`)
2. ~~Bind `DISPATCHER` on serving Worker `devstack-sites`~~ (done)
3. ~~Set `SITES_DISPATCH_NAMESPACE=devstack-sites` on `nvx-stack-builder`~~ (in wrangler vars; ships with merge)
4. ~~Verify `SITES_CF_API_TOKEN` can upload scripts into the namespace~~ (probe upload+delete OK)
5. Merge PR to `main` / prod deploy of `nvx-stack-builder` (this GO)
6. Remote D1 migrations — **none required** for this change
7. New R2 buckets — **none** (reuse `devstack-sites` `sources/` prefix)
8. DNS wildcard / other secret changes — **no further DNS** in this GO

## API surface

| Method | Path | Default |
|---|---|---|
| POST | `/api/projects` | Create **hosted** project (no GitHub repo) |
| POST | `/api/projects/:id/deploy` | `target=devstack` → hosting; `target=cloudflare` optional |
| POST | `/api/projects/:id/builds` | container build (archive or imported repo) |
| POST | `/api/projects/:id/rollback` | promote a prior live deployment to production |
| POST | `/api/deployments/:id/rollback` | same, keyed by deployment id |
