# Flutter web app on uat-beam.page

> **The build must be made with `/app/` as the base href.** Run
> `flutter build web --base-href=/app/` — the flag bakes
> `<base href="/app/">` into `index.html` and cannot be patched after
> the build. Call `usePathUrlStrategy()` on app start to drop the `#/`
> URL fragment so deep `/app/...` paths render cleanly.

Ship a Flutter web build to a uat-beam.page project. The app lives under
`/app/`; the bare subdomain redirects to it.

This is the right pattern when:

- The app is a Flutter web build (`flutter build web`)
- The output is a `build/web/` folder with `index.html`, `main.dart.js`,
  `flutter.js`, canvas-kit assets, etc.
- The user wants the Flutter app to own the URL space under `/app/`

The key platform fact: ZIP uploads require `index.html` at the **root**
of the archive. So you upload the build to a sub-page named `app` and
ZIP from inside `build/web/` — the archive root is the build root, and
the page slug `app` is what maps the files to `/app/...` on the
subdomain.

## 1. Build with the `/app/` base href

```bash
flutter build web --base-href=/app/
```

This sets `<base href="/app/">` inside the emitted `index.html` and
makes every generated URL relative to `/app/`. Without this flag the
app will 404 its own assets on uat-beam.page. The flag must be set at build
time; it cannot be patched in afterwards.

## 2. Stage the ZIP

```bash
cd build/web && zip -r ../../build.zip .
cd ../..
```

Verify with `unzip -l build.zip` — you should see `index.html` (not
`app/index.html`), `main.dart.js`, `flutter.js`, `assets/...` at the
top level.

## 3. Create the project and the `app` sub-page

```python
import requests, time
API = "https://api.uat-beam.page"
H = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
slug = "my-flutter-app"

requests.post(f"{API}/projects", headers=H, json={
    "slug": slug,
    "context": "Flutter web app under /app/.",
})
time.sleep(2)

requests.post(f"{API}/projects/{slug}/pages", headers=H, json={
    "slug": "app",
    "notes": "Flutter web build. Do not hand-edit — regenerate from source.",
})
time.sleep(1)

r = requests.post(
    f"{API}/projects/{slug}/pages/app/upload-zip-url",
    headers=H,
).json()
```

Then PUT the ZIP (MCP agents: ask the user to run the `curl`; chat-only
MCP cannot PUT binary bytes):

```bash
curl -X PUT --data-binary @build.zip \
  -H "Content-Type: application/zip" \
  -H "x-amz-meta-via: presigned-zip-upload" \
  -H "x-amz-meta-tenant-id: <from response.headers>" \
  -H "x-amz-meta-user-sub: <from response.headers>" \
  -H "x-amz-meta-entity: PAGE" \
  -H "x-amz-meta-project-id: my-flutter-app" \
  -H "x-amz-meta-slug: app" \
  "<response.uploadUrl>"
```

Echo every header from `response.headers` verbatim — mismatch fails
with `SignatureDoesNotMatch`.

## 4. Redirect the bare subdomain to the app

```python
requests.post(f"{API}/projects/{slug}/redirects", headers=H, json={
    "from": "/",
    "to": "/app/",
})
```

Wait ~5 seconds.

## 5. Verify

```bash
curl -sI https://my-flutter-app.uat-beam.page/
# → HTTP/2 301 ... location: /app/

curl -s https://my-flutter-app.uat-beam.page/app/ | head
# → Flutter's index.html
```

Flutter's web router owns every deeper path under `/app/`
(`/app/settings`, `/app/users/42`, etc.) — all resolve to
`<slug>/app/index.html` and the Dart router takes over in the browser.

If asset requests 404, re-run the build with `--base-href=/app/`.
