Milestone 0 — Architecture Audit (DevStack Project Creation & Deployment Platform)
On this page
Base: release/2026-10-17 @ 33cf01e (v0.8.0-rc.3), branch feat/new-project-platform. Audit date 2026-10-22.
Method: read the call paths (route → lib → D1 SQL), not file presence; ran npm ci, typecheck, lint, test (results at the bottom).
No push, deploy, remote migration, DNS or production change was made.
1. Runtime & stack (verified)#
| Item | Finding |
|---|---|
| Runtime | Node 22 (CI setup-node 22), Next.js 16.4.0 App Router, React 19, TypeScript 5.x, Vitest |
| Hosting | Cloudflare Workers via @opennextjs/cloudflare (open-next.config.ts, scripts/patch-opennext.mjs postinstall patch + check-opennext-patch.mjs) |
| wrangler.jsonc | Worker nvx-stack-builder, custom domains devstack.bid/www, preview_urls: false, workers_dev: true |
| Bindings | DB (D1 nvx-db, 12 migrations), CHAT_STREAMS (R2 nvx-chat-streams), CREDENTIALS_KEY (Secrets Store), AI, ASSETS, WORKER_SELF_REFERENCE, 5 rate limiters. No Queues, no Durable Objects, no Containers, no R2 bucket for build artifacts. |
| CI | .github/workflows/ci.yml: lint, typecheck, test, docs:check, next build on push to main + PRs. No deploy, no secrets. jekyll-gh-pages.yml unrelated. |
2. What already exists (read from code)#
- Auth/session —
src/lib/auth/session.ts: revocable D1auth_sessions, signed cookie, fails closed without D1. Providers: Cursor, Google, GitHub OAuth, password (+ Turnstile). - Account isolation —
src/lib/connections/route-kit.ts#gate: same-origin on writes, per-route burst limit, signed-in required. All project/deployment SQL isWHERE ... AND user_id = ?(getProject,getDeployment,listDeployments). Tested intests/connections.test.ts,deploy.test.ts. - Secrets at rest —
src/lib/auth/token-box.tsAES-GCM v2 with HKDF bound tokind:user_idandCREDENTIALS_KEYfrom Secrets Store;user_credentials(kinds:cloudflare,openai),user_env(migration 0012),github_credentials. - GitHub integration — OAuth App, not a GitHub App. Classic scopes (
repo,user:email;?scope=public= public_repo).src/lib/auth/github-api.ts:listWritableRepos(single page,per_page=50, owner+collaborator, no org picker, no search),readTree,readBlob,createRepo,commitFiles,openPullRequest. Retry with backoff (fetchRetry). No webhooks, no installation tokens. - Projects — D1
projects(0010):id, user_id, name, template_id, repo_full_name, default_branch, created_at.POST /api/projectscreates a new repo in the customer's GitHub (createProjectRepo); there is no "import existing repo" API.POST /api/projects/uploadcreates an upload-backed project; files inproject_files(0011, BLOB ≤1.9 MB/row). - Deployments — D1
deployments(0010) with statusqueued|uploading|live|failed,error_logsanitized.src/lib/deploy/service.ts#startDeploy: quota, one-in-flight (15 min), credential resolution, profile resolution, backgroundwaitUntilupload (≤30 s).GET /api/deployments/:idpolled by UI. - Provider abstraction —
provider.tsinterface (supports,deploy),providers.tsregistry +pickProvider. Cloudflare provider = direct upload only (static assets or plain ESM Worker; no build). Vercel = honest stub (supports: () => false). - Deploy profiles —
templates.tsmapping table: targetsworker-assets | worker-script | opennext | not-deployable, pathsdirect | cf-build | none.cf-buildprofiles are not built by DevStack; user gets a "Prepare for Cloudflare" PR (/api/projects/:id/prepare) + Deploy-to-Cloudflare button. - Upload safety —
upload.ts#parseDeployZip: size cap, file-count cap, declared-size zip-bomb guard,../absolute/backslash rejection, safe path charset, secret-file and secret-content scanning,__MACOSXstripping. Symlinks are not explicitly detected (JSZipunixPermissionsnot checked). - Log sanitizer —
deploy/sanitize.tsredacts known secrets + token patterns, strips control chars, 4000-char cap. - Audit —
audit_eventstable +connections/audit.ts, eventsdeploy.started|live|failed,credential.saved. - Limits —
src/lib/limits/capacity.ts: capacity-deriveddeploysPerDay,maxFixAttempts,maxFiles(≤200),maxBundleBytes. - UI —
/projects,/projects/[id](components/projects/*: list, detail, direct-deploy, upload-zip, deploy-button panels,deploy/deploy-status.tsx),/settings/connections|environment|cli|mcp,/builder,/factory,/studio(separate Vite app understudio/). These stay untouched unless in scope.
3. Feature matrix (spec t357u)#
Legend: REUSE works as-is · EXTEND exists, must be widened · IMPLEMENT absent · BLOCKED needs owner credentials/permissions/infrastructure.
| # | Spec feature | Status | Evidence / gap |
|---|---|---|---|
| 1 | Login / session | REUSE | auth/session.ts, gate() |
| 2 | Account isolation (A cannot see B) | REUSE | all SQL user-scoped; extend to new tables |
| 3 | Secret encryption at rest | REUSE | token-box v2 + Secrets Store CREDENTIALS_KEY |
| 4 | Connect GitHub account | EXTEND | OAuth works; scopes broad (repo), no per-repo grant |
| 5 | Install / authorize GitHub App | BLOCKED | no GitHub App exists; owner must register it (app id, private key, webhook secret) |
| 6 | Choose org vs personal account | IMPLEMENT | needs installations list (GitHub App) |
| 7 | List accessible repos + search + pagination | EXTEND | listWritableRepos single page of 50, no search |
| 8 | Import public / private existing repo | IMPLEMENT | only "create new repo" exists |
| 9 | Branch + root directory selection | IMPLEMENT | default_branch stored; no root_directory |
| 10 | States: loading/denied/empty/expired/retry | EXTEND | reauth/401 codes exist; UI states partial |
| 11 | Mode B: ZIP upload + validation | REUSE | parseDeployZip |
| 12 | Mode B: folder / multi-file upload | IMPLEMENT | zip only |
| 13 | Mode B: symlink detection | EXTEND | not checked explicitly |
| 14 | Mode B: project root selection + file list | EXTEND | auto single-root only |
| 15 | Mode C: start from template | EXTEND | templates + createProjectRepo(templateId); wire into New Project flow |
| 16 | Mode D (4th entry mode in spec) | IMPLEMENT | not present |
| 17 | Framework detection | EXTEND | detectProfile/uploadProfile coarse (package.json ⇒ "needs build"); add Next/Vite/Astro/SvelteKit/Remix/static detection + presets |
| 18 | Build config (install/build/output cmd, root, Node version) | IMPLEMENT | not stored |
| 19 | Project slug validation | EXTEND | safeWorkerName; no uniqueness table |
| 20 | Environment variables per project/environment | EXTEND | user_env is per-user only; need project+environment scope |
| 21 | Provider abstraction / registry | REUSE | provider.ts, providers.ts |
| 22 | Cloudflare direct deploy (no build) | REUSE | verified by tests with recorded API shapes |
| 23 | Cloudflare build pipeline (install+build untrusted code) | BLOCKED | needs isolated executor (Containers / Workers Builds) + owner infra approval — see BUILD-STRATEGY.md |
| 24 | Build queue / job state machine | IMPLEMENT | only 4-state deployment row; no queue binding |
| 25 | Real-time build logs | IMPLEMENT | only final sanitized error_log |
| 26 | Log sanitization | REUSE | sanitizeLog |
| 27 | Preview deployments + URL | EXTEND | one workers.dev URL per project; previews_enabled:false; need Worker versions/preview aliases |
| 28 | Production environment + promotion | IMPLEMENT | no env distinction |
| 29 | Deployment history | REUSE | listDeployments (30 rows) |
| 30 | Redeploy same commit | EXTEND | commit_sha column exists but is never written |
| 31 | Rollback | IMPLEMENT | cf_version_id stored; no rollback call |
| 32 | Git push trigger / PR preview / auto-build | BLOCKED | needs GitHub App webhooks + public webhook secret |
| 33 | Webhook signature verify + idempotency | IMPLEMENT | none |
| 34 | Concurrency / one-in-flight | REUSE | 15-min inflight check (non-atomic; tighten with unique index) |
| 35 | Quotas | REUSE | takeQuota + capacity limits |
| 36 | Audit events | EXTEND | table exists, no project_id/metadata |
| 37 | Retry logic | EXTEND | GitHub fetchRetry; none for CF API / jobs |
| 38 | Vercel provider | IMPLEMENT (later) | stub only, by design |
| 39 | Monitoring / alerts | EXTEND | Workers observability on; no deploy metrics |
| 40 | Production approval gate | IMPLEMENT | — |
| 41 | Dashboard (project settings page) | EXTEND | /projects/[id] exists |
| 42 | Tests: unit/integration/security/E2E | EXTEND | 40 Vitest files; no E2E for main app (Playwright only in studio/) |
Counts (42 rows): REUSE 10 · EXTEND 16 · IMPLEMENT 13 · BLOCKED 3 (Vercel counted as IMPLEMENT-later).
4. Risks found during audit#
- GitHub OAuth
reposcope grants write to all user repos; a GitHub App with per-repo install is the least-privilege fix (blocked on owner). - Inflight check is read-then-insert (race on concurrent clicks) → add a partial unique index in a migration.
waitUntil30 s budget caps direct deploys at ~200 files; any build step cannot run inside the main Worker (spec forbids it anyway).commit_shanever recorded → redeploy/rollback cannot be exact today.- Policy forbids writing
.github/workflows/*into customer repos — this rules out a GitHub-Actions-injection build strategy by design.
5. Test results (local, Node 22)#
| Step | Result |
|---|---|
npm ci |
pass (2 moderate npm-audit advisories, deprecated glob@10 transitive) |
npm run typecheck |
pass |
npm run lint |
pass (1 config warning: React version not set in eslint-plugin-react) |
npm test |
39 files / 388 tests passed |
npm run build, cf:build |
not run in M0 (no feature code changed) |
| Integration vs real GitHub / Cloudflare | BLOCKED — no test credentials approved; not reported as passed |
Source: docs/platform/AUDIT.md · /docs/platform/audit.md