Skip to content
Search Documentation
Search across all documentation pages
Supabase Storage to HLS

Supabase Storage upload to HLS

Your users upload to a Supabase Storage bucket. A database webhook tells us an object landed, and an ingest rule turns it into an HLS ladder. Nothing of yours runs in between.

Supabase is the simplest of the four providers to wire, because a database webhook can send a static header — so it posts to the rule’s endpoint directly, with no relay.

Before you start

  • A Supabase project with a Storage bucket your users upload to.
  • A Transcodely origin pointing at that bucket, with read permission. Supabase Storage is S3-compatible: create an S3 origin with your project’s S3 endpoint and credentials, as in Storage setup.
  • A preset for the ladder you want. Adaptive streaming covers building one.

1. Create the rule

curl -X POST https://api.transcodely.com/transcodely.v1.IngestRuleService/Create 
  -H "Authorization: Bearer {{API_KEY}}" 
  -H "Content-Type: application/json" 
  -d '{
    "origin_id": "ori_a1b2c3d4e5f6",
    "name": "Supabase uploads",
    "filters": {
      "prefix": "uploads/",
      "suffixes": [".mp4", ".mov", ".mkv"],
      "min_bytes": 1024
    },
    "action": {
      "outputs": [{ "preset": "pst_x9y8z7w6v5" }],
      "managed": true
    }
  }'

managed: true sends the outputs to Transcodely hosting, so each upload becomes a playable video with a CDN URL. To write the ladder back into your own bucket instead, drop managed and set output_origin_id to a write-enabled origin.

The min_bytes filter is worth keeping. Several clients write a zero-byte placeholder before the real upload, and it would otherwise become a job that fails on an empty input.

Two values from the response matter:

  • rule.endpoint_url — where the webhook posts.
  • secret — shown once, here. Store it before you close the response.

2. Add the database webhook

In the Supabase dashboard, go to Database → Webhooks → Create a new hook:

FieldValue
Tablestorage.objects
EventsInsert only
TypeHTTP Request, POST
URLyour endpoint_url
HTTP headerAuthorization: Bearer ings_…

Or as SQL, which is what the templates repo ships as supabase/sql/database_webhook.sql:

create trigger transcodely_ingest
after insert on storage.objects
for each row execute function supabase_functions.http_request(
  'https://api.transcodely.com/ingest/ing_a1b2c3d4e5f6',
  'POST',
  '{"Content-Type":"application/json","Authorization":"Bearer ings_…"}',
  '{}',
  '5000'
);

If you would rather the rule’s secret never left your project, the templates repo also ships supabase/edge-function/index.ts: an Edge Function that takes the database webhook and forwards it signed instead of carrying a bearer token. A bearer is replayable by anyone who captures one request; a signature binds the request to its body and to a five-minute window. Point the webhook at the function instead of at Transcodely, and keep the secret in the function’s environment.

INSERT only, deliberately. An upload also produces an UPDATE once its metadata is written, and the two carry different eTags. Acting on both would give one upload two deduplication keys, and therefore two jobs.

Supabase’s own documentation warns a webhook can fire more than once. That is absorbed: an object is identified by (rule, bucket, key, etag), so a redelivery is answered 202 with the id of the event that already won.

The same identity has a consequence worth knowing before you rely on it. A Supabase INSERT often carries no eTag, and the re-upload itself arrives as an UPDATE, which is ignored by design — so overwriting a key produces no second job. When you do want the new bytes transcoded, replay the original event:

curl -X POST https://api.transcodely.com/transcodely.v1.IngestRuleService/ReplayEvent 
  -H "Authorization: Bearer {{API_KEY}}" 
  -H "Content-Type: application/json" 
  -d '{ "event_id": "sev_a1b2c3d4e5f6g7" }'

The rule re-runs against whatever lives at that key now. On buckets that do report a version identity — S3, R2, Google Cloud Storage — a re-upload is a new object and needs no replay.

3. Check it before you upload

Test runs the same matching the live path runs, without creating anything:

curl -X POST https://api.transcodely.com/transcodely.v1.IngestRuleService/Test 
  -H "Authorization: Bearer {{API_KEY}}" 
  -H "Content-Type: application/json" 
  -d '{
    "id": "ing_a1b2c3d4e5f6",
    "object_key": "uploads/demo.mp4",
    "size_bytes": 52428800,
    "content_type": "video/mp4"
  }'

A matched: false answer carries a reason naming the filter that rejected the key.

4. Upload, then read the log

Upload a file into the bucket under your prefix and read the rule’s events:

curl -X POST https://api.transcodely.com/transcodely.v1.IngestRuleService/ListEvents 
  -H "Authorization: Bearer {{API_KEY}}" 
  -H "Content-Type: application/json" 
  -d '{ "rule_id": "ing_a1b2c3d4e5f6" }'
{
  "events": [
    {
      "id": "sev_a1b2c3d4e5f6g7",
      "rule_id": "ing_a1b2c3d4e5f6",
      "bucket": "customer-uploads",
      "object_key": "uploads/demo.mp4",
      "size_bytes": 52428800,
      "content_type": "video/mp4",
      "source": "supabase",
      "status": "created",
      "job_id": "job_a1b2c3d4e5f6"
    }
  ]
}

The same log is on the origin’s page in the dashboard, under Ingest rules.

Subscribe to job.succeeded to learn when the ladder is ready, rather than polling. The job carries ingest_rule_id and ingest_object_key in its metadata, so the webhook tells you which upload it was for.

When nothing arrives

What you seeWhat it means
No events at allThe webhook is not firing, or is posting somewhere else. Check the hook’s URL and that it is on INSERT.
401 in the Supabase webhook logThe header is missing or the secret is wrong. Rotate the rule and update the header.
status: skipped, reason: filter_*The object did not match a filter. Test the key to see which.
status: skipped, reason: bucket_mismatchThe event names a different bucket than the rule’s origin — usually a hook pointed at the wrong rule.
status: failedJob creation was refused. The reason is the API error code that refused it, and it is not retried — replay it once the cause is cleared.

Next