# Full-stack projects across browser and Node hosts

The guest project can keep its frontend, FastAPI backend, and `http://127.0.0.1:8000`
API URLs. Hosting prerequisites belong to the outer application that embeds SandboxedJS.
They cannot all be removed by a JavaScript library.

| Where SandboxedJS executes | Frontend → guest backend | Backend → external API | Lifetime |
| --- | --- | --- | --- |
| Browser, served by a static CDN | `createPreview(box)` rewrites loopback fetch/XHR/EventSource requests into virtual ports | Direct when browser policies permit; otherwise a deployed egress function or an explicitly configured relay | Owner tab stays open |
| Long-running Node process | `box.request()` or an application HTTP adapter; `box.expose()` for local access | Node performs outbound requests | Process stays alive |
| Node serverless function | Use `box.request()` within the invocation; return its result through the platform's HTTP response | Node performs outbound requests | Invocation only; do not rely on a background server surviving the response |

A Vercel-hosted browser app is still a **browser** runtime. Serving its assets from Vercel
does not give code in the visitor's browser Node networking permissions. A purely static
host cannot itself relay an API that rejects cross-origin browser calls. Use a function on
that host, or provide `network.proxy` pointing to a relay you operate.

## Browser host setup

1. Serve over HTTPS (localhost is suitable for development).
2. Send these HTTP response headers on the outer document:

   ```http
   Cross-Origin-Opener-Policy: same-origin
   Cross-Origin-Embedder-Policy: require-corp
   ```

3. Serve the matching worker and Python runtime assets from the installed package. When
   copying them into `public`, copy them on **every production build**, not just on `dev`:

   ```json
   {
     "scripts": {
       "predev": "npm run stage:runtime",
       "prebuild": "npm run stage:runtime",
       "build": "tsc && vite build"
     }
   }
   ```

   The staging script must copy `dist/service-worker.js` from the same installed
   `sandboxedjs` as the host bundle. Copy the guest worker, Python worker, and Python
   distribution if your host explicitly serves those from `public` too. Do not keep an
   independently maintained old service worker. Alternatively, let your bundler emit the
   default worker asset used by `createPreview()`.

4. Register the emitted worker and use its URL for the iframe:

   ```ts
   const preview = await createPreview(box, { scriptUrl: '/service-worker.js' });
   if (!preview) throw new Error('Preview worker could not be registered');
   iframe.src = preview.urlFor(3000);
   ```

   Without a copied/custom worker, omit `scriptUrl`. Preserve the same-origin worker asset
   as a file; do not inline it as a data URL. A strict guest CSP that rejects the injected
   scripts, compressed HTML, or `{ websocket: false }` prevents the current loopback shim
   from being installed. Use normal uncompressed preview HTML with injection enabled.

5. Enable outbound access for package installation and API calls:
   `network: { allowOutbound: true }`. Start both guest servers, check their exit/output
   and `waitForPort()` results, and keep the owning container alive.

## External APIs: create the relay in the right directory

Run from the **host application root**, not the guest's `/workspace` shell:

```sh
# Cloudflare Pages
npx sandboxedjs-egress init --target cloudflare --allow ollama.com,api.openai.com
# Vercel
npx sandboxedjs-egress init --target vercel --allow ollama.com,api.openai.com
# Netlify
npx sandboxedjs-egress init --target netlify --allow ollama.com,api.openai.com
```

Use one command for the platform you deploy to. An explicit project directory may appear
before or after the flags. Older CLI builds incorrectly treated a flag value as the
project directory; if you see a folder called `ollama.com,api.openai.com` or `cloudflare`,
upgrade the CLI and generate the function at the root instead.

Expected paths:

| Platform | File relative to host project root | Probe URL |
| --- | --- | --- |
| Pages | `functions/__sandboxedjs__/egress.ts` | `/__sandboxedjs__/egress` |
| Vercel | `api/__sandboxedjs__/egress.ts` | `/api/__sandboxedjs__/egress` |
| Netlify | `netlify/functions/sandboxedjs-egress.ts` | `/.netlify/functions/sandboxedjs-egress` |

A GET to the deployed probe must return JSON containing
`{"sandboxedjs":"egress","protocol":1}`, not the site's HTML. Containers discover these
paths automatically. Development uses `npx sandboxedjs-egress --allow ollama.com` or
`npx sandboxedjs-serve dist`; Vite alone does not execute Pages Functions.

Deploy the function along with the static build. Cloudflare dashboard drag-and-drop only uploads
`dist/` and does not compile a root `functions/` directory automatically: either deploy via Git integration
or Wrangler, or compile it into `dist/_worker.js` during build:
`wrangler pages functions build functions --outdir dist/_worker.js`.
See [Cloudflare's deployment documentation](https://developers.cloudflare.com/pages/get-started/direct-upload/).

Keep the relay's allowlist narrow and apply your application's authentication and rate limits
at the host route. Guest `.env` files and browser-delivered keys are visible to the visitor;
operator-owned credentials belong in server-side secrets. Do not commit actual keys.

## FastAPI and a static frontend

Inside one container, use the project's normal commands:

```sh
cd /workspace/backend
pip install -r requirements.txt
fastapi run main.py --host 0.0.0.0 --port 8000 &
cd /workspace/frontend
npx serve . -l 3000
```

Preview port 3000 with `preview.urlFor(3000)`. Its fetch to
`http://127.0.0.1:8000/agent/run/stream` must appear in browser developer tools as a
same-origin `/…/__sbx__/8000/agent/run/stream` request. No CORS relay is needed for this
internal hop. An external Ollama request made by FastAPI uses the separate egress path.

The bridge currently buffers response bodies. A finite SSE response arrives after the
backend finishes, preserving its event text, but tokens do not appear progressively.
An indefinitely open stream will time out. `createPreview(box, { timeoutMs: 600_000 })`
changes the preview timeout; it cannot change a hosting platform's function limits.

## Node and serverless usage

The stable transport API is `box.request(port, { method, path, headers, body })`. Start the
backend and wait for its virtual port before invoking it. Return `response.bytes`, status,
and headers using your framework's response adapter. Clean up the container when its
owning request/session ends. `box.expose()` opens a local socket and is useful for local
Node development; it does not create a public route in Vercel.

Serverless workers may be frozen or discarded between invocations. Store files/session
state externally if needed and reconstruct the container, or use a persistent host for
interactive sessions. Package support, available workers/WASM, memory, payload sizes and
[function duration limits](https://vercel.com/docs/functions/limitations) still apply.
This project does not promise that an arbitrary browser workload runs unchanged inside an
edge runtime.

## Diagnose the actual failed hop

- **Frontend still requests `127.0.0.1:8000` in developer tools:** check the deployed worker
  for `installPreviewFetch`, rebuild staged assets, then reload the host and reopen the
  preview. Also check CSP/injection restrictions. Changing FastAPI CORS does not fix an
  address that points at the visitor's machine.
- **Same-origin `__sbx__/8000` returns 502:** check backend process output and
  `await box.request(8000, { path: '/docs' })`.
- **Backend receives the request but its AI call fails:** verify the egress probe, allowed
  hostname, upstream credentials and upstream response. Test authenticated and ordinary
  user sessions independently; application permissions may differ.
- **504 after a long wait:** inspect backend completion and buffering/host time limits.
- **Works locally but not after build:** inspect actual deployed headers, worker bytes,
  copied runtime versions and function deployment. A successful asset build alone does
  not deploy a function.

## Regression evidence

`test/preview-worker-build.test.ts` evaluates the distributed service worker, executes its
injected script, and verifies the loopback POST route and navigation paths.
`test/egress-cli.test.ts` checks generated paths and flag order for all three platforms.
`test/fixtures/fullstack-browser` builds a real static browser fixture with a Python
FastAPI backend, a separate frontend, and a finite SSE POST. It needs no AI credentials;
see its README for running it.
