Skip to main content
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:
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 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.

4. 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>
  )
}

5. 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 { realtime } from "@/lib/realtime"

type WorkflowPayload = {
  userId: string
  action: string
}

export const { POST } = serve<WorkflowPayload>(async (context) => {
  const { userId, action } = context.requestPayload
  const workflowRunId = context.workflowRunId

  const channel = realtime.channel(workflowRunId)

  await context.run("validate-data", async () => {
    const result = { valid: true, userId, action }

    // emit step completion
    await channel.emit("workflow.stepFinish", {
      stepName: "validate-data",
      result,
    })

    return result
  })

  // emit run completion
  await context.run("run-finish", () => channel.emit("workflow.runFinish", {}))

  return { success: true, workflowRunId }
})
Key points:
  • We use realtime.channel(workflowRunId) to create a unique channel per workflow run
  • Emit events after each step completes
  • Emit events inside context.run steps to ensure that they are emitted only once
  • Events are emitted to separate event names like workflow.stepFinish and workflow.runFinish

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((data) => [...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