Realtime Quickstart
Upstash Realtime lets you emit events from your workflow and subscribe to them in real-time on your frontend.
How It Works
Upstash Realtime is powered by Upstash Redis and provides a clean, 100% type-safe API for publishing and subscribing to events:
- Your frontend can subscribe to events
- When you emit an event, it's instantly delivered to live subscribers on the frontend
- You can also replay events that happened in the past
This guide shows you how to integrate Upstash Workflow with Upstash Realtime to display real-time progress updates in your frontend.
Setup
1. Install Packages
npm install @upstash/workflow @upstash/realtime @upstash/redis zod2. Configure Upstash Realtime
Create a Realtime instance in lib/realtime.ts:
import { InferRealtimeEvents, Realtime } from "@upstash/realtime";import { Redis } from "@upstash/redis";import z from "zod/v4";const redis = Redis.fromEnv();const schema = { workflow: { runFinish: z.object({}), stepFinish: z.object({ stepName: z.string(), result: z.unknown().optional(), }), },};export const realtime = new Realtime({ schema, redis });export type RealtimeEvents = InferRealtimeEvents<typeof realtime>;3. Create a Realtime Middleware
Create a custom middleware that will emit events to Realtime at lib/middleware.ts:
import { WorkflowMiddleware } from "@upstash/workflow";import { realtime } from "./realtime";export const realtimeMiddleware = new WorkflowMiddleware({ name: "realtime-events", callbacks: { afterExecution: async ({ context, stepName, result }) => { const channel = realtime.channel(context.workflowRunId); await channel.emit("workflow.stepFinish", { stepName, result, }); }, runCompleted: async ({ context }) => { const channel = realtime.channel(context.workflowRunId); await channel.emit("workflow.runFinish", {}); }, },});Key points:
- The
afterExecutioncallback is triggered after each workflow step completes - The
runCompletedcallback is triggered when the entire workflow finishes - We use
context.workflowRunIdto create a unique channel for each workflow run - Events are automatically emitted without needing to manually call emit inside your workflow steps
4. Create a Realtime Endpoint
Create an API route at app/api/realtime/route.ts to handle Realtime connections:
import { handle } from "@upstash/realtime";import { realtime } from "@/lib/realtime";export const GET = handle({ realtime });This endpoint enables Server-Sent Events (SSE) connections for real-time updates.
5. Add the Realtime Provider
Wrap your application in the RealtimeProvider by updating your root layout at app/layout.tsx:
"use client";import { RealtimeProvider } from "@upstash/realtime/client";export default function RootLayout({ children,}: { children: React.ReactNode;}) { return ( <html> <body> <RealtimeProvider>{children}</RealtimeProvider> </body> </html> );}6. Create a Typed Client Hook
Create a typed useRealtime hook at lib/realtime-client.ts:
"use client";import { createRealtime } from "@upstash/realtime/client";import type { RealtimeEvents } from "./realtime";export const { useRealtime } = createRealtime<RealtimeEvents>();Building the Workflow
1. Create the Workflow Endpoint
Create your workflow at app/api/workflow/route.ts:
import { serve } from "@upstash/workflow/nextjs";import { realtimeMiddleware } from "@/lib/middleware";type WorkflowPayload = { userId: string; action: string;};export const { POST } = serve<WorkflowPayload>( async (context) => { const { userId, action } = context.requestPayload; await context.run("validate-data", async () => { return { valid: true, userId, action }; }); await context.run("process-action", async () => { // Your business logic here return { processed: true, userId, action }; }); return { success: true, workflowRunId: context.workflowRunId }; }, { middlewares: [realtimeMiddleware], });Key points:
- Import the
realtimeMiddlewarefrom@/lib/middleware - Pass the middleware in the
middlewaresarray to theservefunction - The middleware automatically emits events after each step and when the workflow completes
2. Create a Trigger Endpoint
Create an endpoint to trigger workflows at app/api/trigger/route.ts:
import { NextRequest, NextResponse } from "next/server";import { Client } from "@upstash/workflow";export const workflowClient = new Client({ token: process.env.QSTASH_TOKEN, baseUrl: process.env.QSTASH_URL,});export async function POST(request: NextRequest) { const workflowUrl = `${request.nextUrl.origin}/api/workflow`; const { workflowRunId } = await workflowClient.trigger({ url: workflowUrl, body: { userId: "user-123", action: "process-data", }, }); return NextResponse.json({ workflowRunId });}Building the Frontend
1. Create a Custom Hook
Create a React hook to manage the Realtime subscription at hooks/useWorkflow.ts:
"use client";import { useRealtime } from "@/lib/realtime-client";import { useState, useCallback } from "react";interface WorkflowStep { stepName: string; result?: unknown;}export function useWorkflow() { const [workflowRunId, setWorkflowRunId] = useState<string | null>(null); const [steps, setSteps] = useState<WorkflowStep[]>([]); const [isRunFinished, setIsRunFinished] = useState(false); useRealtime({ enabled: Boolean(workflowRunId), channels: workflowRunId ? [workflowRunId] : [], events: ["workflow.stepFinish", "workflow.runFinish"], onData({ event, data }) { if (event === "workflow.stepFinish") { setSteps((prev) => [...prev, data]); } if (event === "workflow.runFinish") { setIsRunFinished(true); } }, }); const trigger = () => { setSteps([]); setIsRunFinished(false); const response = await fetch("/api/trigger", { method: "POST", }); const data = await response.json(); setWorkflowRunId(data.workflowRunId); }; return { trigger, workflowRunId, steps, isRunFinished, };}Key features:
- Subscribe to multiple events using the
eventsarray:["workflow.stepFinish", "workflow.runFinish"] - The hook manages both triggering the workflow and subscribing to updates
- Type-safe event handling with TypeScript
2. Use the Hook in Your Component
"use client";import { useWorkflow } from "@/hooks/useWorkflow";export default function WorkflowPage() { const { trigger, steps, isRunFinished } = useWorkflow(); return ( <div> <button onClick={trigger}>Click to Trigger Workflow</button> {isRunFinished && <p>✅ Workflow Finished!</p>} <p>Workflow Steps:</p> {steps.map((step, index) => ( <div key={index}> <strong>{step.stepName}</strong> {Boolean(step.result) && <span>: {JSON.stringify(step.result)}</span>} </div> ))} </div> );}How It All Works Together
- User triggers workflow: The frontend calls
/api/trigger, which returns aworkflowRunId - Frontend subscribes: Using the
workflowRunId, the frontend subscribes to the Realtime channel - Workflow executes: The workflow runs as a background job, emitting events at each step
- Real-time updates: As the workflow emits events, they're instantly delivered to the frontend via Server-Sent Events
Full Example
For a complete working example with all steps, error handling, and UI components, check out the Upstash Realtime example on GitHub.
Next Steps
- Learn about human-in-the-loop workflows with Realtime
- Explore Realtime features
- Check out Workflow configuration options