payload-mux
Host and play back video in Payload without a media server — Mux stores the clip, your admin stays the source of truth.
Video hosting inside Payload, powered by Mux.
Drop a clip into the admin and it streams everywhere — adaptive HLS, posters, and preview GIFs, public or signed. No media server to run, and no huge files in your storage: Mux holds the video, Payload holds the data, and they stay in sync both ways.
pnpm add @pro-laico/payload-muxBased on @oversightstudio/mux-video (MIT),
originally created by Idan Yekutiel. A port of that Mux plugin,
restructured to our conventions and kept as a first-party package so we can track Payload and Mux
updates directly. Full credit and thanks to the original author.
Requirements
- A Mux account for the API credentials.
- A live site, eventually, for the webhook. Mux pushes encoding events to your endpoint, so the webhook only works on a deployed, publicly reachable site — it can't reach
localhost, and local development doesn't need it: the admin recovers encoding state by asking Mux directly. - No framework requirement. Payload
^3and React 19, and that's it — zeronext/imports, nonextpeer. Runs in any Payload app, Next.js or not.
What's included
Video hosting that lives inside Payload, powered by Mux:
- Upload and it just plays: drop a clip in the admin and Mux encodes it to adaptive HLS with posters and preview GIFs — no media server, no huge files bloating your storage.
- Direct-to-Mux uploads: the browser posts bytes straight to Mux, so video never touches your server or database.
- Playback URLs computed on read: each row of a video's
playbackOptionscarries virtualplaybackUrl(HLS),posterUrl, andgifUrl, JWT-signed automatically under a signed policy. - Kept in sync both ways: a webhook finishes slow-encoding videos and mirrors Mux-dashboard deletes back into Payload.
- Server ingest and declarative seeding: create a video from a file or URL in code, or seed one like any other doc through
@pro-laico/payload-seed.
Quickstart
Set Mux credentials
The plugin and the Mux SDK read the standard MUX_* environment variables automatically, so the
plugin call itself takes no arguments.
MUX_TOKEN_ID=...
MUX_TOKEN_SECRET=...
MUX_WEBHOOK_SECRET=...
NEXT_PUBLIC_SERVER_URL=https://your-site.com # your site URL — becomes the upload CORS originAdd the plugin
Zero-config reads the env vars above. For browser uploads, cors_origin resolves to
options.uploadSettings.cors_origin ?? process.env.NEXT_PUBLIC_SERVER_URL ?? '*'. Pass options only
to override: a non-standard env var name, a custom CORS origin, or signed playback.
import { buildConfig } from 'payload'
import { muxPlugin } from '@pro-laico/payload-mux'
export default buildConfig({
plugins: [muxPlugin()], // credentials from MUX_*, cors_origin from NEXT_PUBLIC_SERVER_URL
})Add the two signing keys to your env:
MUX_SIGNING_KEY=...
MUX_PRIVATE_KEY=...muxPlugin({
options: {
playbackPolicy: 'signed', // new videos upload as signed
signedUrlOptions: { expiration: '1d' }, // signed URL lifetime
},
})This one setting governs every new video — admin uploads and server-side ingest / seeding alike.
The virtual playbackUrl / posterUrl / gifUrl are then JWT-signed on every read.
Generate the admin import map
The collection registers admin components (the uploader field and the list-view thumbnail cell) by string path, so regenerate the import map, then restart the dev server:
pnpm payload generate:importmapThe plugin has now added a Video (mux-video) collection to the admin. Drop a clip on it and
Mux encodes it — no webhook needed yet; the uploader polls Mux for the encoding result.
Relate to a video from your collections
mux-video is a normal collection — reference it with a relationship field wherever you need
video:
import type { CollectionConfig } from 'payload'
export const Posts: CollectionConfig = {
slug: 'posts',
fields: [
{ name: 'video', type: 'relationship', relationTo: 'mux-video' },
],
}Play it back
Every ready video carries virtual playbackUrl / posterUrl / gifUrl fields, computed on read —
no extra API calls. Hand them to any Mux player, e.g.
@mux/mux-player-react
(pnpm add @mux/mux-player-react):
import config from '@payload-config'
import MuxPlayer from '@mux/mux-player-react'
import { getPayload } from 'payload'
export default async function Page() {
const payload = await getPayload({ config })
const post = await payload.findByID({ collection: 'posts', id: '…', depth: 1 })
const playback = typeof post.video === 'object' ? post.video?.playbackOptions?.[0] : undefined
if (!playback) return null // still encoding — no playback ids yet
return <MuxPlayer playbackId={playback.playbackId} src={playback.playbackUrl} poster={playback.posterUrl} />
}More in Using video.
Once the site is live: point a Mux webhook at it
In the Mux dashboard, send webhooks to https://your-site.com/api/mux/webhook (or
<your routes.api>/mux/webhook if you've customized Payload's API route), with
MUX_WEBHOOK_SECRET matching the dashboard's signing secret.
The webhook is what finishes videos that encode slower than the upload's ~6s poll and what mirrors
Mux-dashboard deletes back into Payload. It can't work locally — Mux has to POST to your site, and
it can't reach localhost — but local development doesn't need it: the uploader and doc saves
refetch the asset state from Mux on their own. See
Endpoints and webhooks for exactly what
depends on it.
Explore
Use a video
Relate to mux-video from any collection and play it back, public or signed.
Endpoints and webhooks
The upload and webhook endpoints, and creating videos server-side from a file or URL.
How it works
The asset lifecycle, revalidation with Next.js, and declarative seeding.
Reference
Plugin options, environment variables, endpoints, and exports.
Troubleshooting
Videos stuck on "preparing", missing thumbnails, signed-playback 403s, and more.
Plugin options
Zero-config by default — every option falls back to a MUX_* env var or a sensible default. See
Reference for options, environment variables, endpoints, and exports.