Documentation Index
Fetch the complete documentation index at: https://upstash.com/docs/llms.txt
Use this file to discover all available pages before exploring further.
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 zod
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
afterExecution callback is triggered after each workflow step completes
- The
runCompleted callback is triggered when the entire workflow finishes
- We use
context.workflowRunId to 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:
app/api/realtime/route.ts
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:
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
realtimeMiddleware from @/lib/middleware
- Pass the middleware in the
middlewares array to the serve function
- 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
events array: ["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 a workflowRunId
- 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