# Real-time processing dashboard with Supabase and Ittybit

Build a live media processing dashboard using Supabase Realtime and Ittybit webhooks

Submit media processing jobs and see status updates appear instantly. Instead of polling, Supabase Realtime pushes row changes to every connected client the moment an Ittybit webhook updates the database.

## Architecture

1. Frontend inserts a job row into a `jobs` table
2. A database webhook triggers an Edge Function that dispatches an Ittybit task
3. Ittybit processes the media and POSTs a webhook on completion
4. A second Edge Function receives the callback and updates the job row
5. Supabase Realtime pushes the row change to all subscribed clients
6. The dashboard UI updates live without any refresh or polling

## Create the jobs table

```sql
create table public.jobs (
  id uuid primary key default gen_random_uuid(),
  input_url text not null,
  task_id text,
  status text default 'pending',
  output_url text,
  error text,
  created_at timestamptz default now(),
  updated_at timestamptz default now()
);

-- Enable Realtime for this table
alter publication supabase_realtime add table public.jobs;
```

## Edge Function: dispatch Ittybit task

This function fires when a new row is inserted into `jobs`. It sends the media URL to Ittybit and writes the task ID back.

<CodeGroup labels={["TypeScript", "curl"]}>
```typescript
// supabase/functions/dispatch-task/index.ts

serve(async (req) => {
const payload = await req.json();
const record = payload.record;

const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
);

const res = await fetch("https://api.ittybit.com/jobs", {
method: "POST",
headers: {
Authorization: `Bearer ${Deno.env.get("ITTYBIT_API_KEY")}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
input: record.input_url,
kind: "video",
options: {
width: 1280,
format: "mp4",
quality: "high",
},
metadata: {
job_id: record.id,
},
}),
});

const task = await res.json();

await supabase
.from("jobs")
.update({
task_id: task.id,
status: "processing",
updated_at: new Date().toISOString(),
})
.eq("id", record.id);

return new Response(JSON.stringify({ ok: true }), {
headers: { "Content-Type": "application/json" },
});
});

````

```bash
# Test by creating a task directly
curl -X POST https://api.ittybit.com/jobs \
  -H "Authorization: Bearer $ITTYBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input": "https://example.com/uploads/video.mp4",
    "kind": "video",
    "options": {
      "width": 1280,
      "format": "mp4",
      "quality": "high"
    },
    "metadata": {
      "job_id": "your-uuid-here"
    }
  }'
````

</CodeGroup>

Wire this up in the Supabase Dashboard under **Database > Webhooks**:

- **Table:** `public.jobs`
- **Events:** `INSERT`
- **Type:** Supabase Edge Function
- **Function:** `dispatch-task`

## Edge Function: receive Ittybit webhook

When Ittybit finishes processing, it POSTs to this function. The row update triggers a Realtime broadcast to all subscribers.

<CodeGroup labels={["TypeScript", "curl"]}>
```typescript
// supabase/functions/ittybit-callback/index.ts

serve(async (req) => {
const payload = await req.json();

const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
);

const jobId = payload.metadata?.job_id;
if (!jobId) {
return new Response("Missing job_id in metadata", { status: 400 });
}

if (payload.status === "completed") {
await supabase
.from("jobs")
.update({
status: "completed",
output_url: payload.output?.url ?? null,
updated_at: new Date().toISOString(),
})
.eq("id", jobId);
} else {
await supabase
.from("jobs")
.update({
status: "failed",
error: payload.error?.message ?? "Unknown error",
updated_at: new Date().toISOString(),
})
.eq("id", jobId);
}

return new Response(JSON.stringify({ ok: true }), {
headers: { "Content-Type": "application/json" },
});
});

````

```bash
# Register your webhook endpoint in Ittybit
curl -X POST https://api.ittybit.com/webhooks \
  -H "Authorization: Bearer $ITTYBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-project.supabase.co/functions/v1/ittybit-callback",
    "events": ["job.succeeded", "job.failed"]
  }'
````

</CodeGroup>

## Deploy and configure

```bash
supabase functions deploy dispatch-task
supabase functions deploy ittybit-callback
supabase secrets set ITTYBIT_API_KEY=your_ittybit_api_key
```

`SUPABASE_URL` and `SUPABASE_SERVICE_ROLE_KEY` are injected automatically into Edge Functions.

## Subscribe to Realtime changes

This is where the dashboard comes alive. Subscribe to the `jobs` table and update your UI state whenever a row changes.

```typescript

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);

// Subscribe to all changes on the jobs table
const channel = supabase
  .channel('jobs-updates')
  .on(
    'postgres_changes',
    {
      event: '*',
      schema: 'public',
      table: 'jobs',
    },
    (payload) => {
      console.log('Change received:', payload);
      // payload.new contains the updated row
      // payload.eventType is INSERT | UPDATE | DELETE
    },
  )
  .subscribe();
```

When the webhook Edge Function updates a job row from `processing` to `completed`, every connected client receives that change in real time.

## React dashboard component

Wire the subscription into a component that displays job status live.

```tsx

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);

type Job = {
  id: string;
  input_url: string;
  task_id: string | null;
  status: string;
  output_url: string | null;
  created_at: string;
};

export function Dashboard() {
  const [jobs, setJobs] = useState<Job[]>([]);

  useEffect(() => {
    // Load existing jobs
    supabase
      .from('jobs')
      .select('*')
      .order('created_at', { ascending: false })
      .then(({ data }) => setJobs(data ?? []));

    // Subscribe to live changes
    const channel = supabase
      .channel('dashboard')
      .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'jobs' }, (payload) => {
        setJobs((prev) => [payload.new as Job, ...prev]);
      })
      .on('postgres_changes', { event: 'UPDATE', schema: 'public', table: 'jobs' }, (payload) => {
        setJobs((prev) =>
          prev.map((j) => (j.id === (payload.new as Job).id ? (payload.new as Job) : j)),
        );
      })
      .subscribe();

    return () => {
      supabase.removeChannel(channel);
    };
  }, []);

  async function submitJob(inputUrl: string) {
    await supabase.from('jobs').insert({ input_url: inputUrl });
    // The database webhook handles the rest
  }

  return (
    <div>
      <form
        onSubmit={(e) => {
          e.preventDefault();
          const form = e.target as HTMLFormElement;
          const url = new FormData(form).get('url') as string;
          submitJob(url);
          form.reset();
        }}
      >
        <input name="url" placeholder="Media URL" required />
        <button type="submit">Process</button>
      </form>

      <table>
        <thead>
          <tr>
            <th>Input</th>
            <th>Status</th>
            <th>Output</th>
          </tr>
        </thead>
        <tbody>
          {jobs.map((job) => (
            <tr key={job.id}>
              <td>{job.input_url}</td>
              <td>{job.status}</td>
              <td>{job.output_url ? <a href={job.output_url}>Download</a> : '—'}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}
```

Insert a row, and the dashboard picks it up via the `INSERT` subscription. Once Ittybit calls back and the Edge Function updates the row, the `UPDATE` subscription pushes the new status to every open tab.

## See also

- [Auto-process Supabase uploads](/guides/auto-process-supabase-uploads) -- trigger processing from Supabase Storage uploads
- [Build a user upload pipeline](/guides/build-a-user-upload-pipeline) -- multi-task processing for uploads
- [Process uploads in Next.js](/guides/process-uploads-in-nextjs) -- Server Actions and webhook callbacks