> ## Documentation Index
> Fetch the complete documentation index at: https://docs.idyllic.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart

> Build a streaming document improvement application in 5 minutes

# Quickstart

This guide walks you through building a document improvement application. By the end, you'll have an app where users type text, click a button, and watch AI-improved prose stream in word by word. You'll also see real-time synchronization in action by opening multiple browser tabs that share the same state.

## Prerequisites

* Node.js 18 or later
* An API key from an LLM provider (OpenAI, Anthropic, etc.)

## Create the Project

```bash theme={null}
npx create-idyllic my-app
cd my-app
```

This scaffolds a project with the following structure:

```
my-app/
├── systems/
│   └── DocImprover.ts    # Backend logic
├── components/
│   └── Editor.tsx        # React component
├── app/
│   └── page.tsx          # Next.js page
├── .dev.vars             # Environment variables (gitignored)
└── package.json
```

The `systems/` directory is where you write backend code. Each file exports a class that extends `AgenticSystem`, which the framework transforms into a Durable Object with WebSocket capabilities. The `components/` directory contains React components that connect to these systems using the `useSystem` hook.

## Configure Your API Key

Open `.dev.vars` and add your LLM provider's API key:

```bash theme={null}
OPENAI_API_KEY=sk-...
```

The `.dev.vars` file is automatically gitignored, keeping your credentials on your local machine during development. For production, you configure environment variables through Cloudflare's dashboard.

## The System

Open `systems/DocImprover.ts`. This file contains the backend logic for your application:

```typescript theme={null}
import { AgenticSystem, field, action, stream } from 'idyllic';

export default class DocImprover extends AgenticSystem {
  @field content = '';
  @field improved = stream<string>('');
  @field status: 'idle' | 'improving' = 'idle';

  @action()
  async setContent(text: string) {
    this.content = text;
  }

  @action()
  async improve() {
    this.status = 'improving';
    this.improved.reset();

    for await (const chunk of ai.stream(`Improve this text:\n\n${this.content}`)) {
      this.improved.append(chunk);
    }

    this.improved.complete();
    this.status = 'idle';
  }
}
```

The class defines three state properties that serve different purposes. The `content` property stores whatever text the user types into the input field. The `improved` property uses `stream<string>()`, which creates a streaming primitive that can accumulate AI output token by token and broadcast each piece to connected clients. The `status` property tracks whether the system is processing a request, allowing the frontend to disable the button and show loading indicators.

Two methods are decorated with `@action()`, which marks them as callable from the client. The `setContent` method updates the input text whenever the user types. The `improve` method does the interesting work: it sets status to indicate processing has begun, clears any previous output with `reset()`, then iterates through the LLM's response. Each time `append(chunk)` is called, the framework broadcasts that chunk to all connected clients, creating the word-by-word streaming effect.

## The Component

Open `components/Editor.tsx`. This React component connects to the system and renders the UI:

```tsx theme={null}
import { useSystem } from '@idyllic/react';
import type { DocImprover } from '../systems/DocImprover';

export function Editor() {
  const { content, improved, status, setContent, improve } = useSystem<DocImprover>();

  return (
    <div>
      <textarea
        value={content}
        onChange={e => setContent(e.target.value)}
        placeholder="Enter your rough text here..."
      />
      <button onClick={improve} disabled={status === 'improving'}>
        {status === 'improving' ? 'Improving...' : 'Improve'}
      </button>
      <div>
        {improved.current}
        {improved.status === 'streaming' && <span className="cursor">|</span>}
      </div>
    </div>
  );
}
```

The `useSystem` hook establishes a WebSocket connection to the backend system and returns both state and actions in a single object. When you call `setContent(e.target.value)` in the onChange handler, the framework serializes that value and sends it to the server. The server's `setContent` method runs, updates `this.content`, and that update broadcasts back to all connected clients.

The hook knows the correct types because you pass `DocImprover` as a type parameter. This means your IDE provides autocomplete for state properties and action methods, and TypeScript catches type errors at compile time rather than runtime.

## Run the Application

```bash theme={null}
npx idyllic dev
```

Open `http://localhost:5173` in your browser. Type some rough text into the textarea and click **Improve**. You should see the improved text begin streaming into the output area token by token within a second or two. A blinking cursor appears during generation to indicate the model is still working.

## Test Multi-Client Synchronization

Idyllic synchronizes state across multiple clients automatically. To see this in action:

1. Open a second browser tab at `http://localhost:5173`
2. Both tabs display identical content because they connect to the same system instance
3. Type in one tab and watch the other update immediately
4. Click Improve in one tab and both tabs stream the response simultaneously

Both browser tabs establish WebSocket connections to the same Durable Object on the server. When you call an action in one tab, it modifies server-side state, and that modification broadcasts to all connected clients. You didn't write WebSocket handlers, implement pub/sub, or configure synchronization logic—the framework handles all of that based on your state definitions.

## Deploy to Production

```bash theme={null}
npx idyllic deploy
```

This command bundles your code and uploads it to Cloudflare's global edge network. Your application runs in over 300 locations worldwide, with cold starts around 10 milliseconds. State persists automatically through Cloudflare Durable Objects. There are no containers to configure and no databases to provision.

***

## Summary

| Your Code                 | What the Framework Does                                          |
| ------------------------- | ---------------------------------------------------------------- |
| `@field property = value` | Syncs to all clients over WebSocket, persists to durable storage |
| `stream<T>()`             | Broadcasts incremental updates as they arrive                    |
| `@action()`               | Exposes the method as a typed RPC endpoint                       |
| `useSystem<T>()`          | Connects to the system with full TypeScript types                |

## Next Steps

<CardGroup cols={2}>
  <Card title="AgenticSystem" icon="box" href="/concepts/system">
    Full API reference for systems
  </Card>

  <Card title="Streaming" icon="wave-pulse" href="/concepts/streaming">
    Advanced streaming patterns
  </Card>

  <Card title="Examples" icon="code" href="/examples/document-improver">
    More complete examples
  </Card>

  <Card title="How It Works" icon="gears" href="/concepts/how-it-works">
    Architecture and internals
  </Card>
</CardGroup>
