kuldeepโœฆ
โ†back to all writing
Jan 2026 ยท 8 min read
full stack๐Ÿ“บdistributed media systems

Building NewTube: Full-Stack Video Platform with Mux & tRPC

Architectural breakdown of NewTube: zero-server media ingestion with Mux, webhook asset migration to Uploadthing, Upstash QStash AI workflows, and Drizzle ORM on Neon PostgreSQL.

Media Ingestion

Direct-to-Mux

Background Jobs

Upstash QStash

Database

Neon Serverless PG

Type Safety

100% tRPC + Zod

view repository โ†—live demo โ†—#Next.js 16#Mux Video#tRPC#Drizzle ORM#Neon DB#Upstash Workflows#Clerk

1. The Serverless Compute Dilemma: Decoupling Media Bytes from API Endpoints

Streaming multi-gigabyte video uploads through serverless Node.js API routes quickly leads to disaster: memory exhaustion, strict request payload caps, cold-start stalls, and exorbitant bandwidth egress costs.

For NewTube, our application runtime never buffers raw video bytes. Instead, the client invokes a protected tRPC mutation (videos.create) that asks Mux for a direct upload session. The serverless route merely initializes a pending video record in Neon PostgreSQL with a waiting status, returning the direct Mux upload URL so the client streams video chunks straight to Mux's global edge.

Decoupling video transport from serverless compute enables unlimited file sizes with zero server CPU overhead.

src/modules/videos/server/procedures.ts
1export const videosRouter = createTRPCRouter({
2 create: protectedProcedure.mutation(async ({ ctx }) => {
3 const { id: userId } = ctx.user;
4
5 const upload = await mux.video.uploads.create({
6 new_asset_settings: {
7 passthrough: userId,
8 playback_policies: ["public"],
9 input: [
10 {
11 generated_subtitles: [
12 { language_code: "en", name: "English" },
13 ],
14 },
15 ],
16 },
17 cors_origin: "*",
18 });
19
20 const [video] = await db
21 .insert(videos)
22 .values({
23 userId,
24 title: `Untitled ${Date.now()}`,
25 muxStatus: "waiting",
26 muxUploadId: upload.id,
27 })
28 .returning();
29
30 return { video, url: upload.url };
31 }),
32});

2. Webhook Orchestration & Dual-CDN Asset Archival

Once Mux finishes transcoding adaptive HLS streams (from 360p up to 1080p60) and generating automated English subtitles, it fires asynchronous webhooks back to Next.js. Every incoming payload is cryptographically validated using HMAC SHA-256 signatures via mux.webhooks.verifySignature() before any database mutation occurs.

Rather than leaving thumbnail and animated preview assets on ephemeral endpoints, our webhook handler intercepts video.asset.ready, downloads the generated assets, and immediately archives them to Uploadthing CDN via UTApi.uploadFilesFromUrl(). This dual-CDN strategy guarantees permanent, lightning-fast thumbnail delivery independent of Mux lifecycle states.

src/app/api/videos/webhook/route.ts
1mux.webhooks.verifySignature(
2 body,
3 { "mux-signature": muxSignature },
4 SIGNING_SECRET
5);
6
7switch (payload.type) {
8 case "video.asset.ready": {
9 const data = payload.data;
10 const playbackId = data.playback_ids?.[0]?.id;
11 const tempThumbnailUrl = `https://image.mux.com/${playbackId}/thumbnail.jpg`;
12 const tempPreviewUrl = `https://image.mux.com/${playbackId}/animated.gif`;
13
14 // Archive generated media directly to Uploadthing CDN
15 const utapi = new UTApi();
16 const [uploadedThumbnail, uploadedPreview] = await utapi.uploadFilesFromUrl([
17 tempThumbnailUrl,
18 tempPreviewUrl,
19 ]);
20
21 await db
22 .update(videos)
23 .set({
24 muxStatus: data.status,
25 thumbnailUrl: uploadedThumbnail.data?.ufsUrl,
26 thumbnailKey: uploadedThumbnail.data?.key,
27 previewUrl: uploadedPreview.data?.ufsUrl,
28 previewKey: uploadedPreview.data?.key,
29 muxPlaybackId: playbackId,
30 muxAssetId: data.id,
31 duration: data.duration ? Math.round(data.duration * 1000) : 0,
32 })
33 .where(eq(videos.muxUploadId, data.upload_id));
34 break;
35 }
36}

๐Ÿ“Œ Key Engineering Takeaways:

  • โœ“HMAC signature verification prevents spoofed webhook replay attacks.
  • โœ“Automated Uploadthing migration protects against missing or expired thumbnails.
  • โœ“Mux subtitle track webhooks (video.asset.track.ready) supply raw transcripts for AI pipelines.

3. Distributed AI Pipelines via Upstash QStash Workflows

A modern creator studio requires automated intelligence: SEO titles, detailed descriptions with timestamps, and custom thumbnails. Traditional queue workers (like BullMQ + Redis containers) require heavy persistent infrastructure incompatible with pure serverless platforms.

NewTube implements durable execution using @upstash/workflow/nextjs. When a creator triggers AI generation, QStash coordinates a multi-step background workflow that fetches the auto-generated transcript directly from Mux text tracks (stream.mux.com/.../text/*.txt), streams it to OpenRouter (deepseek/deepseek-chat) for SEO title synthesis, and invokes Hugging Face FLUX.1-schnell for 16:9 thumbnail rendering.

QStash Workflows provide durable execution with step-level retries and zero container management.

src/app/api/videos/workflows/title/route.ts
1export const { POST } = serve(async (context) => {
2 const { userId, videoId } = context.requestPayload as InputType;
3
4 // Step 1: Verify video ownership in Neon PostgreSQL
5 const video = await context.run("get-video", async () => {
6 const [res] = await db.select().from(videos).where(and(eq(videos.id, videoId), eq(videos.userId, userId)));
7 if (!res) throw new Error("Video not found");
8 return res;
9 });
10
11 // Step 2: Stream Mux generated subtitles transcript
12 const transcript = await context.run("get-transcript", async () => {
13 const trackUrl = `https://stream.mux.com/${video.muxPlaybackId}/text/${video.muxTrackId}.txt`;
14 const res = await fetch(trackUrl);
15 return await res.text();
16 });
17
18 // Step 3: LLM generation conditioned on actual spoken audio
19 const title = await context.run("generate-title", async () => {
20 return await queryOpenRouter("deepseek/deepseek-chat", TITLE_SYSTEM_PROMPT, transcript);
21 });
22
23 // Step 4: Atomic database update
24 await context.run("update-video", async () => {
25 await db.update(videos).set({ title }).where(eq(videos.id, video.id));
26 });
27});

4. Relational Integrity: Composite Primary Keys & CTEs in Drizzle ORM

Instead of Prisma's Rust query engine which adds cold-start latency to serverless lambdas, Drizzle ORM compiles directly to clean SQL executed over Neon's HTTP connection pool.

To guarantee strict uniqueness without racing conditions, junction tables like video_reactions, subscriptions, and playlist_videos enforce composite primary keys at the database engine level. Complex feed queries leverage Common Table Expressions (db.$with) and sub-queries (db.$count) to fetch videos, creator profiles, view counts, and viewer like status in a single round-trip.

src/db/schema.ts
1export const reactionType = pgEnum("reaction_type", ["like", "dislike"]);
2
3export const videoReactions = pgTable(
4 "video_reactions",
5 {
6 userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
7 videoId: uuid("video_id").notNull().references(() => videos.id, { onDelete: "cascade" }),
8 type: reactionType("type").notNull(),
9 createdAt: timestamp("created_at").notNull().defaultNow(),
10 updatedAt: timestamp("updated_at").notNull().defaultNow(),
11 },
12 (t) => [
13 primaryKey({
14 name: "video_reactions_pk",
15 columns: [t.userId, t.videoId],
16 }),
17 ]
18);

5. Production Hardening: Rate Limiting & Clerk User Synchronization

Public-facing APIs require defense-in-depth. We deployed Upstash Redis sliding window rate limiters (10 requests per 10-second window) to thwart scraping bots and reaction spam.

Authentication is handled by Clerk, with user lifecycle events (user.created, user.updated, user.deleted) synchronized into Neon PostgreSQL via verified webhooks. Combined with React Compiler (babel-plugin-react-compiler) automatically memoizing player and feed components, NewTube delivers buttery-smooth 60 FPS browsing with zero runtime type surprises.

๐Ÿ“Œ Key Engineering Takeaways:

  • โœ“Sliding window rate limiters protect mutation procedures from API abuse.
  • โœ“Clerk webhook synchronizations preserve foreign key references and cascade deletions.
  • โœ“Drizzle ORM + Neon HTTP driver eliminates serverless connection pool exhaustion.

โ€” Kuldeep Rajput โœŒ๏ธ

Software Developer ยท building ambitious full-stack web apps