White-Label Remotion Templates: An Agency Licensing Checklist
By RenderComp Team Editorial policy
Remotion’s core promise — video as deterministic, frame-indexed computation — is exactly what makes it dangerous to hand to an agency without a deliberate handoff architecture. When a React component renders frame 47 the same way every time, that predictability is an asset. But when ten clients need ten differently-branded outputs from the same animation logic, predictability becomes a liability if your props boundary leaks brand state between renders.
The practical consequence: most “white-label” Remotion template sales fail at one of three seams. The template author ships source code that couples animation constants to a specific brand palette, making customization require patching internals. The agency recipient can’t load custom fonts without reaching for a CDN that may be blocked in their render environment. Or the licensing terms are vague about whether the buyer can sub-license renders to their own clients, a question that gets expensive when someone asks.
This article is a technical checklist that covers all three failure modes. Each section is a real decision point with code you can apply directly to a template you’re packaging for resale.
The White-Label Surface Area
Before writing any code, audit exactly what an agency needs to swap out. For a typical branded video template the surface area splits into three tiers:
Tier 1 covers pure data: company name, tagline, colors, logo path. Swapping these values requires no changes to animation code.
Tier 2 involves layout overrides: aspect ratio, safe-zone margins, and secondary composition variants (portrait vs. landscape). This requires swapping Remotion <Composition> definitions; the underlying animation logic stays untouched.
Tier 3 handles behavioral overrides: custom timing curves, locale-specific text handling, swapped motion segments. Template authors often incorrectly conflate Tier 3 with Tier 1, and this is where license complexity concentrates, because Tier 3 delivery typically means shipping editable source.
A well-scoped white-label license covers Tier 1 and optionally Tier 2. If an agency needs Tier 3, that is a different product at a different price point. Being explicit about this in your license document prevents the most common dispute pattern.
Designing a Resale-Ready Props Schema
Remotion’s Zod integration gives you a machine-readable contract between the template and the agency’s brand config. Define it once, export it, and every downstream tool (Studio, Lambda, CLI --props flag) enforces it automatically.
The goal is a schema that expresses the full Tier 1 surface with no escape hatches to internals:
// src/schema.ts
import { z } from "zod";
import { zColor } from "@remotion/zod-types";
export const BrandSchema = z.object({
// Colors — use zColor so Remotion Studio renders a picker
primaryColor: zColor().default("#1A1A2E"),
accentColor: zColor().default("#E94560"),
onPrimaryColor: zColor().default("#FFFFFF"),
// Text content
companyName: z.string().min(1).max(60),
tagline: z.string().max(120).optional().default(""),
// Logo: a path relative to the public/ dir, not an external URL
logoPath: z
.string()
.regex(/^[a-zA-Z0-9_\-./]+$/, "logoPath must be a relative public/ path")
.default("logos/placeholder.svg"),
// Timing overrides (Tier 1 boundary: only duration, not curve shape)
introDurationFrames: z.number().int().min(15).max(90).default(45),
});
export type BrandConfig = z.infer<typeof BrandSchema>;
The logoPath regex is a deliberate constraint: it rejects http:// or ../ paths, forcing the agency to place assets inside the Remotion public/ directory. This closes the most common injection vector in parameterized renders and prevents your template from making network requests during rendering.
Register the schema in your root:
// src/Root.tsx
import { Composition } from "remotion";
import { BrandSchema } from "./schema";
import { BrandedIntro } from "./BrandedIntro";
export const RemotionRoot: React.FC = () => (
<Composition
id="BrandedIntro"
component={BrandedIntro}
schema={BrandSchema}
defaultProps={BrandSchema.parse({})} // surface Zod defaults
durationInFrames={150}
fps={30}
width={1920}
height={1080}
/>
);
Brand Config Injection at Render Time
An agency’s render pipeline should never touch your source code. The handoff artifact is a brand.json file validated against your published schema. At render time, Remotion’s --props flag merges that JSON with defaultProps.
For Lambda renders this becomes:
// agency-render.ts — runs in the agency's CI, not your codebase
import { renderMediaOnLambda } from "@remotion/lambda/client";
import { BrandSchema } from "@your-org/branded-intro"; // published npm package
const brandConfig = BrandSchema.parse(
JSON.parse(fs.readFileSync("./acme-brand.json", "utf8"))
);
await renderMediaOnLambda({
composition: "BrandedIntro",
serveUrl: SERVE_URL,
codec: "h264",
inputProps: brandConfig,
// Each render is an isolated Lambda invocation — client A's config
// cannot bleed into client B's because there is no shared process state.
region: "us-east-1",
functionName: FUNCTION_NAME,
});
The isolation property here is structural, not configured: Remotion Lambda spins a fresh Node.js process per render. You therefore need no mutex locks or session namespacing when running concurrent branded renders; the architecture handles it.
For cases where the brand config includes assets too large for inputProps (a 4K logo PNG, a brand music track), use staticFile with a per-client public directory instead of passing binary data through props:
// Inside your composition component
import { staticFile, useVideoConfig } from "remotion";
export const BrandedIntro: React.FC<BrandConfig> = ({ logoPath, primaryColor }) => {
const { fps } = useVideoConfig();
// staticFile resolves relative to public/ at the serve URL
const resolvedLogo = staticFile(logoPath);
// ...
};
Self-Hosted Fonts and the Licensing They Carry
Font licensing is the overlooked sub-problem inside template licensing. When you ship a white-label template that uses, say, Inter or Geist, you are implicitly asking the agency to acquire (or inherit) a license for that font in their renders.
The safest approach for packaged templates is to default to system font stacks and document the upgrade path for custom typefaces:
// src/typography.ts
export const fontStack = {
// Japanese-capable fallback stack — no external dependencies
ja: '"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif',
// Latin stack
en: '-apple-system, "Segoe UI", Roboto, sans-serif',
} as const;
If your template needs a specific typeface, use @remotion/fonts with a self-hosted woff2 file. The agency places the font file in public/fonts/ and the template loads it with delayRender to prevent the first frame from rendering before the font is ready:
import { staticFile, delayRender, continueRender } from "remotion";
import { loadFont } from "@remotion/fonts";
import { useEffect, useState } from "react";
export function useBrandFont(fontFileName: string) {
const [loaded, setLoaded] = useState(false);
const [handle] = useState(() => delayRender(`Loading font: ${fontFileName}`));
useEffect(() => {
loadFont({
family: "BrandFont",
url: staticFile(`fonts/${fontFileName}`),
weight: "400",
style: "normal",
})
.then(() => {
setLoaded(true);
continueRender(handle);
})
.catch((err) => {
// Don't swallow: a missing font produces silent fallback rendering
// that is hard to detect in QC. Fail loudly.
console.error("Font load failed:", err);
continueRender(handle); // still release or render hangs
});
}, [fontFileName, handle]);
return loaded;
}
Your license document should specify: “This template uses system fonts by default. To substitute a custom typeface, you are responsible for holding a valid commercial web/video license for that font. Remotion renders are video files, not web deployments, but confirm with your font foundry that video embedding is covered.”
Watermark Gating and License State
Preview renders before purchase should carry a visible watermark. The watermark logic needs to survive prop serialization; it cannot depend on a runtime environment variable that the agency could simply omit.
The cleanest pattern threads a licenseToken prop through the schema and renders the watermark when the token is absent or fails a checksum:
// Append to BrandSchema
licenseToken: z.string().optional(),
// src/LicenseWatermark.tsx
import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate } from "remotion";
function isValidToken(token: string | undefined): boolean {
if (!token) return false;
// A real implementation would verify a HMAC or lookup a signed token.
// For a template package, keep validation logic server-side and
// have the CLI return a signed token at purchase time.
return token.startsWith("rlc_") && token.length === 48;
}
export const LicenseWatermark: React.FC<{ licenseToken?: string }> = ({
licenseToken,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
if (isValidToken(licenseToken)) return null;
// Pulse on a 4-second cycle so it is not just a static overlay
const cycleFrame = frame % (fps * 4); // frames 0-119 at 30fps
const opacity = interpolate(
cycleFrame,
[0, fps * 0.4, fps * 3.6, fps * 4],
[0.15, 0.45, 0.45, 0.15],
{ extrapolateRight: "clamp" }
);
return (
<AbsoluteFill
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
pointerEvents: "none",
zIndex: 9999,
}}
>
<div
style={{
opacity,
color: "#FFFFFF",
fontFamily: '-apple-system, "Segoe UI", Roboto, sans-serif',
fontSize: 64,
fontWeight: 700,
letterSpacing: "0.25em",
textTransform: "uppercase",
mixBlendMode: "difference", // survives on both dark and light backgrounds
userSelect: "none",
}}
>
PREVIEW ONLY
</div>
</AbsoluteFill>
);
};
One caveat: since your template’s source may ship to the agency, a determined buyer can remove this check. Watermarks are a friction layer and a contractual signal, not a cryptographic barrier. The real enforcement is your license agreement and the delivery architecture (sell renders, not source, where possible).
Schema Evolution and Backward Compatibility
Once an agency has a brand.json in production CI, you cannot break its shape in a patch release. Remotion’s Zod integration enforces the schema at parse time, so any field you remove or rename will throw when existing configs try to render.
Use .optional().default() for every new field you add after v1:
// v1
export const BrandSchema = z.object({
primaryColor: zColor().default("#1A1A2E"),
companyName: z.string(),
});
// v2 — adding a field safely
export const BrandSchema = z.object({
primaryColor: zColor().default("#1A1A2E"),
companyName: z.string(),
// New in v2 — default ensures v1 configs still parse
showEndCard: z.boolean().optional().default(true),
endCardDurationFrames: z.number().int().min(15).max(60).optional().default(30),
});
Never remove or rename a field in a minor version. If you need to restructure, publish a v2 schema under a new export name (BrandSchemaV2) and let agencies migrate on their own timeline. A migration helper makes this tractable:
// src/migrate.ts
import { BrandSchema as V1 } from "./schema-v1";
import { BrandSchema as V2 } from "./schema-v2";
export function migrateBrandConfig(raw: unknown): z.infer<typeof V2> {
const v1Result = V1.safeParse(raw);
if (v1Result.success) {
return V2.parse({
...v1Result.data,
showEndCard: true, // sensible v2 default
endCardDurationFrames: 30,
});
}
return V2.parse(raw); // try parsing as v2 directly
}
Delivery Checklist
Before handing a white-label template to an agency, run through each item:
Schema and props
- All customizable values are in the Zod schema with explicit
.default()values - No animation constants (timing curves, stiffness values, pixel offsets) are hardcoded in component files; they belong in the schema or in a named constants file the agency does not need to touch
-
logoPathand any asset references are validated to be relative paths
Fonts and assets
- Template renders correctly with system font stacks on macOS, Linux (Lambda), and Windows without any external
@importor<link>dependencies - If a custom typeface is required, a
useBrandFonthook withdelayRender/continueRenderis in place - Font licensing terms are documented in the handoff README
Licensing state
-
LicenseWatermarkrenders on anyinputPropsthat omits or failslicenseToken - License terms explicitly state: what the agency may render (unlimited renders for their clients? capped volume?), whether they may sub-license the template itself (typically: no), and which schema version the license covers
Render isolation
- All client-specific assets live under
public/<client-id>/rather thanpublic/root so that a misconfigured path defaults to a visible error rather than silently loading another client’s logo - The
serveUrlused in Lambda renders points to a specific deployed bundle version, notlatest, so a schema-breaking template update cannot retroactively break an agency’s production pipeline
Documentation
- A
brand.jsonreference file is included that passes schema validation and produces a visible test render - The README specifies the minimum
@remotion/coreversion the template is tested against (Remotion has minor-version breaking changes in composition APIs, so pinning is essential)
Wrapping Up
The structural advantage of Remotion for white-label work is that the agency never needs to understand frame math to get a correct branded output. They hand you a JSON file and get a deterministic video back. But that only holds if you architect the handoff surface deliberately: a validated Zod schema that owns the brand tier, self-contained font loading that does not reach external servers, and license state baked into the composition rather than trusting environment configuration.
The checklist above is the same one used when structuring templates like those in the RenderComp catalog for multi-agency distribution. The short version: sell renders where you can, sell inputProps contracts where you must, and sell source only when you have priced the support cost of a buyer who will inevitably edit the wrong layer.
Schema versioning is the piece most authors skip and most regret. Add .optional().default() to every new field from day one. Retrofitting backward compatibility onto a schema that ten agencies already have in production CI generates the kind of support burden that compounds quickly with each new integration.
Now available
Get 1,000+ Remotion Templates
Pay once — no subscription. Lifetime updates. TypeScript-first.
View pricing →