feat(render): plan-gate quality tiers — free=360p watermarked, paid=all
CI/CD / CI · Web (tsc) (push) Successful in 1m8s
CI/CD / Deploy · full stack (push) Successful in 3m8s

Monetization gate for the template render flow:
- render-quality.ts: single source of truth (free -> 360p only +
  watermark; pro/business -> 540p..4K, no watermark).
- /api/render: server-authoritative gate — rejects >360p for free
  users with 403 quality_locked; passes a watermark flag through
  createRenderJob -> /v1/renders (render-svc passthrough, wired later).
- /api/render/limits: GET endpoint exposing the user's allowed tiers
  and watermark state to the studio.
- render page: locks higher tiers for free users (dashed + lock badge,
  click routes to /pricing), clamps the selected resolution down,
  shows the "free = 360p + watermark, upgrade" notice, and handles the
  403 quality_locked response.

AI-video "no free preview" rule is a future hook (no AI gen yet).
Watermark rendering (ffmpeg drawtext on the node) is a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-15 15:17:25 +03:30
parent 468ae2ae97
commit 896ce3dfa9
5 changed files with 142 additions and 17 deletions
+31
View File
@@ -0,0 +1,31 @@
/**
* Render quality policy (monetization gate).
*
* Free plan → 360p only, watermarked preview.
* Pro/Business → every resolution (540p4K), no watermark.
*
* (AI-generated videos will have NO free preview at all — pay/credits before the
* result is shown. That rule belongs in the AI-video feature when it's built;
* this module covers the normal template render flow.)
*/
import type { PlanId } from "@/lib/plans";
import type { RenderSettings } from "@/lib/render-schemas";
export type Resolution = RenderSettings["resolution"]; // "360p" | "540p" | "720p" | "1080p" | "4K"
export const FREE_RESOLUTION: Resolution = "360p";
export const ALL_RESOLUTIONS: Resolution[] = ["360p", "540p", "720p", "1080p", "4K"];
/** Resolutions a plan may render at. Free = 360p only; any paid plan = all. */
export function allowedResolutions(plan: PlanId): Resolution[] {
return plan === "free" ? [FREE_RESOLUTION] : ALL_RESOLUTIONS;
}
export function isResolutionAllowed(plan: PlanId, res: Resolution): boolean {
return allowedResolutions(plan).includes(res);
}
/** Free renders get a watermarked preview; paid plans get clean output. */
export function isWatermarked(plan: PlanId): boolean {
return plan === "free";
}