TL;DR: Upload a selfie → Gemini analyzes the photo and writes 3 birthday scene prompts → FLUX.2 Pro generates the images → Cloudflare R2 stores them. The whole backend runs on Cloudflare Workers with zero servers.
I recently launched
The Stack
Frontend: Next.js 15 (App Router) → static export → Cloudflare Pages
Backend: Cloudflare Workers (TypeScript)
Database: Cloudflare D1 (SQLite)
Storage: Cloudflare R2
Cache: Cloudflare KV (sessions)
Queue: Cloudflare Queues
AI: Gemini 2.5 Flash (via Replicate) + BFL FLUX.2 Pro
Payment: PayPal
Auth: Google OAuth
The entire backend runs on a single Cloudflare Worker. No EC2, no containers, no ops headaches.
The Core Pipeline
The generation flow looks like this:
User uploads photo
↓
Worker validates + deducts credits + enqueues job
↓
Queue consumer picks it up (runs up to 15 min)
↓
Step 1: Gemini 2.5 Flash analyzes the photo → outputs 3 scene prompts (JSON)
↓
Step 2: Submit 3 FLUX.2 Pro jobs sequentially → each fires a webhook when done
↓
Webhook handler saves images to R2, finalizes task
↓
User polls /api/task/:id → gets results
The key insight: use Cloudflare Queues to break the CPU/time limit of a single Worker request. Workers have a 30-second wall clock limit on HTTP requests, but Queue consumers can run for up to 15 minutes — enough time for Gemini + 3 FLUX calls.
Step 1: Gemini Analyzes the Photo
The hardest part wasn't the image generation. It was writing a prompt good enough to make Gemini output exactly what FLUX needs.
I use Gemini 2.5 Flash via Replicate's API. The system prompt is ~800 words and instructs Gemini to:
- Count people in the foreground (ignore background bystanders)
- Describe each person's face features in detail (for face preservation in the generated image)
- Design 3 completely different birthday scene themes
- Output a structured JSON with
start_prompt,end_prompt, and 3scenes
The JSON structure separates shared prompt parts from scene-specific content:
{
"people_count": 1,
"start_prompt": "This is the same person from the reference photo. Preserve their exact face shape...",
"end_prompt": "Shot on Canon EOS R5, 85mm f/1.4, photorealistic, 8K ultra HD",
"scenes": [
{ "name": "Golden_Gala", "prompt": "Glamorous gold ballroom with 40 metallic gold balloons..." },
{ "name": "Tropical_Paradise", "prompt": "Vibrant beach party setting with palm trees..." },
{ "name": "Enchanted_Garden", "prompt": "Magical outdoor garden with floral arches..." }
]
}
When submitting to FLUX, I assemble the full prompt as:
const fullPrompt = [analysis.start_prompt, scene.prompt, analysis.end_prompt]
.filter(Boolean)
.join(' ');
This makes it easy to tune the shared "face preservation" instructions without touching each scene prompt.
Step 2: FLUX.2 Pro Image Generation
I use — you get 10 free credits on signup (no credit card required).
Happy to answer any questions about the architecture in the comments!

SOCIAL SHARE CARD GENERATOR