> For the complete documentation index, see [llms.txt](https://docs.frayme.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.frayme.ai/getting-started/quickstart.md).

# Quickstart

Go from an empty Next.js app to a streaming, interactive Frayme UI in under five minutes.

## What you'll build

A page with one button. Click it, and a signup form streams in live: composed by Frayme, rendered by `@frayme/runtime`, with a submit action wired back to your code.

**Prerequisites:** Node ≥ 20.19, a Next.js project (App Router, React 19), and a Frayme API key.

{% hint style="info" %}
Get an API key from your Frayme dashboard at [app.frayme.ai](https://app.frayme.ai/api-keys). The code below is exactly what runs against the live API.
{% endhint %}

## 1. Install

{% tabs %}
{% tab title="npm" %}

```bash
npm i @frayme/api @frayme/runtime
```

{% endtab %}

{% tab title="pnpm" %}

```bash
pnpm add @frayme/api @frayme/runtime
```

{% endtab %}

{% tab title="bun" %}

```bash
bun add @frayme/api @frayme/runtime
```

{% endtab %}
{% endtabs %}

## 2. Add your API key

```bash
# .env.local
FRAYME_API_KEY=fr_live_...
```

Never expose this in the browser. The next step keeps it server-side.

## 3. Create the server route

The browser never holds your key. Instead, a route in your app checks the caller, adds the key and forwards compose calls to Frayme. `createFraymeHandler` is that route, ready-made:

```ts
// app/api/frayme/[...path]/route.ts
import { createFraymeHandler } from '@frayme/api/server';

export const POST = createFraymeHandler({
  // Allows local development only. Replace it with your own session check
  // before you deploy: this route spends your quota.
  authorize: () => process.env.NODE_ENV === 'development',
});
```

This is [keyless proxy mode](/getting-started/authentication.md#keyless-proxy-mode): the `@frayme/api` client in the browser points at `/api/frayme`, and only this route knows the key. The handler reads `FRAYME_API_KEY`, accepts only compose fields, streams the answer straight through, and keeps only the actions a request declares, which is why the page below declares its one action. See [Server handler](/guides/server-handler.md) for every option.

## 4. Compose and render

```tsx
// app/page.tsx
'use client';

import { useMemo } from 'react';
import Frayme from '@frayme/api';
import { FraymeRenderer, useFraymeCompose } from '@frayme/runtime/react';
import '@frayme/runtime/styles.css';

export default function Page() {
  // Keyless proxy mode: no API key in the browser.
  const client = useMemo(
    () => new Frayme({ apiKey: null, baseURL: '/api/frayme' }),
    [],
  );
  const { compose, spec, status, restartKey } = useFraymeCompose(client);
  const busy = status === 'streaming' || status === 'restarting';

  return (
    <main style={{ maxWidth: 720, margin: '0 auto', padding: 24 }}>
      <button
        disabled={busy}
        onClick={() =>
          compose({
            prompt:
              'A signup form: full name, work email, and team size, ' +
              'with a submit button bound to a create_account action',
            signals: { data_shape: ['form'], density: 'compact' },
            // The server route only keeps declared actions, so declare this one.
            actions: [{ name: 'create_account', role: 'Create account', required: true }],
          })
        }
      >
        {busy ? 'Composing…' : 'Compose a signup form'}
      </button>

      <FraymeRenderer
        spec={spec}
        mode="progressive" // render streaming snapshots as they arrive
        restartKey={restartKey} // resets client state if the API restarts an attempt
        loading={busy}
        onDynamicAction={(event) => {
          // The one round-trip: a spec-declared action reaching your code.
          console.log('action:', event.action, 'state:', event.state);
        }}
      />
    </main>
  );
}
```

Three things are doing the heavy lifting:

* **`useFraymeCompose`** streams the composition and keeps a live `spec` snapshot. If Frayme restarts a weak attempt on a stronger model, the hook clears the snapshot and bumps `restartKey` for you.
* **`<FraymeRenderer mode="progressive">`** renders partial specs as they stream: the UI builds up element by element.
* **`onDynamicAction`** receives only spec-declared actions. Typing in the fields never reaches your code: that state stays local in the renderer.

## 5. Run it

```bash
npm run dev
```

Open `http://localhost:3000` and click the button.

**You should see:** the form streams in progressively over a few seconds. A card appears first, then a heading, then the name, email, and team-size fields, then the submit button. Typing in the inputs is instant (no network traffic: that's local state). When you fill the form and press submit, your browser console logs `action: create_account` with a `state` object containing exactly what you typed.

## Where to go next

* [Your first generation](/getting-started/first-generation.md): what actually went over the wire just now
* [Authentication](/getting-started/authentication.md): key types, the browser guard, and proxy hardening
* The Sandbox in your dashboard ([app.frayme.ai/sandbox](https://app.frayme.ai/sandbox)): configure an agent over your own files (up to five) and export it as a runnable Next.js app
* Pass real `data` and declared `actions` to ground the UI in your domain. See the API reference for the full compose request
