---
name: imbrace-vibe-code-v2
description: Best practices for writing, scaffolding, deploying and debugging code that runs on the iMBRACE Fission platform through the imbrace-vibe-code-v2 CLI/SDK — React/Vite + Hono fullstack apps, static sites, and single Python/Node.js/Go functions. Use when creating or editing anything under frontend/, backend/, ui/ or api/ of an app deployed with `imbrace-vibe-code-v2 deploy`, when asked to "deploy to Fission", "write a function", "make an API", or when a deploy/build/route fails. Also use when a person asks to get started with iMBrace (vibe code) or to sign in / log in to iMBrace.
---

# Writing code for iMBRACE Fission (imbrace-vibe-code-v2)

`imbrace-vibe-code-v2 deploy <folder> --env <env>` uploads **source** to the vibe-fission cluster of that
environment. The cluster builds it. No Docker image is built and nothing is built locally.

Every app gets its own domain, which is its address. The domain depends on the environment (`imbrace-vibe-code-v2
config` prints it as `app domains`); cloud has no environment name in it:

| Environment | App domain | `<server>` |
|---|---|---|
| dev | `https://<app>.vibe.dev.imbrace.co/` (VPN) | `https://api.vibe.dev.imbrace.co` |
| sandbox | `https://<app>.vibe.sandbox.imbrace.co/` | `https://api.vibe.sandbox.imbrace.co` |
| cloud | `https://<app>.vibe.imbrace.co/` | `https://api.vibe.imbrace.co` |

The UI is at `/` of the domain and the API at `/api/...`. **Give the person the `domain` line `deploy` prints**, never
an address built by hand, and test the page and the API on that domain. The app is also served at a second address,
the path URL `<server>/fission/<app>/`, but not every cluster routes it the same way (its `/api/...` can answer with the
page instead of the API), so never use it to test or to hand out. A single-file function has no domain: it is at
`<server>/fission/<file-name>`.

The environment is the one the person chose (`imbrace-vibe-code-v2 envs` lists them: dev, sandbox, cloud). In an
imbrace-app project, `npm run login` writes it into `backend/src/imbrace.manifest.json` and `deploy` uses it; anywhere
else pass `--env`. With nothing given the CLI falls back to dev, which is not a choice made for the person. Override the
target with `--api-url`, `IMBRACE_VIBE_V2_API_URL` or `imbrace-vibe-code-v2 config set-env|set-url`. The app name becomes a sub-domain, so use
`a-z 0-9 -` and avoid reserved names (`api`, `www`, `admin`, `app`, ...).

## 0. A person asked to build an app on iMBrace, or to get started

This applies to every such request: "Read <this file's link> and help me get started", and also "Read <this file's
link> and build me <an app>", even when it says to build and deploy without stopping. **First ask the person two
things, in one message, and wait for the answers** before you create, install or sign in to anything:
- a short app name (`a-z 0-9 -`; it becomes the sub-domain);
- which iMBrace they use: **develop**, **sandbox**, **cloud**, or their company's own **on-premise** iMBrace (then
  also its address: what they open in the browser, e.g. `imbrace.acme.com`).

There is no default environment: never pick one yourself, and never take it from a folder name, another project on the
computer or the CLI's fallback. Then do these steps in order. **Never
ask the person for an API key or a password**: the sign-in below needs neither. When the CLI is installed
(`imbrace-vibe-code-v2 --version` works), run `imbrace-vibe-code-v2 <command>` wherever this section says
`npx --yes imbrace-vibe-code-v2@latest <command>`: the installed CLI is the version this file came with.

1. **Create the project** with the name they gave: `npx --yes imbrace-vibe-code-v2@latest create <app> --template imbrace-app` and `cd <app>`.
2. **Sign in** to the iMBrace they chose: `npx --yes imbrace-vibe-code-v2@latest login --env dev` (or `--env sandbox`,
   `--env cloud`, `--env onprem --domain <address>`). It prints ONE link and returns at once (it also tries to open the browser).
   Show the person the link as a clickable link and tell them: "Open this link, sign in to iMBrace and pick your
   organization. I'll carry on by myself once you're signed in." (It is the iMBrace sign-in page, SSO: they sign in
   there, never in the chat.)
   **In the same turn, without waiting for a reply**, run `npx --yes imbrace-vibe-code-v2@latest login --wait`. It
   returns as soon as the browser is done, so the person never has to tell you they signed in:
   - exit 0: signed in. It prints the email and the organization; tell the person.
   - exit 2: still waiting for the browser (each run waits about 2 minutes). Run `login --wait` again at once; the link
     works for 30 minutes.
   - exit 1: failed or expired. Run `login` again for a new link.

   The CLI keeps an org api key, acting as that person, in `~/.imbrace-vibe-code-v2-credentials.json` (mode 600).
   Never print that file and never put the key in the chat. `imbrace-vibe-code-v2 login info` shows who is signed in.
3. **Set the project up.** `npm run setup`, then `npm run login -- --env <the same as step 2>` (and `--domain` for
   on-premise): it copies the key into `.env` and writes the environment (an on-premise one by its gateway) and
   organization into the manifest, without asking anything.
4. **Build.** Ask what the business needs and follow `.claude/skills/imbrace-app/SKILL.md` (boards, functions, agents),
   then `npm run provision`. Test locally with `npm run dev:api` and `npm run dev:ui`.
5. **Deploy** (section 6; develop needs the iMBrace VPN): `npm run deploy -- --name <app> --yes`. It deploys to the
   environment of step 2 (from the manifest): its first line names the target, e.g. `Fission API: https://api.vibe.sandbox.imbrace.co/...
   (backend/src/imbrace.manifest.json env sandbox)`; if that is not the person's choice, stop and fix it. Then give the
   person the `domain` link it prints.

After sign-in the browser comes back to `127.0.0.1`, so it must run on the same computer as the CLI. `login` says so
when an environment has no SSO (an on-premise iMBrace without an auth service; `--auth-url` gives one at another
address). In those cases, or with no browser there (a cloud agent): `npm run login -- --env <env> --manual`,
where the person pastes an org api key into the terminal themselves (on-premise: also `--gateway <address>/api`).
An app deployed with `imbrace-vibe-code-v2` runs on the vibe cluster: an on-premise iMBrace must be reachable from it.

## 1. Pick a layout. The folder shape decides everything

Projects are detected from `package.json`, so folder names are free:

| Folder content | Detected as | Cluster does |
|---|---|---|
| `package.json` with `scripts.build` + `index.html` (or `public/index.html`) | **frontend** | `npm ci` → `npm run build -- --base=./` (Vite) → serves `dist/` (or `build/`, `out/`) |
| `package.json` with dependency `hono` | **backend** (owns all of `/api/*`) | `npm ci` → esbuild bundles the entry with a generated adapter |
| `ui/` (or `public/`, `static/`, `web/`) without package.json, or a root `index.html` | pre-built static UI | nothing |
| `api/*.py`, `api/*.js`, `api/*.go` | one function per file at `/api/<file>` and `/api/<file>/*` | Go is compiled; py/js load as-is |

- Prefer the **fullstack** layout (`imbrace-vibe-code-v2 create my-app`) for anything with npm dependencies.
- Use the **scripts** layout (`create my-app --template scripts`) for small stdlib-only endpoints.
- An app has a Hono backend **or** `api/` scripts, never both. That combination is rejected with a 400.
- Never upload build output on purpose. `node_modules/`, `dist/`, `.git/` and dotfiles are skipped automatically.
- Never add `vibe_build.sh`, `vibe_static.js` or `vibe_hono_adapter.mjs`. The platform reserves these names.

### Hono backend or `api/` scripts?

| | Hono backend (`backend/`) | `api/` scripts |
|---|---|---|
| Use when | npm packages (iMBRACE SDK, auth, zod, date libraries...), several endpoints sharing code (auth middleware, helpers, types), REST routes with params, TypeScript, an app that will grow | a few independent endpoints, the standard library is enough, Python or Go, a quick prototype or demo |
| Functions | one, owns every `/api/*` route | one per file, served at `/api/<file>` |
| Build | `npm ci` + esbuild in the cluster (~10–20 s) | none (Go is compiled) |
| Size limit | the whole project is one package | each file is its own package |

Rule of thumb: anything that needs `npm install`, or more than 3–4 endpoints sharing logic, is a Hono backend. The
`imbrace-app` template always uses one. Moving from `api/` to a backend later means deleting `api/`.

### Where the data lives: never create a database

- Records go on **iMBRACE data boards**. The `imbrace-app` template declares them in its manifest and
  `npm run provision` creates them (`.claude/skills/imbrace-app/SKILL.md`); the backend reads and writes them through
  the platform API. One board per kind of thing.
- **Do not create a database for an app**: no SQLite, lowdb or JSON files, no in-memory arrays as storage, no new
  Supabase / Neon / MongoDB Atlas / Firebase project, no database container. A function pod has no disk that survives
  a recycle and there is no secret store, so such a database is lost within minutes or needs a hard-coded password.
- Use another store only when the person names an **existing** one and says how the app may reach it: an
  internet-reachable API, never a private (VPC) address. Ask; do not pick one yourself.
- Uploaded files go through the platform's upload (`"uploads": true` in imbrace-app), not into the app folder and
  not into a board as base64.
- The template's key-mode files (`.data/chat.db`, `.data/users.json`) are for testing on a laptop only and are
  never deployed.

### Split the app by how often each part runs

Before writing code, list the parts of the request and put each one where it costs least:

| Part | Example | Put it in |
|---|---|---|
| Used on every visit | the app shell, the main list and detail screens and their API routes | the app's frontend and its one backend. Keep both small: they decide the first page load and every cold start |
| Used now and then | reports, exports, settings, admin, bulk import | a lazy-loaded screen (`React.lazy(() => import("./pages/Reports"))`). If it needs a heavy library (PDF or Excel generation, charts, image processing), an iMBRACE function, not the main backend |
| Runs when data changes | "when a row is approved, notify the owner" | a board automation + a function (imbrace-app recipe F) |
| Runs on a schedule or in the background | nightly sync, reminders, clean-ups | **not in a vibe function** (there is no cron here): an iMBRACE workflow with a schedule trigger, built in the dashboard |
| Long work | bulk import, long AI generation, anything near 60 s | an iMBRACE function that starts the job and writes progress to a board row; the UI polls the row |
| An unrelated tool | a second business process | its own app (own name, own domain) |

- **One app = one business purpose**: one frontend and one backend (or a few `api/` files). Do not put two products
  in one folder, and do not cut one product into many tiny apps either.
- **Every active function is a pod** (about 40 run at once per environment) and each pod pays its own cold start.
  A backend with 20 routes is one function; 20 `api/` files are 20. Past 3–4 endpoints, use one Hono backend.
- **The backend is one bundle**: a heavy npm package used by one rare feature slows the cold start of every request.
  Keep it out of the main backend.
- **Another app's API is another origin**: calling it from this app's page needs CORS on that backend
  (`hono/cors`). Shared logic is better as an iMBRACE function, reached through the platform API like any other.

## 2. Frontend rules (React/Vite or any SPA)

- **Only relative URLs.** The same build is served at `/` on the app domain *and* at `/fission/<app>/` on the
  path URL. Only relative URLs work on both.
  - API calls: `fetch("api/todos")` ✅. Do not write `fetch("/api/todos")` ❌, because that bypasses the app prefix.
  - Assets: Vite gets `--base=./` automatically. Do not hard-code `base: "/"` paths or `<img src="/logo.png">`.
    Import assets, or use `./logo.png`.
  - Router: prefer `createHashRouter` / `HashRouter`, which works on both URLs with no base path. A browser
    router works on the domain (unknown non-file paths fall back to `index.html`), but not on the path URL
    unless you set a basename.
- `npm run build` must succeed from a clean `npm ci`. Keep `package-lock.json` committed for reproducible builds.
- Lazy-load screens that are not used on every visit (`React.lazy` + `Suspense`, or `import()` in the route):
  the first load then only downloads the shell and the main screen.
- Local dev: proxy `/api` to the backend in `vite.config.js` (`server.proxy: { "/api": "http://localhost:8787" }`).
  The same relative `fetch("api/...")` then works both locally and on Fission.

## 3. Backend rules (Hono)

```ts
// backend/src/app.ts   (entry search order: src/app.ts, src/app.js, src/index.ts, src/index.js, app.ts, index.ts, server.ts)
import { Hono } from "hono";
const app = new Hono().basePath("/api");     // every route under /api
app.get("/todos", (c) => c.json([]));
export default app;                          // REQUIRED: the adapter calls app.fetch()
```

```ts
// backend/src/index.ts — local dev only, never imported by app.ts
import { serve } from "@hono/node-server";
import app from "./app";
serve({ fetch: app.fetch, port: 8787 });
```

- **Do not call `serve()` / `listen()` in the entry file.** The platform imports it and calls `app.fetch`.
- Dependencies are bundled by esbuild for `node22`: pure-JS packages only. Native addons (`bcrypt`, `sharp`,
  `sqlite3`, `canvas`...) will not work. Use pure-JS alternatives such as `bcryptjs`, or call an external service.
- Do not use `__dirname`-relative file reads for data files. Only the bundle is deployed. Inline the data or fetch it.
- Return proper status codes and JSON errors: `c.json({ error: "title is required" }, 400)`.
- No CORS setup is needed, because UI and API are same-origin.

## 4. Single-file functions (`api/<name>.ext` or one file)

| Language | Contract | Libraries |
|---|---|---|
| Python | `def main():` reads `flask.request` (`request.args`, `request.get_json(silent=True)`, `request.headers`) and returns a dict (→ JSON), str, or `(body, status)` | stdlib + `flask` only (no pip) |
| Node.js 22 | `module.exports = async (context) => ({ status, headers, body })`. `context.request` is an Express request: `.method`, `.query`, `.headers`, `.body` (JSON already parsed) | built-ins only (no npm) |
| Go 1.23 | `package main` + `func Handler(w http.ResponseWriter, r *http.Request)` | stdlib (plugin ABI: avoid third-party modules) |

- Inside an app, `api/users.py` receives `/api/users` (path `/`) and `/api/users/42` (path `/42`), with the
  prefix stripped. Route on the remaining path if needed.
- Name files with `a-z 0-9 -` only. The file name becomes the route and part of the function name.
- Python: never name a module `server.py`, because it clashes with the runtime's own `/app/server.py`.

## 5. Runtime facts that shape the design

### Limits (the same on dev, sandbox and cloud)

| Limit | Value | Notes |
|---|---|---|
| Upload per package, zipped | **100 MB** | what `deploy` sends: one frontend project, one backend project, or one `api/` file; `node_modules/` and `dist/` are never uploaded and npm dependencies do not count |
| Request duration | **60 s** | function timeout |
| Functions deployed | no practical limit | idle functions use no pods |
| Functions running at the same time | **~40** per environment | each active function is one pod with up to 512 MiB of memory; a burst of cold starts is served one new pod at a time (12 at once: 8–33 s for the last one) |
| Build (`npm ci` + build) | 2 GiB / 2 CPU | build log: `get <fn>` |
| Idle before a pod is recycled | ~120 s | in-memory state is lost |
| Cold start | 0.2–0.6 s; ~4 s when the warm pool is empty | warm requests ~0.1 s |

`GET <server>/fission-ui/api/config` returns the live upload limit (`max_package_bytes`). The limits are a
ceiling, not a target: see "Keep the upload small" below.

### Design rules that follow

- **Stateless and short-lived.** Pods come from a warm pool and are recycled after about **120 s idle**, on every
  redeploy, and under load (more pods). In-memory state (arrays, caches, sessions) is lost or diverges between pods.
  Keep state in iMBRACE data boards (see "Where the data lives") and send the record id with every request. An
  in-memory cache is fine only as a cache.
- **60 s per request.** Do no work after the response is sent, and avoid long polling. Streaming and WebSockets are
  untested on this platform, so design for plain request/response. Long or scheduled work (big exports, batch
  jobs, long LLM calls, cron) does not belong in a function: start it in an iMBRACE workflow/agent or another
  service and let the UI poll for the result.
- Local files are ephemeral and per-pod. Never use the filesystem as storage.
- Clusters differ in CPU (dev = amd64, the old box = arm64). Anything prebuilt must be architecture-independent.
- **Network:** function pods reach the internet and DNS. Other namespaces of the cluster are always blocked; on
  sandbox and cloud every private address is blocked too (VPC databases, `10.x`, `172.16–31.x`, `192.168.x`, cloud
  metadata). Dev is looser, so something that works on dev can fail on sandbox/cloud: a service the person named
  must be internet-reachable, or reached through an iMBRACE API.
- Secrets: there is no secret/env mechanism in imbrace-vibe-code-v2 yet. **Never hard-code credentials** in code
  you deploy. Ask the user how secrets should be provided.
- Security: UI, API and `/fission-ui/` currently have **no authentication**. Do not expose sensitive data or
  destructive endpoints without adding your own auth check.

### Keep the upload small (best practice for every app)

The CLI zips the whole folder, sends it in one request, the cluster rebuilds it, and every cold start downloads the
built UI again. Small uploads deploy faster and wake up faster.

- **Aim for under 5 MB zipped.** A typical React + Hono app is 0.1–2 MB of source. If yours is bigger, something
  in the folder does not belong there. The deploy prints the upload size (`deploying app <name> (<n> files, <size> KB)`).
- **Media does not go in the app.** Videos, audio, datasets, PDFs and images over ~1 MB belong in S3/CDN or
  iMBRACE storage, loaded by URL. Compress what stays (WebP/AVIF images, subset fonts, SVG icons).
- **Declare dependencies, never vendor them.** List packages in `package.json` + a committed lockfile; the cluster
  runs `npm ci`. Do not copy library code, minified bundles or `node_modules` into `src/`.
- **Delete leftovers before deploying.** The CLI skips `node_modules/`, `dist/`, VCS folders and dotfiles only.
  `build/`, `out/`, `coverage/`, test fixtures, screenshots, `*.zip`/`*.tar` archives and local databases ARE
  uploaded. Keep them outside the app folder.
- **No data files read at runtime.** Only the bundle is deployed (see section 3); large JSON/CSV seed data goes
  into a data board and is read by the API.
- **One app = one UI + one backend.** Split unrelated tools into separate apps instead of growing one folder.

## 6. Deploy and verify loop

```bash
imbrace-vibe-code-v2 config                       # target, environment, domain + "status: OK"
imbrace-vibe-code-v2 deploy . --yes               # imbrace-app: the env in its manifest; otherwise add --env dev|sandbox|cloud
                                                  # prints "domain  https://<app>.vibe…/": the address to give and test
imbrace-vibe-code-v2 apps                         # URLs
imbrace-vibe-code-v2 run <app>/todos              # GET  <app domain>/api/todos (no domain: <server>/fission/<app>/api/todos)
imbrace-vibe-code-v2 run <app>/todos -d '{"title":"x"}'   # POST JSON
imbrace-vibe-code-v2 logs <app>-api               # runtime logs (functions: <app>-ui, <app>-api, <app>-api-<file>)
imbrace-vibe-code-v2 get <app>-ui                 # build status + build log
imbrace-vibe-code-v2 delete <app> --yes
```

After every change, deploy and **call the real URLs**: the page, one asset, and each API route you touched.
A deploy that prints "created/updated" only means the upload worked. A build failure shows up in
`get <fn>` and in the deploy error.

## 7. Troubleshooting

| Symptom | Cause → fix |
|---|---|
| Blank page, `assets/...` 404 | absolute asset path or a custom `base`. Use relative paths and do not set `base: "/"` |
| API 404 from the page but works with curl | `fetch("/api/...")` with a leading slash. Use `fetch("api/...")` |
| `no route GET /api/x` | Hono route missing or not under `basePath("/api")` |
| `must export default app` / function crashes on load | entry file has no `export default app`, or it calls `serve()` |
| `build of <app>-ui failed` | `npm ci`/`npm run build` failed in the cluster. Run `npm ci && npm run build` locally to reproduce; check the lockfile |
| `Found both a Hono backend ... and scripts in api/` | keep one backend style |
| `… the limit is … KB` (HTTP 413) | upload is too big (100 MB per package, see `GET /fission-ui/api/config`). Follow "Keep the upload small": move media out, delete build/test leftovers |
| Deploy is slow or times out on upload | the folder is too big for your uplink. Check the KB size the deploy prints and follow "Keep the upload small" |
| Data disappears | in-memory state, a local file or SQLite in a recycled pod. Use a data board |
| Request fails after exactly 60 s | function timeout. Move the long work to a workflow/agent and poll, or split it into steps |
| Internal service times out from the function | private addresses are blocked by the NetworkPolicy. Use an internet-reachable endpoint or an iMBRACE API |
| `did not answer like a Fission API` | wrong `IMBRACE_VIBE_V2_API_URL`. Run `imbrace-vibe-code-v2 config` |
| `not running on a terminal - re-run with --yes` | non-interactive shell or CI. Add `--yes` |
