Fency.ai

Streaming response

One prompt. The reply streams in as it is generated.

Basic

This example sends one prompt and streams the reply as text. There is no chat thread and no multi-turn history.

Prerequisites

You need a publishable key for the React SDK and a secret key for your server routes. This example uses sessions to authenticate the client.

The example uses Clerk for user authentication. That is part of the app shell and not specific to Fency.

Server session routes

The example defines two session routes: one for stream sessions and one for agent task sessions. Each route is standalone so you can read a single file and see the full POST /v1/sessions call.

Create the stream session route at api/create-stream-session/route.ts:

import { NextResponse } from 'next/server'
import { getAuthorizedUserId } from '../../../auth'
import { sessionClientTokenSchema } from '../../sessionClientTokenSchema'

export async function POST() {
  const userId = await getAuthorizedUserId()
  if (!userId) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  const secretKey = process.env.FENCY_SECRET_KEY
  if (!secretKey) {
    throw new Error('FENCY_SECRET_KEY is not defined.')
  }

  const response = await fetch('https://api.fency.ai/v1/sessions', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${secretKey}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ createStream: {} }),
  })
  if (!response.ok) {
    throw new Error('Failed to create Fency session.')
  }
  return NextResponse.json(sessionClientTokenSchema.parse(await response.json()))
}

Create the agent task session route at api/create-agent-task-session/route.ts:

import { NextResponse } from 'next/server'
import { getAuthorizedUserId } from '../../../auth'
import { sessionClientTokenSchema } from '../../sessionClientTokenSchema'

export async function POST() {
  const userId = await getAuthorizedUserId()
  if (!userId) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  const secretKey = process.env.FENCY_SECRET_KEY
  if (!secretKey) {
    throw new Error('FENCY_SECRET_KEY is not defined.')
  }

  const response = await fetch('https://api.fency.ai/v1/sessions', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${secretKey}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      createAgentTask: {
        taskType: 'STREAMING_CHAT_COMPLETION',
        metadata: { userId },
      },
    }),
  })
  if (!response.ok) {
    throw new Error('Failed to create Fency session.')
  }
  return NextResponse.json(sessionClientTokenSchema.parse(await response.json()))
}

Client provider and hooks

Both session routes respond with a client token. Parse that response with a Zod schema in sessionClientTokenSchema.ts so a bad shape throws:

import { z } from 'zod'

export const sessionClientTokenSchema = z.object({
  clientToken: z.string(),
})

Load the Fency client and wrap your component tree with FencyProvider in page.tsx:

'use client'

import { loadFency } from '@fencyai/js'
import { FencyProvider } from '@fencyai/react'
import { Streamer } from './components/Streamer'
import { sessionClientTokenSchema } from './sessionClientTokenSchema'

const publishableKey = process.env.NEXT_PUBLIC_FENCY_PUBLISHABLE_KEY
if (!publishableKey) {
  throw new Error('NEXT_PUBLIC_FENCY_PUBLISHABLE_KEY is not defined.')
}

const fency = loadFency({
  publishableKey,
})

async function fetchCreateStreamClientToken() {
  const res = await fetch('/streaming-response/api/create-stream-session', {
    method: 'POST',
  })
  if (!res.ok) {
    throw new Error('Failed to create stream session')
  }
  const { clientToken } = sessionClientTokenSchema.parse(await res.json())
  return { clientToken }
}

export default function StreamingResponsePage() {
  return (
    <FencyProvider
      fency={fency}
      fetchCreateStreamClientToken={fetchCreateStreamClientToken}
    >
      <Streamer />
    </FencyProvider>
  )
}

Use useAgentTasks and createAgentTask to send one user message in hooks/useStreamingResponse.ts:

'use client'

import { useAgentTasks } from '@fencyai/react'
import { useState } from 'react'
import { sessionClientTokenSchema } from '../sessionClientTokenSchema'

async function fetchCreateAgentTaskClientToken() {
  const res = await fetch('/streaming-response/api/create-agent-task-session', {
    method: 'POST',
  })
  if (!res.ok) {
    throw new Error('Failed to create agent task session')
  }
  const { clientToken } = sessionClientTokenSchema.parse(await res.json())
  return { clientToken }
}

export function useStreamingResponse() {
  const [isSubmitting, setIsSubmitting] = useState(false)
  const { latest, createAgentTask } = useAgentTasks({})

  async function generate(prompt: string) {
    setIsSubmitting(true)
    try {
      const response = await createAgentTask(
        {
          type: 'StreamingChatCompletion',
          messages: [{ role: 'USER', content: prompt }],
          model: 'anthropic/claude-sonnet-4.6',
        },
        { fetchCreateAgentTaskClientToken },
      )

      if (response.type !== 'success') {
        throw new Error(response.error.message)
      }
      if (response.response.taskType !== 'StreamingChatCompletion') {
        throw new Error('Unexpected StreamingChatCompletion outcome.')
      }
      const assistant = response.response.response.messages.at(-1)
      if (assistant?.role !== 'ASSISTANT') {
        throw new Error('StreamingChatCompletion did not return an assistant message.')
      }
    } finally {
      setIsSubmitting(false)
    }
  }

  return {
    latestTask: latest,
    isSubmitting,
    generate,
  }
}

Render the prompt and the streaming reply in components/Streamer.tsx:

'use client'

import { Alert, Badge, Text, Title } from '@mantine/core'
import { AgentTaskProgress } from '@fencyai/react'
import { useStreamingResponse } from '../hooks/useStreamingResponse'
import { PromptForm } from './PromptForm'

export function Streamer() {
  const { latestTask, isSubmitting, generate } = useStreamingResponse()

  return (
    <div className="mx-auto flex w-full max-w-5xl flex-col gap-6 px-4 py-6">
      <div>
        <Badge size="sm" variant="light" color="green" mb={4}>
          Basic
        </Badge>
        <Title order={1} size="h4">
          Streaming response
        </Title>
        <Text size="sm" c="dimmed">
          One prompt. The reply streams in as it is generated.
        </Text>
      </div>

      <PromptForm isSubmitting={isSubmitting} onGenerate={generate} />

      {latestTask?.error ? (
        <Alert color="red">{latestTask.error.message}</Alert>
      ) : latestTask ? (
        <AgentTaskProgress agentTask={latestTask} />
      ) : null}
    </div>
  )
}

The AgentTaskProgress component displays the streaming text as it arrives. components/PromptForm.tsx owns the textarea and calls generate.

Running locally

Clone the repository and install dependencies:

git clone https://github.com/fencyai/fency-react-examples.git
cd fency-react-examples
npm install

Copy .env.example to .env.local and add your Fency keys:

cp .env.example .env.local

Set the keys in .env.local:

FENCY_SECRET_KEY=sk_...
NEXT_PUBLIC_FENCY_PUBLISHABLE_KEY=pk_...

Initialize Clerk authentication (provisions a development app):

npx -y clerk@latest init --keyless -y

Start the development server:

npm run dev

Open http://localhost:3000/streaming-response. Sign up from the header, then generate a reply to see tokens stream in real time.

On this page