DEV Community

Cover image for Building Real-Time Processing Status with Supabase Realtime
Bishoy Bishai
Bishoy Bishai

Posted on • Originally published at bishoy-bishai.github.io

Building Real-Time Processing Status with Supabase Realtime

Elevating UX: Real-Time Processing Status with Supabase Realtime and React

We've all been there: staring at a spinning loader, wondering if a crucial background task in our application is actually making progress or if it's silently crashed. Whether it's a large file upload, a complex data migration, or generating an elaborate report, the absence of real-time feedback is a major source of user frustration and a silent killer of trust.

In my experience, providing transparent, real-time processing status isn't just a "nice-to-have"; it's a fundamental aspect of a polished user experience, especially in professional applications. It transforms a black box operation into an engaging, comprehensible process. And here's the thing: with tools like Supabase Realtime, achieving this level of responsiveness is surprisingly straightforward, especially when paired with a reactive frontend like React.

Why Real-Time Status Matters (Beyond Just "Looking Good")

Think about it from your user's perspective. If they initiate an action that takes more than a few seconds, their brain immediately starts asking questions: "Is it working?", "Did I click correctly?", "How long will this take?", "Should I refresh?". This uncertainty leads to impatience, accidental re-submissions, and ultimately, a poor perception of your application's reliability.

Real-time status updates address this head-on:

  • Reduces perceived wait times: Even if the actual processing time is the same, knowing something is happening makes the wait feel shorter.
  • Builds trust: Users feel informed and in control.
  • Enables error handling: Immediate feedback on failures allows users to take corrective action sooner.
  • Enhances engagement: Dynamic UIs are simply more pleasant to interact with.

I've found this to be particularly critical in internal tools or enterprise applications where users are performing business-critical operations. No one wants to refresh a dashboard every 30 seconds to see if their batch job finished!

Supabase Realtime: Your Serverless Pub/Sub Backbone

Supabase Realtime is a fantastic, lightweight pub/sub server built on Elixir and PostgreSQL. While it's often highlighted for database change subscriptions, its "Broadcast" feature is exactly what we need for custom real-time status updates. It allows any client (or server) to send arbitrary JSON messages to a specific channel, and any client subscribed to that channel will receive them instantly. It's like having a dedicated radio station for each long-running process.

Let's Build It: A React Component for Processing Status

Imagine we have a backend service that processes an uploaded file. It goes through stages: uploading, validating, processing, completed, or failed. We want our React app to show this dynamically.

Step 1: Initialize Supabase Client

First, ensure you have your Supabase client initialized.

// src/supabaseClient.ts
import { createClient } from '@supabase/supabase-js';

const supabaseUrl = process.env.REACT_APP_SUPABASE_URL!;
const supabaseAnonKey = process.env.REACT_APP_SUPABASE_ANON_KEY!;

export const supabase = createClient(supabaseUrl, supabaseAnonKey);
Enter fullscreen mode Exit fullscreen mode

Step 2: The React Component

Now, let's create a React component that subscribes to a specific channel. We'll simulate a file processing workflow.

// src/components/FileProcessorStatus.tsx
import React, { useEffect, useState } from 'react';
import { supabase } from '../supabaseClient';
import { RealtimeChannel } from '@supabase/supabase-js';

interface ProcessingStatus {
  step: 'uploading' | 'validating' | 'processing' | 'completed' | 'failed';
  progress?: number; // For steps like 'processing'
  message?: string;
  fileId: string; // To identify which file's status this is
}

interface FileProcessorStatusProps {
  fileId: string;
}

const FileProcessorStatus: React.FC<FileProcessorStatusProps> = ({ fileId }) => {
  const [status, setStatus] = useState<ProcessingStatus | null>(null);
  const [channel, setChannel] = useState<RealtimeChannel | null>(null);

  useEffect(() => {
    // Define a unique channel name for this file's processing
    // In a real app, ensure this channel name is securely generated and difficult to guess.
    const channelName = `file_processing_${fileId}`;
    console.log(`Subscribing to channel: ${channelName}`);

    const newChannel = supabase.channel(channelName);

    newChannel.on('broadcast', { event: 'status_update' }, (payload) => {
      console.log('Received status update:', payload.payload);
      setStatus(payload.payload as ProcessingStatus);
    }).subscribe(async (status) => {
      if (status === 'SUBSCRIBED') {
        console.log(`Subscribed to ${channelName}`);
        // Optionally, send an initial message or fetch current status from API
      } else if (status === 'CHANNEL_ERROR') {
        console.error(`Error subscribing to channel ${channelName}`);
      } else if (status === 'CLOSED') {
        console.log(`Channel ${channelName} closed.`);
      }
    });

    setChannel(newChannel);

    return () => {
      // Clean up subscription when component unmounts or fileId changes
      console.log(`Unsubscribing from channel: ${channelName}`);
      supabase.removeChannel(newChannel);
    };
  }, [fileId]); // Re-subscribe if fileId changes

  if (!status) {
    return <p>Awaiting processing status for file: {fileId}...</p>;
  }

  const getStatusColor = (step: ProcessingStatus['step']) => {
    switch (step) {
      case 'completed': return 'text-green-500';
      case 'failed': return 'text-red-500';
      case 'validating':
      case 'uploading':
      case 'processing': return 'text-blue-500';
      default: return 'text-gray-500';
    }
  };

  return (
    <div className="p-4 border rounded-lg shadow-md bg-white">
      <h3 className="text-lg font-semibold mb-2">File Processing Status: <span className="text-gray-700">{fileId}</span></h3>
      <p className={`font-medium ${getStatusColor(status.step)}`}>
        Current Step: <span className="capitalize">{status.step.replace(/_/g, ' ')}</span>
      </p>
      {status.progress !== undefined && status.step === 'processing' && (
        <div className="w-full bg-gray-200 rounded-full h-2.5 my-2">
          <div
            className="bg-blue-600 h-2.5 rounded-full"
            style={{ width: `${status.progress}%` }}
          ></div>
          <span className="text-sm text-gray-600 ml-2">{status.progress}%</span>
        </div>
      )}
      {status.message && (
        <p className="text-sm text-gray-600 mt-1">Message: {status.message}</p>
      )}
      {(status.step === 'completed' || status.step === 'failed') && (
        <p className="text-sm text-gray-500 mt-2">
          {status.step === 'completed' ? 'Processing finished successfully!' : 'Processing failed.'}
        </p>
      )}
    </div>
  );
};

export default FileProcessorStatus;
Enter fullscreen mode Exit fullscreen mode

Step 3: Broadcasting from Your Backend (Conceptual)

On your backend (e.g., a Node.js server, a Python worker, a Supabase Edge Function), you'd use the Supabase client to broadcast messages to the same channel.

// Example: Node.js / Supabase Edge Function (conceptual)
import { createClient } from '@supabase/supabase-js';

const supabaseAdmin = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY! // Use service role key for backend broadcasts
);

async function sendStatusUpdate(fileId: string, status: ProcessingStatus) {
  const channelName = `file_processing_${fileId}`;
  await supabaseAdmin.channel(channelName).send({
    type: 'broadcast',
    event: 'status_update',
    payload: status,
  });
  console.log(`Broadcasted status for ${fileId}: ${status.step}`);
}

// --- Inside your file processing logic ---
async function processFile(fileId: string) {
  // Initial state
  await sendStatusUpdate(fileId, { fileId, step: 'uploading', message: 'File received...' });

  // Simulate validation
  await new Promise(resolve => setTimeout(resolve, 1500));
  await sendStatusUpdate(fileId, { fileId, step: 'validating', message: 'Checking file integrity...' });

  // Simulate actual processing with progress
  for (let i = 0; i <= 100; i += 10) {
    await new Promise(resolve => setTimeout(resolve, 500));
    await sendStatusUpdate(fileId, { fileId, step: 'processing', progress: i, message: `Processing chunk ${i/10}...` });
  }

  // Simulate completion or failure
  const success = Math.random() > 0.2; // 80% chance of success
  if (success) {
    await sendStatusUpdate(fileId, { fileId, step: 'completed', message: 'File processed successfully!' });
  } else {
    await sendStatusUpdate(fileId, { fileId, step: 'failed', message: 'Error: Invalid file format.' });
  }
}

// To trigger this (e.g., from an API endpoint):
// processFile('unique-file-id-123');
Enter fullscreen mode Exit fullscreen mode

Real-World Insights and Lessons Learned

  1. Channel Naming Strategy: For dynamic, per-process status, a unique channel name is crucial. Using a UUID or a unique identifier for the specific operation (like file_processing_UUID) ensures that only relevant clients receive updates. Be mindful of potential channel name collisions if not handled carefully.

  2. Security and Authorization: By default, Supabase Realtime's Broadcast channels are public. This is fine for some use cases (e.g., publicly visible progress). However, for sensitive data, you must implement authorization.

    • Option 1: Private Channels (Postgres RLS): If your status updates are directly tied to a database row, using Supabase's built-in RLS with supabase.from('your_table').on(...) can secure the data. This isn't for custom broadcast events, though.
    • Option 2: Token-based Channels: For broadcast events, you'll likely need to implement your own authorization. When a user initiates a process, your backend generates a unique channel_id and a temporary, signed token for that user. The client then subscribes to channel_id using this token. Your backend's broadcast logic should also be authorized to send to that channel. Supabase Realtime's channel method takes an optional config object which could include an accessToken for authenticated channels, though this is usually for Postgres changes, not direct broadcast. For pure broadcast, often the channel name itself acts as a "key" and is unique/hard to guess. For enterprise, consider creating an API endpoint that validates the user's access before telling them which fileId channel to subscribe to.
  3. Graceful Disconnections: Supabase Realtime handles reconnections automatically, which is a huge win. Your useEffect cleanup ensures old subscriptions are removed. However, consider what happens if a user navigates away and comes back. Do you show the last known status? You might need to make an API call upon initial subscription to fetch the current state if the process started before the client connected.

  4. Beyond Simple Status: Don't limit yourself to just step and progress. Use the payload to send rich data: error details, estimated time remaining, links to results, or even user-specific messages. This flexibility is incredibly powerful.

  5. Performance: Supabase Realtime is highly performant. But be mindful of how much data you're broadcasting and how frequently. Sending updates every few milliseconds for thousands of concurrent users might be overkill. For most status updates, a few updates per second or per major milestone are more than enough.

Pitfalls to Avoid

  • Forgetting to Unsubscribe: Not cleaning up your useEffect subscriptions can lead to memory leaks and unexpected behavior, especially in SPAs where components mount and unmount frequently.
  • Publicizing Sensitive Channel Names: If your channel names contain sensitive information or are easily guessable, an attacker could potentially subscribe and receive private status updates. Always assume broadcast channels are public unless you implement custom authentication.
  • Over-broadcasting: Sending too many messages too quickly can overwhelm clients or even hit rate limits (though Supabase handles a lot, it's good practice). Consolidate updates where possible.
  • Assuming Sequential Delivery: While Realtime is fast, networking can be unpredictable. Don't build logic that absolutely relies on messages arriving in the exact order they were sent if there's a chance of network jitters. Include timestamps or sequence numbers in your payload if strict ordering is critical.

Wrapping Up

Supabase Realtime, especially its Broadcast feature, is an incredibly potent tool for enhancing the user experience of any application that deals with asynchronous operations. By providing instant, transparent feedback, you don't just "inform" users; you empower them, build trust, and ultimately create a much more enjoyable and reliable product. It's a small change in implementation that delivers an outsized impact on user perception. Give it a shot on your next project – you'll be amazed at the difference it makes.


✨ Let's keep the conversation going!

If you found this interesting, I'd love for you to check out more of my work or just drop in to say hello.

✍️ Read more on my blog: bishoy-bishai.github.io

Let's chat on LinkedIn: linkedin.com/in/bishoybishai


Top comments (0)