Oferta ograniczona czasowo: plany roczne Starter i Basic 50% taniej 🎉

NestJS and Supabase for AI Video Workflows: A Backend Guide

Oct 13, 2026

Every AI video feature eventually hits the same wall. The interface looks trivial — a prompt box, a duration selector, a generate button — but the machinery behind that button has to survive multi-minute jobs, provider APIs that throttle without warning, files measured in gigabytes, and users who refresh the page every few seconds to see whether anything happened yet.

Standard CRUD backends are a poor fit for that shape of work. NestJS and Supabase make a practical pairing for it: NestJS provides a disciplined, modular TypeScript service layer that can host queues, provider adapters, and webhook handlers, while Supabase supplies Postgres, authentication, row-level security, real-time subscriptions, and object storage without standing up a separate data platform. Together they absorb the boring infrastructure so the interesting part — orchestration — stays visible.

Why AI Video Backends Look Different From Typical SaaS Apps

Most SaaS products are request-response systems. A user submits data, the server writes a row, the client re-renders. Latency is measured in tens or hundreds of milliseconds, and the worst-case failure is a validation error. Generative video breaks every one of those assumptions at once.

A single generation request can occupy a GPU for thirty seconds or several minutes. The provider may accept the request and then stall, return a partial artifact, or complete successfully while the callback never arrives. The output is a binary asset, not a JSON payload, and the same prompt can produce wildly different results across model families and versions. Treating any of that as a synchronous HTTP call is the fastest way to build something that melts under modest traffic.

The three properties that break CRUD assumptions

  • Duration. Jobs outlive request timeouts, browser sessions, and sometimes deployments. Your workflow must be persisted, resumable, and observable long after the originating connection is gone.
  • Variability. Different model endpoints have different input schemas, different polling behavior, different rate limits, and different failure vocabularies. The backend needs an adapter boundary so a new model does not ripple through the whole codebase.
  • Verification. You cannot trust a client saying a job finished, and you cannot always trust a provider webhook. Completion must be provable through server-side reconciliation against the provider's own API and your stored artifacts.

The practical consequence is that the database becomes the source of truth for workflow state, and the API layer becomes a thin coordinator that dispatches work and reports progress.

A Reference Stack That Handles Model Diversity

The stack splits cleanly into five layers, each with one job.

Layer Responsibility Typical choice
API and orchestration Validation, authorization, job dispatch, webhooks NestJS with TypeScript
Data, auth, realtime Entity storage, policies, live status, identity Supabase (Postgres, Auth, Realtime, RLS)
Queue and workers Long-running execution, retries, concurrency control Redis-backed queue or a Postgres-backed queue
Media pipeline Transcoding, thumbnails, normalization FFmpeg workers on cheap CPU instances
Delivery Signed access, caching, bandwidth control Object storage plus CDN

The important design decision is not which queue library you pick. It is deciding where state lives. If the queue is the only record of a pending job, a single queue wipe destroys user-visible work. If Postgres is the record and the queue is just a delivery mechanism, you can rebuild the queue from the database at any time — a much healthier invariant.

Where each layer earns its place

NestJS earns its place through structure: modules, dependency injection, guards, interceptors, and pipes that keep cross-cutting concerns from leaking into business logic. Supabase earns its place through Postgres. Row-level security, real-time change feeds, and storage policies are all extensions of the same relational core, which means access rules live in one place instead of three.

Modeling Projects, Assets, and Jobs in Postgres

A workable schema separates intent from execution. Users create projects and scenes; the system creates jobs and assets. Mixing those two ideas produces tables that are impossible to query cleanly after a few months of iteration.

A minimal but durable model includes:

  • workspaces and workspace_members — ownership and sharing boundaries.
  • projects — the container a user thinks in terms of.
  • scenes or shots — the unit a prompt applies to, including duration, aspect ratio, and reference image identifiers.
  • assets — every uploaded or generated file, with checksum, dimensions, byte size, storage path, and provenance.
  • jobs — one row per attempted generation, with provider, model identifier, status, attempt count, and timestamps.
  • job_events — an append-only log of state transitions, useful for debugging and for replaying progress in the UI.
  • model_profiles — the capabilities and constraints of each model the platform exposes, kept as data rather than hard-coded branches.

Row-level security as the default access boundary

Supabase policies let you express rules like "a user can read a job only if they are a member of the workspace that owns the parent project." Writing that once in SQL is far safer than re-implementing it in every controller. Server-side jobs use a privileged role that bypasses these policies, so the policy layer protections apply to client traffic specifically, which is exactly where the risk sits.

Indexing and partitioning for job history

Job tables grow faster than any other table in the system. Index on (status, created_at) for queue recovery, on (project_id, created_at desc) for UI listings, and on (provider, external_id) for webhook lookups. If volume justifies it, partition job history by month so deletion of old records is a cheap operation rather than a vacuum nightmare.

Structuring NestJS Around Modules and Adapters

NestJS rewards teams that resist the temptation to put everything in one service. A layout that scales looks roughly like this:

  • ProjectsModule — project and scene CRUD, ownership checks.
  • AssetsModule — upload signing, metadata extraction, deduplication by checksum.
  • RenderModule — job creation, state machine transitions, cancellation.
  • ProviderModule — one adapter per model family, all implementing a shared interface.
  • WebhooksModule — signature verification, idempotent ingestion, reconciliation triggers.
  • RealtimeModule — publishing structured progress events to the right channels.

Dependency injection and provider adapters

Define an interface with methods such as submit, poll, cancel, and normalizeResult. Each model adapter implements it. Injection tokens map a model identifier from model_profiles to a concrete adapter, so adding a new generation endpoint becomes a new class and a database row rather than a change to a controller. That boundary is what keeps model churn from becoming a rewrite.

DTO validation and shared contracts

Input validation at the edge prevents an astonishing number of downstream bugs. Reject durations outside a model's supported range before a job is created, not after a provider returns an error three minutes later. Keep the validation rules derived from the same capability rows the adapters read, so the UI, the API, and the queue all agree about what is possible.

Keep business rules out of controllers

Controllers should translate HTTP into a service call and nothing more. State transitions, quota checks, and asset promotion belong in services that can be unit-tested without a running server. When a bug report arrives, you should be able to point at one file that decides whether a job may move from queued to running.

Orchestrating Long-Running Generations Without Losing Track

Job lifecycle management is where most of the engineering effort actually goes. A state machine that everyone respects beats a clever queue configuration every time.

A state set that covers real-world behavior: draft, queued, dispatching, running, post_processing, succeeded, failed, canceled. Every transition is written to jobs and appended to job_events in the same transaction. If the process crashes halfway through, the transaction guarantees you never see a job that claims to be running with no event explaining how it got there.

Idempotency, retries, and the cancellation problem

Every dispatch needs an idempotency key stored on the job row. If a worker retries after a network timeout, the provider adapter can detect the duplicate and reconcile instead of submitting twice. Retries should use exponential backoff with jitter, and a capped attempt count that moves the job to a dead-letter state rather than retrying forever.

Cancellation deserves explicit treatment. Some providers support cancellation; most do not. The honest approach is to mark the job canceled locally, stop polling it, discard the result when it arrives, and tell the user plainly that provider-side compute may already be spent. Pretending cancellation is instant creates support tickets.

Reconciliation instead of blind trust

Run a periodic reconciliation pass over jobs stuck in running past their expected duration. Query the provider directly, update state accordingly, and flag jobs whose provider record has vanished. This single worker catches the majority of production incidents in generation pipelines, because nearly all of them look like a job that simply never finished.

Real-Time Status and Collaboration

Polling is the default and it is rarely the right answer. Supabase Realtime lets clients subscribe to a channel scoped to a project, receiving progress updates the moment the server writes them.

Subscribe on the narrowest useful scope. A channel per project rather than per user, with the client filtering by scene, keeps the subscription count manageable. Publish structured payloads — job identifier, phase, percentage, and any partial artifact reference — rather than free-form strings, so the interface can render progress consistently across every model.

Presence for editing awareness

Presence tracks who is currently viewing or editing a project. It is genuinely useful in collaborative tools and genuinely expensive when abused, so cap presence payloads to small identifiers and never put asset metadata inside them.

Throttle and batch updates

Providers rarely report granular progress. Throttle writes to something like one event every few seconds and batch anything faster. Flooding Realtime with high-frequency updates costs bandwidth and produces jumpy interfaces that feel broken even when they are working correctly.

Media Storage, Transcoding, and Delivery

Generated video is heavy. Storage design determines whether the platform stays affordable as usage grows.

Upload directly to object storage using signed URLs issued by the API. The API validates the request, returns a short-lived signed URL, and never proxies the bytes. After upload, the client confirms, and a worker verifies the object's checksum and extracts metadata. This pattern keeps the Node process free of large buffers, which is one of the most common causes of memory pressure in video backends.

Normalize before you distribute

Do not serve whatever the model returned. Run a transcode pass that produces a web-friendly rendition, generates a poster frame, and records exact duration and dimensions. Users who download the original and viewers who stream the normalized version want different files, and serving the wrong one to the wrong audience is a persistent source of complaints.

Lifecycle rules that keep storage honest

Apply lifecycle policies: delete abandoned draft exports after a set window, move finished projects to colder storage tiers after long inactivity, and never keep raw intermediate frames indefinitely. Thumbnails and preview clips should be cheap to regenerate if a purge is ever needed, which means the source of truth must be the original asset plus a reproducible recipe.

Security, Quotas, and Abuse Resistance

Generative video is expensive compute, which makes it a target. The first rule is unglamorous: privileged service keys never appear in client code, and every privileged operation happens server-side behind an authorization check.

Row-level security covers direct client access. On top of that, enforce per-workspace rate limits at the API layer — so many concurrent jobs, so many submissions per hour, and a hard ceiling on queue depth. Queue depth limits matter more than most teams expect: without them, one enthusiastic user can starve everyone else while appearing to do nothing wrong.

Limits that protect the system

Make limits legible. Return a clear error that names the limit and states when it resets. Silent failures and generic error messages push users toward retry loops that make the underlying congestion worse. A visible, explainable ceiling is far easier to live with than a black box that sometimes ignores a button press.

Validate provider callbacks ruthlessly

Verify webhook signatures, reject replays using stored event identifiers, and never trust a callback payload as the sole source of truth about completion. A webhook is a hint that something changed, not a verdict.

Observability, Testing, and Common Mistakes

If you cannot trace a job from submission to final asset, you cannot support a video product. Structured logs should carry the job identifier on every line, and traces should span the API request, the queue dispatch, the provider call, and the webhook or reconciliation that closed the loop.

Useful metrics include queue wait time, generation duration percentiles per model, failure rate by provider, and the proportion of jobs that required reconciliation. That last number is a leading indicator: when it climbs, a provider's behavior has changed before users start complaining.

Testing strategies that actually catch pipeline bugs

Unit-test each adapter with recorded provider responses, including malformed and partial ones. Write contract tests that verify capability rows match adapter behavior. Load-test the queue with realistic durations rather than instant stubs, because a stub that returns in ten milliseconds hides every concurrency bug you have. Finally, deliberately simulate lost webhooks in staging; recovery paths that are never exercised are not really implemented.

Mistakes that show up again and again

  • Storing video blobs inside Postgres instead of object storage.
  • Generating synchronously inside an HTTP handler and hoping the request survives.
  • Treating the queue as the source of truth for workflow state.
  • Skipping idempotency keys and submitting duplicate generations under retry.
  • Letting clients poll every second when a subscription channel would do.
  • Hard-coding model capabilities across controllers, templates, and validation.
  • Publishing privileged keys into any browser bundle.

Frequently Asked Questions

Do I need NestJS specifically, or will any Node framework work?

Any framework can host this architecture, but NestJS makes the module and dependency-injection boundaries explicit, which matters when you have a dozen provider adapters and a state machine to protect. The value is structural discipline rather than any unique feature.

Can Supabase Realtime replace a dedicated WebSocket service?

For progress updates and presence, yes, at least until you have very specific latency or fan-out requirements. The advantage is that access rules live alongside the data they protect, so a subscription cannot leak information that a query would refuse to return.

Where should job state live, in the queue or the database?

Always the database. Treat the queue as a transport layer that can be rebuilt. When a worker crashes or a deployment drains the queue, you should be able to reconstruct pending work from rows rather than from memory of what was enqueued.

How many workers do I need?

Let the provider's rate limits decide. Worker concurrency is usually constrained by upstream capacity, not by your own CPU. Start conservative, measure queue wait time and provider error rates, and scale until one of those two numbers begins degrading.

How do I handle a model being deprecated?

Keep capabilities in model_profiles, mark the row as retired, and leave historical jobs referencing it so old projects remain reproducible. New submissions should route to a successor model, and the interface should tell users which of their scenes used the retired one.

What is the fastest way to make this backend more reliable?

Add reconciliation. A single scheduled worker that re-checks jobs stuck in a running state resolves the largest category of real incidents, because most failures in generation pipelines look identical from the outside: nothing ever came back.

The architecture is not complicated in individual pieces. Its difficulty is in refusing shortcuts — persisting state, verifying completion, and keeping model-specific behavior behind adapters. Do those three things consistently, and the same backend will absorb each new generation model without a rewrite.

Alexander

Alexander