Realtime

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 zod

2. Configure Upstash Realtime

Create a Realtime instance in lib/realtime.ts:

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:

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:

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:

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:

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

  1. User triggers workflow: The frontend calls /api/trigger, which returns a workflowRunId
  2. Frontend subscribes: Using the workflowRunId, the frontend subscribes to the Realtime channel
  3. Workflow executes: The workflow runs as a background job, emitting events at each step
  4. 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

Loading search…