Convex is a seriously impressive serverless database. It's not just fast; the developer experience is one of the best I've ever, well... experienced. Convex is now my go-to for every app I've been building lately. Since I've been building applications using Convex for the past few months, I've noticed a sudden fluidity in the way that I build features. With Convex, you can spin up agents, store data in a database, and create an API with minimal effort. They don't always make them like this, which is why we had to build a component for Mux so that developers can easily build video streaming apps with a backend.
Convex released Components: self-contained backend modules that bundle functions, schemas, and data. Think of them as plugins for your database. @mux/convex is our component that keeps your Mux assets, live streams, and uploads in sync with real-time webhook updates and scaffolds the tables you need to start building video streaming apps.
So you might be asking yourself now, what could I actually build with this @mux/convex component? The answer is just about anything, but let me give you an example to steer you where you could be going with this.
I built Robotube—a mobile app built with Mux, Convex, and React Native, and of course, @mux/convex. Using @mux/convex was really for setting the foundation and getting myself up to speed quickly. This is a video streaming app that uses a combination of Mux and Convex features like AI-generated tags, summaries, chapters, and video embeddings, using @mux/ai. We also used @mux/ai for moderating videos during uploading to make sure they were appropriate. Not only did I get to integrate these great AI features, but since I generated the embeddings of my videos with @mux/ai, it was easy to sync my app up with Convex to utilize its vector search and RAG features. So, when I say you can pretty much just build anything with this stack, you see what I mean.
What gets created
After setup, your Convex project gets five tables that are compatible with Mux:
- assets — video assets and their processing state
- uploads — direct upload records
- liveStreams — live stream configurations
- events — a webhook event log
- videoMetadata — your app's own data: titles, user ownership, visibility, tags
The first four tables are managed by Mux and kept up to date via webhooks. The videoMetadata table is yours to fill in. That means your users can set titles, or you can generate summaries and tags with @mux/ai and store them there. When you need both, the component’s queries return Mux data and your metadata together in one response.
Things you need beforehand
- Mux and Convex accounts
- Mux Token ID
- Mux Secret Key
- Mux Webhook Secret
Getting set up
Install the packages:
npm install convex @mux/convex @mux/mux-nodeRun the init script to generate the Convex config and webhook handler:
npx @mux/convex init --component-name muxThis drops four files into your `convex/` directory: component config, a migrations file, a webhook handler, and an HTTP route that exposes the webhook endpoint. If you already have a convex.config.ts or http.ts, pass the flags `--skip-config` or `--skip-http` and wire things up manually.
Set your Mux credentials in convex:
npx convex env set MUX_TOKEN_ID your-mux-token-id
npx convex env set MUX_TOKEN_SECRET your-mux-token-secretStart the dev server and run the backfill to pull in your existing assets:
npx convex dev
# in another terminal:
npx convex run migrations:backfillMux '{}'Then create a webhook in the Mux Dashboard pointed at your Convex HTTP endpoint: https://your-deployment.convex.site/mux/webhook
Copy the signing secret, add it to your environment, and you're done:
npx convex env set MUX_WEBHOOK_SECRET your-webhook-signing-secretFrom here, every asset event Mux fires gets routed into your database automatically.
Querying your videos
The component exposes queries through components.mux. You wrap them in your own functions to control what your app actually exposes:
// convex/videoQueries.ts
import { query } from './_generated/server';
import { components } from './_generated/api';
import { v } from 'convex/values';
export const listAssets = query({
handler: async (ctx) => {
return await ctx.runQuery(components.mux.catalog.listAssets, {});
},
});
export const getAsset = query({
args: { muxAssetId: v.string() },
handler: async (ctx, args) => {
return await ctx.runQuery(components.mux.catalog.getAssetByMuxId, {
muxAssetId: args.muxAssetId,
});
},
});Call them from your React components using Convex's useQuery hook:
import { useQuery } from 'convex/react';
import { api } from '../convex/_generated/api';
function VideoLibrary() {
const assets = useQuery(api.videoQueries.listAssets);
if (!assets) return <div>Loading...</div>;
return (
<ul>
{assets.map((asset) => (
<li key={asset.muxAssetId}>
{asset.muxAssetId} — {asset.status}
</li>
))}
</ul>
);
}Let's take a second to admire Convex’s ease of use. There are no SQL queries, no need to change languages, and the way you can just import into a React component is a work of art on its own. As you can see from this example, the query is reactive. When a webhook comes in and updates an asset from preparing to ready, that change propagates to your UI automatically—no polling, no manual cache invalidation. It's the perfect developer’s "chef’s kiss."
All metadata is accepted here
The way this component is set up is so that the Mux-level data like playback IDs, duration, aspect ratio, and tracks comes in through webhooks automatically when a video is uploaded. Now the metadata is up to you, the developer. Like I was saying about my Robotube example, that app is hooked up with convex auth to get the uploaded user’s name, and mux/ai takes care of all the other metadata like summary, tags, chapters, and more. You'll also want to attach app context: who uploaded it, what it's called, whether it's public. That's what the videoMetadata table is for:
// convex/videoMutations.ts
import { mutation } from './_generated/server';
import { components } from './_generated/api';
import { v } from 'convex/values';
export const setVideoMetadata = mutation({
args: {
muxAssetId: v.string(),
userId: v.string(),
title: v.optional(v.string()),
visibility: v.optional(
v.union(v.literal('public'), v.literal('private'), v.literal('unlisted'))
),
tags: v.optional(v.array(v.string())),
},
handler: async (ctx, args) => {
await ctx.runMutation(components.mux.videos.upsertVideoMetadata, args);
},
});Once the metadata is set, videos.listVideosForUser (a convex function we packed in the component) returns the Mux asset data and your metadata together.
Cook up with this component
Convex and Mux come together like the perfect PB&J—the right amount of each spread for a seriously satisfying combo. We’re excited to see what you build with this component. Dig in, and stay tuned for the next course where we highlight how we used @mux/convex to build Robotube.



