Integrate FastPix with Supabase

The FastPix Supabase integration syncs your FastPix media, live streams, and uploads to your Supabase database using webhooks and edge functions.

The @fastpix/supabase package provides a CLI that creates a fastpix schema in your database, deploys the edge functions that process webhooks, and keeps your data current automatically.

What you’ll build

After setup, your Supabase database has a fastpix schema that contains the following tables:

  • media stores your on-demand video assets and their metadata.
  • live_streams stores live stream configurations, status, and secrets.
  • uploads stores direct-upload sessions.
  • webhook_events stores the raw webhook event log for debugging and auditing.
  • sync_state stores backfill and reconcile bookkeeping.

The CLI also creates four edge functions (fastpix-webhook, fastpix-worker, fastpix-reconcile, and fastpix-backfill), a pgmq job queue, and two Supabase Cron jobs, a queue drain and a nightly reconcile, that drive the sync.

FastPix
(Media, Live Streams, Uploads)
Webhook Events
┌──────────────────────────┐
│ fastpix-webhook │
│ (Supabase Edge Function) │
└──────────────────────────┘
Stores Event in Queue
┌──────────────────────────┐
│ PGMQ Queue │
└──────────────────────────┘
Cron Job (every 10 sec)
┌──────────────────────────┐
│ fastpix-worker │
│ Processes Queue Events │
└──────────────────────────┘
Fetch Latest Resource
FastPix API
┌──────────────────────────┐
│ fastpix schema │
│ media │
│ live_streams │
│ uploads │
│ webhook_events │
│ sync_state │
└──────────────────────────┘

Prerequisites

Before you begin, make sure you have the following:

  • A FastPix account with an API token ID and token secret. To generate them, see Activate your account.
  • Access to the FastPix dashboard, so that you can create a webhook and copy its signing secret.
  • A Supabase project.
  • The Supabase CLI. Every command on this page runs it through npx, so a global install is optional.
  • Node.js version 20 or later.
  • Docker. See Start Docker.
  • A tunneling tool such as ngrok, to expose your local webhook endpoint to FastPix.

Setup overview

Work through these in order. Each one depends on the one before it.


Start Docker

Local Supabase runs as Docker containers, so Docker must be running before anything else.

  1. Install Docker Desktop (macOS and Windows) or Docker Engine (Linux).

  2. Start it. On macOS and Windows, open the app and wait for the whale icon to stop animating.

  3. Confirm it’s running:

    $docker info

If docker info prints server details, you’re ready. If it reports Cannot connect to the Docker daemon, Docker is installed but not started.

NOTE: Local Supabase starts about a dozen containers. Give Docker at least 4 GB of RAM.


Start Supabase

Initialize Supabase in your project and start it:

$npx supabase init
$npx supabase start

start prints your local credentials. To see them again at any time:

$npx supabase status -o env

Find your token ID and secret in the FastPix dashboard. You create the webhook secret in step 4, when you configure the webhook.


Initialize the FastPix integration

With Supabase running, initialize the integration:

$npx @fastpix/supabase init

This copies three migrations into supabase/migrations for the job queue and cron jobs, creates the four edge functions in supabase/functions, and sets verify_jwt in config.tomlfalse for the webhook, because FastPix signs its requests instead of sending a Supabase JWT.

It then prompts you for four values:

PromptWhat to enter
FASTPIX_TOKEN_IDPaste from the FastPix dashboard.
FASTPIX_TOKEN_SECRETPaste from the FastPix dashboard.
FASTPIX_WEBHOOK_SECRETPress enter to skip. This secret doesn’t exist yet — FastPix creates it later, when you register a webhook.
Database URLPaste the DB_URL value from the previous section. This is where the fastpix tables get created.

NOTE: If you skip the database URL, no tables are created. The rest of the setup will look fine, but nothing will ever sync. Create them later with:

$SUPABASE_DB_URL='postgresql://...' npx @fastpix/supabase migrate

Now restart Supabase so it picks up the config.toml changes and loads the new functions:

$npx supabase stop && npx supabase start

NOTE: Don’t skip the restart. Without it, the webhook endpoint returns 401 and FastPix won’t accept your URL.


Add the Vault secrets

The cron jobs call your edge functions over HTTP, and they read the function URL and service-role key from Supabase Vault so that neither value is committed in a migration.

Do this now, before you set up the webhook. Without these secrets, events arrive and sit in the queue forever — nothing drains, and no error is reported.

Open the SQL Editor at http://127.0.0.1:54323 and run:

1select vault.create_secret('http://host.docker.internal:54321/functions/v1', 'fastpix_functions_url');
2select vault.create_secret('<SERVICE_ROLE_KEY>', 'fastpix_service_role_key');

Replace <SERVICE_ROLE_KEY> with the value from npx supabase status -o env.

Use host.docker.internal rather than localhost — the URL has to be reachable from inside the Postgres container, where localhost means the container itself.


Expose your local server

FastPix delivers webhooks over the public internet, so your local Supabase needs a public address.

  1. In one terminal, expose port 54321:

    $ngrok http 54321

    Leave it running. Copy the forwarding address it prints, then add /functions/v1/fastpix-webhook to it. That’s your webhook URL:

    https://d99d3b847eb8.ngrok-free.app/functions/v1/fastpix-webhook
  2. In a second terminal, serve the edge functions:

    $npx supabase functions serve

    Leave this running too.

  3. Confirm the endpoint answers, using your own URL:

    $curl https://d99d3b847eb8.ngrok-free.app/functions/v1/fastpix-webhook

    You should get back ok. Don’t move on until you do — FastPix checks this URL next, and won’t save it otherwise.

    • A 404 means functions serve isn’t running, or your ngrok URL has changed.
    • A 401 means you skipped the restart after initializing FastPix.

Create the webhook

  1. In the FastPix dashboard, go to Org Settings > Webhooks > Create new webhook, and enter the URL from the previous section. For more information, see Set up webhooks.

  2. Copy the signing secret that FastPix shows you.

  3. Open supabase/functions/.env and paste it in:

    $FASTPIX_WEBHOOK_SECRET=your-webhook-secret

    WARNING: Copy the secret exactly as the dashboard shows it. It’s base64, and the engine decodes it before verifying signatures — a re-typed or truncated value makes every check fail silently, with no rows and no error.

  4. Restart the functions. Go to the terminal running functions serve, press Ctrl+C, and start it again:

    $npx supabase functions serve

    This isn’t optional. functions serve reads .env once at startup, so the secret you just pasted has no effect until you restart. You only need to restart this one process, not the whole Supabase stack.

NOTE: On ngrok’s free tier the URL changes every time you restart the tunnel. If that happens, update the URL in the FastPix dashboard. The signing secret stays the same.

  1. In the FastPix dashboard, go to Org Settings > Webhooks > Create new webhook and enter that URL. For more information, see Set up webhooks.

  2. Copy the signing secret from FastPix into FASTPIX_WEBHOOK_SECRET in supabase/functions/.env.


Verify that it works

Upload a media asset from the FastPix dashboard, then open the media table in the fastpix schema at http://127.0.0.1:54323. A row appears within a few seconds, and its mediaId matches the ID in FastPix.

If the row doesn’t appear, query the event log to see where processing stopped:

1select "type", "processStatus", "lastError" from fastpix.webhook_events
2order by "receivedAt" desc limit 10;
  • No rows at all — FastPix isn’t reaching your webhook, or the signature is failing. Recheck your ngrok URL and the signing secret.
  • Rows stuck at received — nothing is draining the queue. Recheck the Vault secrets.
  • Rows at failed — read lastError. It’s usually a wrong token ID or secret.

NOTE: Column names match the FastPix API, so they’re camelCase and need double quotes in SQL. Use select "mediaId" from fastpix.media, not select mediaId.


Environment variables

init writes these to supabase/functions/.env:

VariableRequiredDescription
FASTPIX_TOKEN_IDYesAPI token ID from the FastPix dashboard.
FASTPIX_TOKEN_SECRETYesAPI token secret.
FASTPIX_WEBHOOK_SECRETYesThe webhook signing secret, base64. You get it when you register the webhook.
FASTPIX_MAX_READ_CTNoHow many queued events the worker claims per run. Defaults to 7.
FASTPIX_WORKFLOWSNoAdvanced. Lets the worker fan out to additional edge functions. See the repository.

You don’t need to add SUPABASE_DB_URL — Supabase gives that to the edge functions on its own. It’s separate from the database URL init asked for, which the CLI used to create the tables.

Whenever you change this file, restart npx supabase functions serve.


Backfill existing data

If your FastPix account already contains media or live streams, backfill them:

$npx @fastpix/supabase backfill

The command prompts for your database URL and FastPix credentials, or reads them from the environment. To sync a single object type, pass it as an argument:

$npx @fastpix/supabase backfill media
$npx @fastpix/supabase backfill live_streams

Backfill checkpoints as it goes, so if it stops you can run it again and it resumes.

To heal webhooks you missed, reconcile resources active in the last few hours:

$npx @fastpix/supabase reconcile # last 24 hours
$npx @fastpix/supabase reconcile 48 # last 48 hours

Backfill programmatically

1import { FastPixSync } from "@fastpix/fp-sync-engine";
2
3const fastpixSync = new FastPixSync({
4 databaseUrl: "your-supabase-database-url",
5 fastpixTokenId: "your-fastpix-token-id",
6 fastpixTokenSecret: "your-fastpix-token-secret",
7 fastpixWebhookSecret: "", // not used by backfill or reconcile
8});
9
10await fastpixSync.syncBackfill({ object: "all" });
11
12const result = await fastpixSync.reconcileRecent({ sinceHours: 24 });
13console.log(result); // { media, liveStreams, deleted: { media, liveStreams } }
14
15await fastpixSync.close();

Deploy to production

The production sequence mirrors the local one: migrations, then Vault secrets, then functions, then the webhook.

  1. Link your project:

    $npx supabase login
    $npx supabase link --project-ref <ref>
  2. Run the migrations. db push applies the queue and cron migrations, and migrate creates the fastpix tables. Find your connection string in the dashboard under Connect.

    $npx supabase db push
    $SUPABASE_DB_URL='postgresql://...' npx @fastpix/supabase migrate

    NOTE: If your project already has migrations, db push fails with “Found local migration files to be inserted before the last migration on remote database.” The FastPix migrations are numbered 00010003, which sorts ahead of Supabase’s timestamped filenames. Re-run with npx supabase db push --include-all.

  3. Add the Vault secrets, the same as locally but with your production URL:

    1select vault.create_secret('https://<ref>.supabase.co/functions/v1', 'fastpix_functions_url');
    2select vault.create_secret('<service-role-key>', 'fastpix_service_role_key');

    If a secret already exists, update it instead:

    1select vault.update_secret(
    2 (select id from vault.secrets where name = 'fastpix_service_role_key'), '<new-key>');
  4. Set your API credentials as edge function secrets:

    $npx supabase secrets set \
    > FASTPIX_TOKEN_ID=<token-id> \
    > FASTPIX_TOKEN_SECRET=<token-secret>
  5. Push the config. This sends the verify_jwt settings — without it, the webhook returns 401.

    $npx supabase config push
  6. Deploy the functions:

    $npx supabase functions deploy
  7. Create the production webhook. After you deploy, the fastpix-webhook URL appears in the dashboard under Edge Functions. Register it in FastPix, then set the signing secret:

    $npx supabase secrets set FASTPIX_WEBHOOK_SECRET=<signing-secret>

    Secrets take effect immediately. You don’t need to redeploy.

  8. If your account has existing media, run npx @fastpix/supabase backfill against your production database.

Then secure the tables as described below.

NOTE: The edge functions open a direct, non-pooled Postgres connection, and the drain cron fires every 10 seconds. On a small instance you may approach your connection limit — if you see connection errors, point the functions at Supavisor or slow the drain in 0003_fastpix_setup_cron_job.sql.


Secure the tables

WARNING: Until you complete this step, the fastpix tables have no row-level security. live_streams holds streamKey and srtSecret, which let anyone stream into your account, and webhook_events stores raw payloads that can contain those same secrets. Never expose either table to your client.

In the SQL Editor, grant access to the service role and enable row-level security on every table:

1grant usage on schema fastpix to service_role;
2grant all on all tables in schema fastpix to service_role;
3alter default privileges in schema fastpix grant all on tables to service_role;
4
5alter table fastpix.webhook_events enable row level security;
6alter table fastpix.media enable row level security;
7alter table fastpix.live_streams enable row level security;
8alter table fastpix.uploads enable row level security;
9alter table fastpix.sync_state enable row level security;

Enabling row-level security with no policies denies everyone except the service role, which is the safe default. To show data in your app, create a view that exposes only the safe columns.


Query your data

For server-side queries, use the service-role key, which bypasses row-level security:

1import { createClient } from "@supabase/supabase-js";
2
3const supabase = createClient(
4 "your-supabase-url",
5 "your-supabase-service-role-key",
6 { db: { schema: "fastpix" } },
7);
8
9const { data: media } = await supabase
10 .from("media")
11 .select("*")
12 .eq("status", "ready");

WARNING: The service-role key bypasses row-level security and belongs only in server-side code. To read FastPix data from the client, add your own policies or expose a view with only the safe columns.


Use the sync engine with any Postgres

If you don’t use Supabase, use @fastpix/fp-sync-engine directly. It’s a framework-free TypeScript library that works with any Postgres database and runs on both Node.js and Deno.

$npm install @fastpix/fp-sync-engine

Create the schema once, then process webhooks in your own server. This example uses Express:

1import express from "express";
2import { FastPixSync, runMigrations, InvalidSignatureError } from "@fastpix/fp-sync-engine";
3
4// Creates or upgrades the `fastpix` schema. Safe to run on every boot.
5await runMigrations({ databaseUrl: process.env.DATABASE_URL });
6
7const sync = new FastPixSync({
8 databaseUrl: process.env.DATABASE_URL,
9 fastpixWebhookSecret: process.env.FASTPIX_WEBHOOK_SECRET,
10 fastpixTokenId: process.env.FASTPIX_TOKEN_ID,
11 fastpixTokenSecret: process.env.FASTPIX_TOKEN_SECRET,
12});
13
14const app = express();
15
16// FastPix pings the URL when you register it.
17app.get("/webhook", (_req, res) => res.send("ok"));
18
19// Take the raw body — the engine verifies the signature over the exact bytes.
20app.post("/webhook", express.text({ type: "*/*" }), async (req, res) => {
21 try {
22 const { eventId, isDuplicate } = await sync.ingestWebhook(
23 req.body,
24 req.headers as Record<string, string>,
25 );
26 res.sendStatus(202);
27
28 // Process out of band so a slow re-fetch can't outrun FastPix's retry timer.
29 if (!isDuplicate) {
30 sync.processStoredEvent(eventId).catch((err) => console.error(err));
31 }
32 } catch (err) {
33 res.sendStatus(err instanceof InvalidSignatureError ? 401 : 500);
34 }
35});
36
37app.listen(3000);

For the full API reference, see the @fastpix/fp-sync-engine README.


Troubleshoot

FastPix won’t save the webhook URL. It checks the URL before saving. Make sure ngrok and functions serve are both running, then curl the URL — you should get ok. If you get 401, you skipped the restart after init.

No rows in fastpix.webhook_events. FastPix isn’t reaching your webhook, or the signature is failing. Check that your ngrok URL is current, that FASTPIX_WEBHOOK_SECRET matches exactly, and that you restarted functions serve after editing .env.

Rows stuck at received. Nothing is draining the queue. Confirm both Vault secrets exist and hold the correct values.

fastpix schema or tables don’t exist. init skipped database setup. Run SUPABASE_DB_URL='postgresql://...' npx @fastpix/supabase migrate.

db push fails with “Found local migration files to be inserted before the last migration.” Re-run with npx supabase db push --include-all.

The webhook returns 401 in production. Either you didn’t run npx supabase config push, or the signing secret is wrong — check with npx supabase secrets list.

A SQL query returns a column-not-found error. Column names are camelCase and need double quotes: select "mediaId" from fastpix.media.

Cannot connect to the Docker daemon. Docker isn’t running. Start Docker Desktop, or on Linux run sudo systemctl start docker.


Resources