← Files HiggsfieldARCHIVED FILE
skills/website-builder/references/app-quickstart.md
12.2 KB · Oct 3, 2026 · 06:02 UTC
# App quickstart — the working critical path
> **Use `sandbox_exec` for all code edits.** Read `references/repo-and-sandbox.md`
> first: `website_repo_access` checks out and pushes without exposing credentials.
> Commit in the returned checkout path and push before the 15-minute lease expires.
Read this FIRST when building a `type: "app"` product. It is the copy-paste
spine every generation app shares: auth → SDK client → submit/poll → render, plus
the handful of Quanta components you'll reach for. Everything here is real,
correct, and enough to build from — you do **not** need to open
`app/packages/{quanta,fnf,fnf-react}/src/**` to confirm any of it. For the full
detail behind each step, the deeper references are cross-linked; for component
props, `app/packages/quanta/ai/AGENTS.md` has the per-component reference.
**Golden rules (violating these is the usual cause of a broken or slow build):**
1. **The home page already exists — wire this spine INTO it.** The starter
template chosen at create ships its layout as REAL CODE, already wired as
the home page (`app/src/routes/index.tsx` → `app/src/layouts/<template>.tsx`).
Everything below (auth, clients, submit/poll, rendering) gets threaded
through that shipped layout — ADAPT IT IN PLACE. Do not scaffold a new home
screen, paste a fresh page over the shipped one, or swap layouts;
`app/src/layouts/AGENTS.md` + `app/src/components/AGENTS.md` are the layout
contract.
2. **Don't read package source.** Props are in `app/packages/quanta/ai/AGENTS.md`;
SDK/hooks are in `app/packages/fnf/ai/AGENTS.md` + `.../fnf-react/ai/AGENTS.md`
(mirrored by `references/fnf-sdk.md` + `references/fnf-react.md`). Reading
`src/` is the single biggest time sink. If a detail is truly missing, make the
reasonable call and let `bun run typecheck` catch a mistake.
3. **fnf runs server-side only** against `https://fnf.internal` — never from the
browser, never with tokens in app code (`references/auth.md`, rule 3 in
`app-flow.md`).
4. **Always dark, no app header.** `data-theme="default-dark"` is pinned; the
Higgsfield host provides the global header/account chrome. Container is
`mx-auto w-full max-w-7xl` (`references/quanta-design.md`).
5. **Real backend + real D1, never a mock.** (rule 3a in `app-flow.md`.)
---
## 1. Auth — the `/api/user` proxy + login/logout
Every app is authenticated. Full contract in `references/auth.md`; the three
pieces you always write:
```ts
// app/src/routes/api/user.ts — browser-safe proxy; preserve status + body 1:1
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/api/user')({
server: {
handlers: {
GET: async () => {
const upstream = await fetch('https://fnf.internal/user')
const body = await upstream.text()
return new Response(body, {
status: upstream.status, // a 401 must stay a 401
headers: {
'content-type': upstream.headers.get('content-type') ?? 'application/json',
'cache-control': 'no-store',
},
})
},
},
},
})
```
```ts
// Browser: read the current user; null = signed out (don't crash the page)
export async function fetchCurrentUser() {
const res = await fetch('/api/user', { credentials: 'include' })
if (res.status === 401) return null
if (!res.ok) throw new Error('Failed to load user')
return res.json()
}
// Login / logout are browser NAVIGATION, not SDK calls:
const login = (ret = location.pathname + location.search) =>
{ location.href = `/__auth/login?return=${encodeURIComponent(ret)}` }
const logout = (ret = '/') =>
{ location.href = `/__auth/logout?return=${encodeURIComponent(ret)}` }
```
A signed-out surface renders a sign-in action that calls `login()`; a server-side
re-check guards every SDK operation. Do not invent an email/password form.
## 2. Create the SDK clients (server-side)
One adapter satisfies jobs + media + profile. Import the adapter from
`@higgsfield/fnf/workflow-platform` (NOT `/adapters` — that subpath is gone):
```ts
import { createJobClient } from '@higgsfield/fnf/client'
import { createMediaClient } from '@higgsfield/fnf/media'
import { createProfileClient } from '@higgsfield/fnf/profile'
import { createWorkflowPlatformAdapter } from '@higgsfield/fnf/workflow-platform'
import { gptImage2 } from '@higgsfield/fnf/jobs' // register ONLY the jobs you use
const adapter = createWorkflowPlatformAdapter({ baseUrl: 'https://fnf.internal' })
const jobs = createJobClient({ adapter, jobs: [gptImage2] })
const media = createMediaClient({ mediaAdapter: adapter })
const profile = createProfileClient({ profileAdapter: adapter })
```
The `jobs: [...]` registry is what gives `model`/`settings` their TypeScript
narrowing. Catalog of models in `app/packages/fnf/ai/AGENTS.md` ("Current SDK
catalog") — e.g. `gptImage2`→`gpt_image_2`, `nanoBanana2`→`nano_banana_2`,
`seedance2_0`, `kling3_0`.
## 3. Submit (with the confirmation gate) → poll → read the URL
The `confirm` gate is passed to the **adapter factory**; it runs once per submit,
after validation, before any network call. Rejecting is a user choice, not a
failure (typed `confirmation_rejected`).
```ts
const adapter = createWorkflowPlatformAdapter({
baseUrl: 'https://fnf.internal',
confirm: async ({ jobSetType }) => {
// UI host opens its cost-preview modal here and resolves; reject to cancel.
if (!(await openConfirmModal(jobSetType))) throw new Error('declined')
},
})
const { generations } = await jobs.submit({
model: 'gpt_image_2',
prompt: { instruction: userPrompt },
settings: { aspectRatio: '1:1', quality: 'high', resolution: '2k', batchSize: 1 },
})
const [done] = await jobs.wait(generations, {
signal,
onProgress: g => console.log(g.status),
})
```
Prefer `safeSubmit` across a client/server or iframe boundary and branch on
`error.code` (survives JSON; `instanceof` does not):
```ts
const r = await jobs.safeSubmit(input)
if (!r.ok) {
if (r.error.code === 'out_of_credits') return showBillingUI()
if (r.error.code === 'confirmation_rejected') return // declined — not an error
throw r.error
}
```
Common codes: `out_of_credits`, `rate_limit`, `prompt_nsfw`, `ip_detected`,
`job_failed`, `timeout`, `validation`, `confirmation_rejected`
(`app/packages/fnf/ai/AGENTS.md`, "Errors and boundaries").
## 4. Render the result
`generation.results` is a single object `{ rawUrl, minUrl?, thumbnailUrl? }`,
present only once completed. Do NOT hand-roll URL selection — use the selectors:
```ts
import { getPreviewUrl, getRawUrl, getJobPhase } from '@higgsfield/fnf/client'
getPreviewUrl(done) // grids/cards — precedence minUrl → thumbnailUrl → rawUrl
getRawUrl(done) // full quality
getJobPhase(done) // 'progress' | 'completed' | 'failed' — drives state UI
```
In-app, compose result cards from `app/src/lib/higgsfield-generation-results.ts`
(`selectGenerationMedia(generation)` returns a resolved `image | video | empty`
union). A completed generation with no `rawUrl` must show an explicit
"preview unavailable" state with refresh — never a blank card.
Carry media geometry across the server/client boundary as part of the flat DTO:
prefer result `width`/`height` when available, otherwise retain the canonical
submitted `aspectRatio`. Use that geometry for both the pending skeleton and the
ready card. Preview optimization may change encoding, never composition: preserve
the whole result and do not hardcode `square`, `16:9`, or `object-cover` for a
generation gallery.
## 5. React wiring (fnf-react hooks)
- **`useGenerationRun(client, opts?)`** — drives one submission end-to-end.
Returns `{ status, generations, isRunning, error, start(input), abort(), reset() }`;
`start` never rejects (errors land in `error`). `client` must be referentially
stable. Best for the "generate" button flow.
- **Live read of one job:** `useQuery(generationQueryOptions(jobClient, jobId, { scopeKey }))`
— polls every 5s while non-terminal, stops when terminal. (There is no
`useGeneration` hook.)
- **`useAttachments(media, opts?)`** — upload controller for input images/media:
`{ items, refs, isUploading, add(files, { role }), settled(), remove(key) }`.
`await controller.settled()` yields submit-ready `MediaRef[]`.
## 6. Media upload — binary never goes through JSON
Never pass `File`/`Blob`/`ArrayBuffer`/base64 through `createServerFn` input or a
JSON body. Use multipart to an app-local route:
```txt
browser File → POST multipart /api/media/upload → media.upload(...) → MediaRef
MediaRef + prompt/settings → submit
```
Server reads `await file.arrayBuffer()`, calls `media.upload({ source: bytes, … })`,
returns only the `MediaRef`. Detail in `references/fnf-sdk.md` ("Media recipes").
## 7. Server functions + bindings
App data operations run through TanStack server functions or `/api/*` routes:
```ts
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'
export const generateMeme = createServerFn({ method: 'POST' })
.inputValidator(z.object({ prompt: z.string().min(1) }))
.handler(async ({ data }) => {
// re-check auth, build the server-side adapter, submit, persist to D1…
})
```
Cloudflare bindings (D1 `DB`, R2 `STORAGE`, KV `KV`) are read server-side via
`app/src/lib/bindings.server.ts` (`import { env } from "cloudflare:workers"`), and
only exist if declared in `app/app.manifest.json` (`"db": true`, etc.). Guard
before use. Runtime detail in `references/runtime-and-infra.md`.
**Return flat, fully-typed DTOs — never a raw SDK object.** TanStack Start
compile-time-checks that every server-function return value is serializable, and
SDK types like `Generation` carry `input.settings: Record<string, unknown>` —
`unknown` fails that check and breaks the build. Map the SDK object to a small
DTO with only the fields the client needs (`{ id, status, previewUrl, … }`),
typed explicitly. Same rule for anything you hand back across the server/client
boundary: no `unknown`, no class instances, plain JSON-shaped data.
**`import type` React types.** In module files there is no `React` UMD global,
so bare `React.ReactNode` / `React.CSSProperties` fail to compile — write
`import type { ReactNode, CSSProperties } from "react"` and use them unqualified.
## 8. The Quanta components you'll actually use
All dark, composed — never restyled. Props reference:
`app/packages/quanta/ai/AGENTS.md` ("Component API reference"). The core set:
```tsx
import { Button } from '@higgsfield/quanta/button'
import { Textarea } from '@higgsfield/quanta/textarea'
import { Loader } from '@higgsfield/quanta/loader'
import { toast } from '@higgsfield/quanta/sonner'
// The generation CTA is ALWAYS marketingPrimary with the credit cost inside:
<Button variant="marketingPrimary">
Generate <SparklesIcon /> {credits}
</Button>
// Prompt field: helper/error props are `description`/`error` (NOT helperText/errorText)
<Textarea label="Prompt" description="Describe your meme" value={v} onChange={e => setV(e.target.value)} />
// Mixed-ratio result tile: keep the submitted/result frame and the whole media.
// Put this app-local component in app/src/components/; it uses Quanta tokens.
const cssRatio = (ratio: string) => {
const [width, height] = ratio.split(':').map(Number)
return width > 0 && height > 0 ? `${width} / ${height}` : '1 / 1'
}
<div
className="grid w-full place-items-center overflow-hidden rounded-lg bg-q-background-secondary"
style={{ aspectRatio: cssRatio(result.aspectRatio) }}
>
{phase === 'completed' && result.previewUrl
? <img
className="block h-full w-full object-contain"
src={result.previewUrl}
alt={prompt}
/>
: <Loader variant="stars" />}
</div>
```
Reach for: `Button` (actions/CTA), `Textarea`/`Input` (prompt/fields),
`Select`/`Dropdown` (options), `Slider`/`Switch`/`Chip` (settings), `Media`
(canonical-ratio media), an app-local ratio-preserving frame for mixed generation
results, `Card`/`Glass` (surfaces), `Grid`/`VirtualGrid` (feeds),
`Loader`/`Progress` (busy), `Modal`/`Vault` (dialogs/sheets), `toast` (notices).
For a gap, build a small component from Quanta primitives in
`app/src/components/` — never a third-party UI library, never restyle Quanta.
---
**Then follow `app-flow.md` steps 6–8:** deploy → cover + metadata (generate the
cover per `references/app-cover.md`) → publish only when asked.
SHA-256: e11a6dd3019de0be02cf0b3131bab0a0cd060d3055eba568de24d1bf227b133a