
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.
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 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.
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.
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.
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.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.
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.
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.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.
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.
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.
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.
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.
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.
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 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
Guida pratica al montaggio video rapido con AI: transizioni fluide, software, workflow, coerenza visiva, errori da evitare e FAQ operative.
A practical guide to planning, generating, editing, and finishing AI-assisted videos with consistent characters, clear shot lists, and reliable review loops.
Build a repeatable AI video post-production pipeline: organize assets, cut story-first, design transitions, refine audio, and review versions calmly.