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
readpermission. 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:
| Field | Value |
|---|---|
| Table | storage.objects |
| Events | Insert only |
| Type | HTTP Request, POST |
| URL | your endpoint_url |
| HTTP header | Authorization: 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 see | What it means |
|---|---|
| No events at all | The 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 log | The 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_mismatch | The event names a different bucket than the rule’s origin — usually a hook pointed at the wrong rule. |
status: failed | Job 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
- Ingest rules — filters, deduplication, every status and reason
- Cloudflare R2 to HLS — the same pattern with a signed Worker
- Webhooks — being told when the ladder is ready