How it works
Follow a video from upload to playback: how Mux encoding, the webhook, revalidation, and seeding fit together.
The upload flow, the virtual URLs, and the webhook all fit together in one lifecycle. This page traces that lifecycle, then covers revalidation and seeding.
The asset lifecycle
When you upload, the admin field posts the bytes straight to Mux. The video never
passes through your server. The doc saves with just its assetId.
The beforeChange hook then polls the new asset for ~6 seconds. A fast encode
returns full playback metadata inline. Anything slower is left for the webhook,
which fills that metadata in once Mux finishes. The webhook also mirrors Mux-dashboard
deletes back into Payload.
If that webhook never arrives — a wrong signing secret, an unreachable endpoint, Mux
downtime — the video isn't stranded. Saving a doc that has an assetId but no
playbackOptions refetches the asset from Mux and lands it on ready (or records
errored), so a lost event is recoverable from the admin rather than by re-uploading.
It's one request, not a poll, and it's best-effort: if Mux can't be reached the save
still goes through and the doc is left as it was. A doc that's already ready never
pays for this — it short-circuits before any Mux call.
The playbackUrl / posterUrl / gifUrl fields aren't stored. They compute from the
playback id on every read, signed on the fly under a signed policy.
Server-side ingest and seeding skip the 6s window entirely. They wait for the asset to
reach ready and write full metadata into the doc, so those videos play straight away
whatever their length. See Server-side ingest.
Revalidation
The plugin keeps Payload and Mux in sync but doesn't touch Next.js's cache. Some writes
land outside the request that rendered your page — the video.asset.ready and
video.asset.deleted webhook events especially.
With @pro-laico/payload-revalidate installed there's
nothing to wire. Its auto-attached hooks cover mux-video like any collection. The webhook
writes go through payload.update, so they fire the same hooks as admin saves and
server-side ingest. Fetch the video doc through an id-keyed cacheDoc getter and derive the
playback URLs from it:
import { cacheDoc, getPayloadClient } from '@pro-laico/payload-revalidate/cache'
export async function getVideo(id: string | number) {
'use cache'
const payload = await getPayloadClient()
return cacheDoc(await payload.findByID({ collection: 'mux-video', id, depth: 0, disableErrors: true }), 'mux-video')
}A page showing "processing" then heals itself. The video.asset.ready webhook busts
mux-video:{id} the moment Mux finishes, so the next request re-fetches exactly that entry
with the playback ids populated.
Without the revalidate plugin, handle it yourself with a Payload afterChange /
afterDelete hook on mux-video. That hook fires for webhook writes, admin saves, and
server-side ingest alike, so it's the one place to call revalidateTag / revalidatePath
for the pages that embed videos.
Seeding
mux-video isn't a plain upload collection — its bytes live at Mux. So it seeds through
@pro-laico/payload-seed's
custom ingestion. muxVideoPlugin() marks the collection with
custom: { seedAsset: { sourceField: 'source' } }, the seed engine auto-discovers that
marker from the live config, and a video seeds like any other doc. Each record carries
its clip on the _file meta-key via the file() token, and other docs point at it with
ref():
import { muxVideoPlugin } from '@pro-laico/payload-mux'
import { defineSeed, seedPlugin } from '@pro-laico/payload-seed'
// src/seed/videos.ts — each doc carries its clip on `_file`
const videos = defineSeed('mux-video', ({ file }) => [
{
_key: 'intro',
_file: file('intro.mp4'), // assets/mux-video/intro.mp4
title: 'Intro',
},
])
// src/seed/pages.ts — reference a seeded video by ref
const pages = defineSeed('pages', ({ ref }) => [
{
_key: 'home',
title: 'Home',
heroVideo: ref('mux-video', 'intro'),
},
])
plugins: [muxVideoPlugin(), seedPlugin({ definitions: [videos, pages] })]Under the hood the engine writes each _file to the collection's source field and the
server-side ingest
hook does the upload. The seed package never imports this one, nor the Mux SDK, so the two
stay decoupled. Ingest waits for each asset to reach ready and writes its full metadata,
so seeded videos play straight away, no webhook required. Playback policy comes from the
plugin's configured policy, so seeded and admin-uploaded videos share one.
Without credentials, seeding skips — it doesn't fail. When MUX_TOKEN_ID /
MUX_TOKEN_SECRET aren't set, the plugin also marks the collection
custom: { seedDisabled: '…' }. The seed engine skips its definition with a warning and
drops any optional ref('mux-video', …), so the rest of the seed runs offline. Set the
env vars and the next run ingests the clips and wires the refs, with no seed-file changes.
See Disabled seeds.
Next
- Use a video — relate, play back, and sign URLs.
- Endpoints & webhooks — the endpoints, webhook, and server-side ingest.
- Troubleshooting — stuck uploads, missing thumbnails, signed-playback 403s.