# Studio Deploy: Wireframe vs Reality Gap

**Written:** 2026-04-22
**Wireframe:** `/v17/studio-deploy.html`
**Goal:** paste/drop/CLI &rarr; live at `floom.dev/apps/<slug>` in **2 visible clicks max**.

---

## TL;DR

Reality today is **3 visible clicks + 1 form page + 1 Publish button**. The wireframe shows **0&ndash;1 clicks** after paste. The biggest missing piece is not UI polish: it is **auto-infer of inputs when there is no OpenAPI spec**. Floom today hard-requires an OpenAPI URL reachable over HTTP. Remove that dependency and the rest of the flow collapses naturally.

---

## Click count: now vs target

| Step | Reality (today) | Wireframe target |
|---|---|---|
| Land on Studio, click &ldquo;+ New app&rdquo; | 1 click | (not needed &mdash; drop on Studio home) |
| Switch to GitHub ramp (vs OpenAPI / Docker / Describe) | 0&ndash;1 clicks | 0 (one input) |
| Paste URL, click &ldquo;Detect&rdquo; | 1 click | 0 (paste auto-detects) |
| Review screen: edit name, slug, description, pick visibility | 0&ndash;4 clicks | 0 (deferred to app settings) |
| Click &ldquo;Publish&rdquo; | 1 click | 0 (auto-publish) |
| Done screen with share link | passive | passive, same page |
| **Total visible clicks** | **3&ndash;7** | **0&ndash;2** |

Reality today: `BuildPage.tsx` is a state machine `ramp &rarr; review &rarr; publishing &rarr; done` with at least two form submissions. One click to detect, one click to publish. The review page shows 4 editable fields (name, slug, description, visibility) as a gate before the Publish button fires.

---

## What needs to change: files & routes

### Frontend

| File | Current behaviour | Needs to |
|---|---|---|
| `apps/web/src/pages/StudioHomePage.tsx` | Shows apps grid + `+ New app` link to `/studio/build` | Render the drop zone **inline** at the top when empty, or as a compact paste bar when populated. Auto-trigger deploy on valid paste. |
| `apps/web/src/pages/BuildPage.tsx` (2157 LOC) | 4-state machine with explicit Detect and Publish buttons, full review form | Collapse to 2 states: `deploying` (live progress) &rarr; `deployed`. The ramp &ldquo;form&rdquo; disappears. Detection runs in the drop-zone component upstream and the page opens in `deploying` state with the parsed source already in URL params. |
| `apps/web/src/components/studio/StudioSidebar.tsx` | Lists apps + settings items | Add the big `Deploy` CTA at the top of the rail (mirrors wireframe). |
| *(new)* `apps/web/src/components/studio/DeployDropZone.tsx` | none | The reusable paste/drop/CLI surface. Handles: URL paste, file drop (folder zipped client-side), GitHub OAuth ramp, autodetect debounce, optimistic redirect to `/studio/deploy?source=<url>&slug=<auto>` on match. |
| `apps/web/src/lib/githubUrl.ts` | `looksLikeGithubRef`, `normalizeGithubUrl` already exist | Extend with a `localhostFolderRef` matcher and a `zipUrlRef` matcher so the drop zone knows which path to dispatch. |
| `apps/web/src/api/client.ts` (`detectApp`, `ingestApp`) | Two separate calls, both require an `openapi_url` | Collapse into one `/api/deploy` call that accepts `{source: {kind, url/file}}` and returns `{app, deployment_status, live_url}`. The API should stream progress (SSE or WebSocket) so the UI can render the progress log without polling. |

### Backend

| File | Current behaviour | Needs to |
|---|---|---|
| `apps/server/src/routes/hub.ts` (`/detect`, `/ingest`) | Two POSTs; both take `openapi_url` (string, HTTP-reachable) | Add `POST /api/deploy` that accepts multiple source kinds (`github`, `zip-url`, `upload`) and orchestrates detect + ingest in one shot. Keep old endpoints for backward compatibility. |
| *(new)* `apps/server/src/routes/deploy.ts` | none | New orchestrator: fetch repo snapshot, run input inference cascade, auto-slug, create app record, stream progress events. |
| *(new)* `apps/server/src/services/input-inference.ts` | none | The 4-step cascade: floom.yaml &rarr; openapi &rarr; function signatures (JS/TS/Python AST) &rarr; single text input fallback. This is the hard part (see Risk below). |
| `apps/server/src/services/openapi-ingest.ts` | Only handles OpenAPI &rarr; NormalizedManifest | Stays for the OpenAPI case. Wire it in as step 2 of the cascade. |
| `packages/detect/src/rules.ts` | Detects runtime (Node/Python/etc.) from repo files | Already does most of the work. Add a hook that, if runtime is detected but no OpenAPI spec is present, calls the new input-inference service. |
| *(new)* server endpoint `POST /api/deploy/upload` | none | Accepts multipart folder/zip upload. Streams to disk, runs detect on the unpacked tree, then proceeds same as a GitHub clone. Does not exist today: the hub router has zero multipart handling. |
| `packages/cli/src/index.ts` (36 LOC) | Has a stub `floom deploy <repo>` command that prints "not wired in the public beta yet" and exits 1 | Replace stub with real implementation: tar the working directory, POST to `/api/deploy/upload` with an API token, stream progress events back to stdout. |

### What already exists vs what is new

| Capability | Exists today | Needs building |
|---|---|---|
| GitHub repo URL accepted | YES (`looksLikeGithubRef`) | &mdash; |
| Auto-detect spec from GitHub repo | YES (probes `openapi.yaml` / `openapi.yml` / `openapi.json` at `main` + `master`) | Extend to also probe `floom.yaml` |
| OpenAPI spec ingest &rarr; NormalizedManifest | YES (`openapi-ingest.ts`) | &mdash; |
| Auto-slug from repo name | YES (but user confirms on review page) | Make it silent: auto-accept, append `-2` on collision. The 409 + suggestions path already returns 3 candidates, we'd just auto-pick the first. |
| Runtime detection from repo files | YES (`@floom/detect` package: Node/Python/PHP/etc.) | &mdash; |
| `floom.yaml` manifest | **NO** &mdash; zero references anywhere in the codebase | Define schema + parser |
| Function signature &rarr; input inference | **NO** | AST parser for JS/TS (Babel or tsc API) + Python (ast module or a Python sidecar) |
| Folder/zip upload endpoint | **NO** &mdash; `hub.ts` has no multipart | New route + storage (probably stream to `/tmp` then process) |
| Private GitHub OAuth | **NO** &mdash; doc says "coming soon" in BuildPage.tsx line 697 | Small OAuth app + token storage; probably a 1-day task |
| CLI deploy | **NO** &mdash; `packages/cli/src/index.ts` is a 36-line stub that prints "not wired" | Implement properly |
| Live progress streaming | **NO** &mdash; `/ingest` is synchronous, returns 200 when done | SSE/WebSocket endpoint |
| Inline live demo on deploy page | Partial (post-publish done screen links to `/p/:slug`) | Embed the actual run form inline (iframe the `/apps/:slug` page, or reuse the `RunPanel` component) |

---

## Biggest risk: what if the repo has no `floom.yaml` AND no clear function signatures?

**This is the hardest part.** The wireframe promises "we auto-infer inputs from code." Today, we only know how to do that when there is an OpenAPI spec at a predictable path. For a random vibe-coded Next.js / FastAPI / Flask repo with no spec, we have three fallbacks, each with trade-offs:

1. **`floom.yaml` manifest** &mdash; solves it cleanly **if creators write one**. But the whole point of this flow is that creators should not need to know about Floom-specific files. So this has to be **optional**.

2. **Function signature parsing** &mdash; works well for a single exported function like `def score_lead(url: str, rubric: str) -> dict`. Breaks down fast when:
   - The repo has **many** exported functions and we have to guess which one is the &ldquo;entry point&rdquo; &rarr; pick by `main`/`run`/`handler` naming convention, or the one referenced in `package.json` `bin`, or the HTTP route handler if Flask/FastAPI/Express.
   - Arguments use **custom types** (e.g. `def run(cfg: Config) -> Result`) &rarr; we can't infer a user-facing schema for `Config` without recursive expansion.
   - The function has **no type hints** at all (plain JS / untyped Python) &rarr; we only get parameter names, not types. Everything becomes `text`.

3. **Single text input fallback** &mdash; last resort. Render a single textarea labelled "Your input", pass the raw string to the app's entry point. Ugly but never blocks deployment. Creator can fix the schema later from Settings.

**The honest answer:** for &ldquo;real&rdquo; apps (OpenAPI or `floom.yaml` present), we hit 100%. For casual repos, we'll land somewhere around 60&ndash;70% correct input schemas. The failure mode is not "deploy fails" but "deploy succeeds with a crummy input form the creator has to fix after." That is acceptable as long as the app is actually live and the fix is one edit away in Settings.

**Mitigation:** on the deployed page, if inference used the text-fallback or had low confidence, show a small green banner &ldquo;We guessed your inputs &mdash; tweak them in Settings.&rdquo; Don't hide it.

---

## Effort estimate

| Chunk | Size | Notes |
|---|---|---|
| Drop-zone component + paste auto-detect | **S** (1&ndash;2 days) | Most of the GitHub detection logic already lives in `BuildPage.tsx`, we extract it. |
| Collapse review step &rarr; auto-publish | **S** (1 day) | Delete code. The hardest part is deciding the defaults. |
| New `POST /api/deploy` orchestrator + SSE progress | **M** (2&ndash;3 days) | New route, but it just wraps existing detect + ingest. SSE is 1 day extra. |
| `floom.yaml` parser + schema | **S** (1 day) | JSON-Schema-like, we already have NormalizedManifest as the target shape. |
| Function-signature input inference (JS/TS + Python) | **L** (5&ndash;8 days) | AST parsing + type resolution + picking the entry point. Python side probably needs a sidecar process. Biggest unknown. |
| Folder/zip upload endpoint | **M** (2&ndash;3 days) | Multipart handling in Hono, temp storage, cleanup, security (don't execute untrusted code). |
| Inline live demo on deployed page | **S** (1&ndash;2 days) | Reuse `RunPanel` or iframe `/p/:slug`. |
| CLI `floom deploy` implementation | **M** (2&ndash;3 days) | Tar + upload + stream progress. Straightforward once the upload endpoint exists. |
| Private GitHub OAuth | **M** (2&ndash;3 days) | GitHub OAuth app, token storage, UI ramp. |
| StudioHome inline drop zone | **S** (1 day) | UI-only change. |

**Total: ~3&ndash;4 weeks of focused work** to get from today's 3&ndash;7-click flow to the wireframe's 0&ndash;2-click flow. Function-signature inference is the risk line item &mdash; if we decide to skip it and rely on the text-input fallback for non-OpenAPI repos, we can ship the rest in ~1.5 weeks.

---

## Recommended phasing

**Phase 1 (1.5 weeks):** Drop zone on Studio home &middot; auto-detect on paste &middot; collapse review step &middot; auto-publish &middot; auto-slug &middot; SSE progress &middot; inline live demo. **OpenAPI-only sources.** This alone hits the 2-click target for any creator who has an OpenAPI spec in their repo. Shippable.

**Phase 2 (1 week):** `floom.yaml` support &middot; folder upload endpoint &middot; CLI implementation. This unlocks creators who don't want OpenAPI.

**Phase 3 (1&ndash;2 weeks):** Function-signature inference &middot; private GitHub OAuth. The &ldquo;it just works on any repo&rdquo; promise. Highest risk, lowest urgency &mdash; ship if we have bandwidth after launch.
