Skip to content
All work
PrototypeFull Stack2025

Festival Creatives Platform

Dynamic Creative Generation at Scale

Businesses produce the same branded graphic for every festival and campaign, rebuilt by hand each time. This platform treats the artwork as a template with typed dynamic fields, binds brand data to it, and renders the output through a background job pipeline — so generation is a queued, retryable operation rather than something blocking a web request.

  • TypeScript
  • Turborepo
  • Next.js
  • PostgreSQL
  • Redis
  • BullMQ
01

The problem

Producing branded creative for every occasion is repetitive work that scales linearly with the number of businesses and occasions — the layout barely changes, only the brand, the copy and the date do. Automating it looks trivial until the details land: rendering is slow and memory-hungry, so it cannot happen inside a request; templates need typed variable slots rather than string replacement; brand assets have to be stored, transformed and served at multiple sizes; and a multi-tenant system needs metering so one account's bulk generation does not consume everyone else's capacity.

02

The approach

A monorepo with a clear split between the API, the worker and the web client, sharing types and validation through internal packages so a template schema change cannot silently diverge between producer and consumer. Templates declare typed dynamic fields; a generation request validates data against that schema, creates a job record and enqueues work in BullMQ backed by Redis. Workers consume the queue, render, upload to Cloudinary and write results back, with retries and failure states handled by the queue rather than by application code. The client polls job state and renders progress, so a slow render is a visible pending job instead of a hung request.

03

Architecture

Request and render are deliberately decoupled. The API accepts and enqueues; workers do the expensive work; the client tracks state.

Generation pipeline
5 stages13 nodes
  • Template definitionSource
  • Typed dynamic fieldsSourceshared schema package
  • Next.js clientInterface
  • Express APIProcess
  • Schema validationProcess
  • Job recordStorePrisma / PostgreSQL
  • BullMQ queueStoreRedis
  • Background workerProcess
  • Data binding + renderProcess
  • Retry / backoffProcess
  • Cloudinary uploadStore
  • Job status updateProcess
  • Asset deliveryOutput
Note 01

A template defines layout plus typed dynamic fields — text, image slots, colours. The schema is shared code, so the editor, the API and the renderer agree on what a valid template is.

04

Technical decisions

  • Queue the render instead of doing it inline

    Image composition is slow and memory-hungry, and bulk generation means many at once. Doing that inside a request ties up a web process, risks timeouts on exactly the largest jobs, and makes a single expensive request degrade the whole service. Moving it to BullMQ workers turns generation into a durable, retryable job — the API stays responsive, and capacity scales by adding workers rather than by scaling the web tier.

  • Turborepo with shared contract packages

    Template schemas are consumed by three places at once: the editor that produces them, the API that validates against them, and the worker that renders them. Separate repositories guarantee those definitions drift, and the failure shows up as a corrupted render rather than a type error. A monorepo with the schema in a shared package makes divergence a compile-time failure.

  • Prisma over a hand-rolled data layer

    The domain is relational and reasonably involved — accounts, brands, templates, jobs, assets — and it changes as templates gain capabilities. Prisma's migrations and generated types keep that evolution tractable and keep the schema honest against the code that queries it, which matters more here than the query-level control a lower-level client would give.

  • Cloudinary rather than raw object storage

    Generated assets need multiple sizes and formats per output, plus CDN delivery. Building that transformation pipeline on top of plain object storage is a real project of its own that adds nothing distinctive to this product. Delegating it keeps the work focused on template rendering, at the cost of a vendor dependency in the media path.

05

System capabilities

5 of 8 implemented. The rest are labelled with what they actually are.

  • Template-driven generation

    Implemented

    Templates with typed dynamic fields, rendered by binding structured data rather than editing artwork by hand.

  • Queued background processing

    Implemented

    Rendering runs on BullMQ workers backed by Redis, with retry and failure handling at the queue level.

  • Media pipeline

    Implemented

    Generated assets and brand uploads are stored and transformed through Cloudinary.

  • Monorepo with shared contracts

    Implemented

    Turborepo workspace sharing types and validation between API, worker and web so schemas cannot drift.

  • Multi-tenant data model

    Implemented

    Prisma schema over PostgreSQL modelling accounts, brands, templates and generation jobs.

  • Admin and user workflows

    In progress

    Separate surfaces for template management and end-user generation.

  • Credit metering and subscriptions

    Designed, not built

    Designed model for usage credits and subscription tiers. Not wired to a live payment provider.

  • AI-assisted content generation

    Designed, not built

    Designed integration for generating copy and imagery variants inside a template.

06

Technology stack

Monorepo

  • Turborepo
  • TypeScript
  • Shared packages

Backend

  • Node.js
  • Express
  • Prisma
  • PostgreSQL

Processing

  • Redis
  • BullMQ
  • Background workers

Media

  • Cloudinary
  • Asset pipeline

Frontend

  • Next.js
  • React
  • Tailwind CSS
07

Challenges

The parts that were genuinely hard. Pretending everything went smoothly makes the rest less believable.

  • Templates need structure, not string replacement

    Naive placeholder substitution breaks the moment text overflows its box, a brand supplies a logo with different proportions, or a colour lands on a background it cannot be read against. Dynamic fields have to be typed and constrained by the template rather than dropped in as strings.

  • Job state has to be legible to the user

    Asynchronous rendering means the user is looking at a job, not a result. Queued, processing, complete and failed all need to be represented honestly in the interface — a silent pending state reads as a broken product.

  • Fair capacity across tenants

    Without metering, one account submitting a large batch consumes the worker pool and every other tenant waits. Usage limits are a scheduling concern before they are a billing one.

08

Current status

Prototype

A working prototype. The monorepo, data model, template system, queued rendering pipeline and media handling are implemented. Admin and user workflows are partially built. Credit metering and subscriptions are modelled in the schema but are not connected to a live payment provider, and AI-assisted content generation is designed rather than built.

Next steps

  • Complete the admin template management surface
  • Wire metering to a payment provider before any billing claim is made