Payload Plugins
Pluginspayload-mux

payload-mux

Host and play back video in Payload without a media server — Mux stores the clip, your admin stays the source of truth.

For AI / LLMs: View Markdown

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-mux

Based 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 ^3 and React 19, and that's it — zero next/ imports, no next peer. 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 playbackOptions carries virtual playbackUrl (HLS), posterUrl, and gifUrl, 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.

.env
MUX_TOKEN_ID=...
MUX_TOKEN_SECRET=...
MUX_WEBHOOK_SECRET=...
NEXT_PUBLIC_SERVER_URL=https://your-site.com   # your site URL — becomes the upload CORS origin

Add 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.

payload.config.ts
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:

.env
MUX_SIGNING_KEY=...
MUX_PRIVATE_KEY=...
payload.config.ts
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:importmap

The 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:

src/collections/Posts.ts
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):

src/app/(frontend)/watch/page.tsx
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

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.

On this page