Skip to main content

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

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:
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:
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:
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

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

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 CodeWhat the Framework Does
@field property = valueSyncs 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