Payload Plugins
Pluginspayload-mux

How it works

Follow a video from upload to playback: how Mux encoding, the webhook, revalidation, and seeding fit together.

For AI / LLMs: View Markdown

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

On this page