# Frontend projects

> Managed React, Next, Svelte, and Vue shells, frontend asset pipeline, and native dev server integration for web frontends.

- Repository: vercel-labs/native
- GitHub: https://github.com/vercel-labs/native
- Human docs: https://grok-wiki.com/public/docs/vercel-labs-native-8ccc3580636a
- Complete Markdown: https://grok-wiki.com/public/docs/vercel-labs-native-8ccc3580636a/llms-full.txt

## Source Files

- `docs/src/app/frontend/layout.tsx`
- `skill-data/core/references/frontend-assets.md`
- `examples/react/build.zig`
- `examples/next/build.zig`
- `examples/browser/frontend/app.js`

---

---
title: "Frontend projects"
description: "Managed React, Next, Svelte, and Vue shells, frontend asset pipeline, and native dev server integration for web frontends."
---

Web frontend apps in Native SDK are **WebView shells**: a Zig native process owns the window and host, while UI runs as web content. The shell switches content source via `native_sdk.frontend.sourceFromEnv` / `productionSource` (`src/frontend/root.zig`): when `NATIVE_SDK_FRONTEND_URL` is set (by `native dev`), the WebView loads the framework dev server; otherwise it serves static files from the configured `dist` root under the `zero://app` origin. Scaffold with `native init --frontend <next|vite|react|svelte|vue>` (web scaffolds always get a full `build.zig` graph because npm install/build must run before package and run).

<Note>
Default `native init` is native-rendered (`.native` + Zig `UiApp`), not a WebView frontend. Use `--frontend` only when shipping web UI inside a WebView.
</Note>

## Architecture

```mermaid
flowchart TB
  subgraph cli ["CLI / build"]
    init["native init --frontend …"]
    zigdev["zig build dev / native dev"]
    zigrun["zig build run"]
    pkg["native package --assets dist"]
  end

  subgraph appzon ["app.zon"]
    fcfg[".frontend dist/entry/dev"]
    sec["security.navigation.allowed_origins"]
  end

  subgraph shell ["Zig shell"]
    main["src/main.zig App + source_fn"]
    fe["native_sdk.frontend"]
    wv["WebViewSource url | assets"]
  end

  subgraph content ["Web content"]
    devsrv["npm run dev\nVite :5173 / Next :3000"]
    dist["frontend/dist or frontend/out"]
  end

  init --> fcfg
  init --> main
  zigdev --> fcfg
  zigdev -->|"spawn command, wait ready"| devsrv
  zigdev -->|"NATIVE_SDK_FRONTEND_URL\nNATIVE_SDK_MODE=dev\nNATIVE_SDK_HMR=1"| main
  main --> fe
  fe -->|"env set"| wv
  fe -->|"env unset"| dist
  dist --> wv
  devsrv --> wv
  zigrun --> dist
  pkg --> dist
  sec -.->|"must allow zero://app\nand dev origin"| wv
```

| Layer | Responsibility |
| --- | --- |
| `app.zon` `.frontend` | Dist path, entry HTML, SPA fallback, dev URL/command/timeout for the CLI |
| `src/main.zig` | `App.source` / `source_fn` bind env-aware WebView content |
| `src/frontend/root.zig` | `sourceFromEnv`, `productionSource`, `Config` defaults |
| `src/tooling/dev.zig` | Spawn frontend process, readiness poll, set env, launch binary, tear down on exit |
| `frontend/` | Framework project (Vite or Next); npm owns HMR and production build |
| `native package` / `bundle-assets` | Copy `frontend.dist` into package/build output |

## Scaffold

```bash
native init my_app --frontend react    # also: next | vite | svelte | vue
cd my_app
zig build dev                          # or: native dev
```

| `--frontend` | Production dist | Dev URL (template default) |
| --- | --- | --- |
| `react`, `vite`, `svelte`, `vue` | `frontend/dist` | `http://127.0.0.1:5173/` |
| `next` | `frontend/out` | `http://127.0.0.1:3000/` |
| `native` (default) | no npm frontend | N/A |

Web scaffolds write `build.zig`, `build.zig.zon`, `src/main.zig`, `src/runner.zig`, `app.zon`, `frontend/`, README, and a CI workflow. Template helpers live in `src/tooling/templates.zig` (`Frontend.distDir`, `devUrl`, `devPort`).

## Project layout

Framework examples (`examples/react`, `examples/next`, `examples/svelte`, `examples/vue`) share this shape:

:::files
my_app/
  app.zon                 # .frontend + navigation origins + webview capability
  build.zig               # frontend-install, frontend-build, run, dev, package
  src/main.zig            # App + sourceFromEnv
  src/runner.zig
  frontend/
    package.json
    # Vite: index.html, vite.config.*, src/
    # Next: next.config.js (output: "export"), app/
:::

Static chrome (no managed dev server) is separate: `examples/browser` serves `frontend/` via `productionSource` only and drives child WebViews through `window.zero` builtins.

## Manifest: `app.zon` `.frontend`

Manifest types: `FrontendConfig` / `FrontendDevConfig` in `src/primitives/app_manifest/types.zig`. Validation: relative `dist`/`entry`, HTTP(S) `dev.url`, non-empty `dev.command`, `timeout_ms > 0` (`validateFrontend`).

### Vite-family (React / Svelte / Vue)

```zig
.frontend = .{
    .dist = "frontend/dist",
    .entry = "index.html",
    .spa_fallback = true,
    .dev = .{
        .url = "http://127.0.0.1:5173/",
        .command = .{ "npm", "--prefix", "frontend", "run", "dev", "--", "--host", "127.0.0.1" },
        .ready_path = "/",
        .timeout_ms = 30000,
    },
},
.security = .{
    .navigation = .{
        .allowed_origins = .{ "zero://app", "zero://inline", "http://127.0.0.1:5173" },
        .external_links = .{ .action = "deny" },
    },
},
.capabilities = .{ "webview" },
```

### Next.js

```zig
.frontend = .{
    .dist = "frontend/out",
    .entry = "index.html",
    .spa_fallback = true,
    .dev = .{
        .url = "http://127.0.0.1:3000/",
        .command = .{ "npm", "--prefix", "frontend", "run", "dev" },
        .ready_path = "/",
        .timeout_ms = 30000,
    },
},
// allowed_origins include http://127.0.0.1:3000
```

Production export requires `output: "export"` in `frontend/next.config.js` so build output lands in `frontend/out`.

### Fields

| Field | Default | Role |
| --- | --- | --- |
| `dist` | `"dist"` | Built asset root copied into packages and loaded as assets |
| `entry` | `"index.html"` | HTML entry under `dist` |
| `spa_fallback` | `true` | Serve `entry` for unknown asset paths |
| `dev.url` | required if `dev` set | URL loaded when env is set; readiness host/port |
| `dev.command` | required if `dev` set | Argv spawned by `native dev` (must be non-empty) |
| `dev.ready_path` | `"/"` | HTTP path polled for 2xx/3xx |
| `dev.timeout_ms` | `30000` | Max wait before `error.Timeout` |

## Runtime source API

`native_sdk.frontend.Config` (`src/frontend/root.zig`):

| Field | Default |
| --- | --- |
| `dist` | `"dist"` |
| `entry` | `"index.html"` |
| `origin` | `"zero://app"` |
| `spa_fallback` | `true` |
| `dev_url_env` | `"NATIVE_SDK_FRONTEND_URL"` |

```zig
// Env present → WebViewSource.url; else assets under dist
return native_sdk.frontend.sourceFromEnv(self.env_map, .{
    .dist = "frontend/dist",
    .entry = "index.html",
});

// Always package assets (no env branch)
return native_sdk.frontend.productionSource(.{
    .dist = "frontend/dist",
    .entry = "index.html",
});
```

Framework shells set both a static `.source` (production default) and `.source_fn` that calls `sourceFromEnv` so `native dev` and `zig build run` share one binary:

```zig
// examples/react/src/main.zig pattern
.source = native_sdk.frontend.productionSource(.{ .dist = "frontend/dist" }),
.source_fn = source, // sourceFromEnv with matching dist/entry
```

`NATIVE_SDK_FRONTEND_ASSETS` is **not** read by `src/frontend/root.zig`. `examples/webview` treats it as an app-level convention to force `productionSource` when set.

## Dev server lifecycle

`src/tooling/dev.zig` `run`:

1. Require `metadata.frontend` and `frontend.dev` (`MissingFrontend` / `MissingDevConfig`).
2. Spawn `dev.command` in its own process group (if non-empty).
3. Poll `dev.url` + `ready_path` every 100ms until HTTP 2xx/3xx or `timeout_ms` (`Timeout`).
4. Launch `binary_path` with env: `NATIVE_SDK_FRONTEND_URL`, `NATIVE_SDK_MODE=dev`, `NATIVE_SDK_HMR=1`.
5. On shell exit (or CLI signal), kill frontend and app process groups.

Only `http://` and `https://` URLs parse; `ws://` fails `InvalidUrl`. Host `localhost` is resolved as `127.0.0.1` for the readiness TCP connect.

Framework HMR stays on the JS tooling (Vite/Next) because the WebView navigates to the live URL; `NATIVE_SDK_HMR=1` marks the shell session and does not implement framework HMR itself.

### Commands

```bash
# From an example or scaffolded app (build.zig wires native dev)
zig build frontend-install
zig build dev

# CLI: build shell + frontend flow when app owns the graph
native dev --manifest app.zon --binary zig-out/bin/react

# Zero-config verb path (no --binary): native builds then hands off to frontend flow
native dev [--url URL] [--command "npm --prefix frontend run dev"] [--timeout-ms 30000]

# Production path: install, build frontend, run shell against assets
zig build run
```

Example `build.zig` steps (React/Next): `frontend-install` → `npm install --prefix frontend`; `frontend-build` → `npm run build`; `run` depends on `frontend-build`; `dev` depends on exe + `frontend-install` and invokes `native dev --manifest app.zon --binary <exe>`.

## Package and assets

- `native package --assets <dir>` defaults assets to `metadata.frontend.dist` when present.
- Examples pass `--assets frontend/dist` (Vite) or `frontend/out` (Next) and depend on `frontend-build`.
- `native bundle-assets [app.zon] [assets] [output]` and `zig build bundle-assets` copy the assets tree and write `asset-manifest.zon` (`src/tooling/assets.zig`).

Build frontend **before** packaging; the package step does not run npm for you unless your `build.zig` `package` step depends on `frontend-build`.

## Security and origins

Allow exact origins the WebView will use:

| Mode | Typical `allowed_origins` |
| --- | --- |
| Packaged assets | `zero://app` |
| Vite/React/Svelte/Vue dev | `http://127.0.0.1:5173` |
| Next dev | `http://127.0.0.1:3000` |
| Inline demos only | `zero://inline` |

Do not use `"*"` for production navigation. Packaged HTML should use a strict CSP; dev CSP must also allow the local WebSocket endpoints the framework uses for HMR.

## Frontend ↔ native bridge

Examples probe the injected bridge:

```ts
// examples/react/frontend/src/App.tsx
setBridge((window as any).zero ? "available" : "not enabled");
```

Invoke host commands with:

```js
const result = await window.zero.invoke("native.ping", { source: "webview" });
```

Builtin window/WebView helpers need `js_window_api` / capability and bridge policy registration. The browser example enables `js_bridge` and policies for `native-sdk.webview.*` / `native-sdk.window.list`. See the bridge and web-engines pages for policy and engine choice.

## Example matrix

| Path | Stack | Dist | Dev | Notes |
| --- | --- | --- | --- | --- |
| `examples/react` | React + Vite | `frontend/dist` | `:5173` | Managed `sourceFromEnv` shell |
| `examples/vue` | Vue + Vite | `frontend/dist` | `:5173` | Same pattern |
| `examples/svelte` | Svelte + Vite | `frontend/dist` | `:5173` | Same pattern |
| `examples/next` | Next static export | `frontend/out` | `:3000` | `output: "export"` |
| `examples/browser` | Static HTML/JS | `frontend/` | none | Multi-WebView chrome via `window.zero` |
| `examples/webview` | Inline HTML + optional env | `dist` | optional | Demonstrates `NATIVE_SDK_FRONTEND_ASSETS` convention |

Standalone checkout of repo examples: `zig build run -Dnative-sdk-path=/path/to/native-sdk`.

## Environment variables

| Variable | Set by | Effect |
| --- | --- | --- |
| `NATIVE_SDK_FRONTEND_URL` | `native dev` | WebView loads this URL (`sourceFromEnv`) |
| `NATIVE_SDK_MODE` | `native dev` → `"dev"` | Session marker for the shell |
| `NATIVE_SDK_HMR` | `native dev` → `"1"` | Marks HMR-oriented dev session |
| `NATIVE_SDK_FRONTEND_ASSETS` | app/user | Not core API; app may force packaged assets |

## Failure modes

| Symptom | Likely cause | Check |
| --- | --- | --- |
| `MissingFrontend` / `MissingDevConfig` | No `.frontend` or no `.dev` in `app.zon` | Manifest parse; `native check` / manifest validation |
| Dev hangs then `Timeout` | Port never answers 2xx/3xx | `dev.url`, `ready_path`, command, firewall; run `npm --prefix frontend run dev` alone |
| Blank WebView / blocked navigation | Origin not allowed | Match `allowed_origins` to `zero://app` and the live dev host:port |
| Production shows empty shell | Dist missing or wrong path | Run `frontend-build`; Next must export to `frontend/out` |
| Package missing UI | Assets dir wrong | `--assets` / `frontend.dist` vs actual build output |
| Orphaned npm process | Manual kill of CLI without process group | Prefer `native dev` teardown; process groups are intentional in `dev.zig` |
| `InvalidUrl` on readiness | Non-HTTP(S) scheme | Use `http://127.0.0.1:…/`, not `ws://` |

## Verification

| Goal | Command / signal |
| --- | --- |
| Install deps | `zig build frontend-install` exits 0; `frontend/node_modules` present |
| Production shell | `zig build run` opens window with built UI (no localhost) |
| Dev + HMR | `zig build dev`; WebView loads Vite/Next URL; edit frontend → framework HMR |
| Source unit tests | Example tests assert `productionSource` root path; `src/frontend/root.zig` tests env branch |
| Package | `zig build package` (or `native package …`) includes frontend assets under configured dist |

## Next

<CardGroup>
  <Card title="Web engines" href="/web-engines">
    System WebView vs Chromium (CEF), flags, and when native-rendered UI coexists with web content.
  </Card>
  <Card title="Bridge and builtin commands" href="/bridge">
    Host-to-content bridge payloads, builtins, async dispatch, and permission boundaries.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    `native init`, `dev`, `bundle-assets`, `package`, flags, and failure modes.
  </Card>
  <Card title="Packaging" href="/packaging">
    Release binaries, asset layout, and package graph outputs for distributable apps.
  </Card>
  <Card title="Platform support and security" href="/platform-security">
    Host capability matrix and navigation/bridge security boundaries.
  </Card>
</CardGroup>
