# How I Built an Edge-First AI Cartoon SaaS with Next.js, Cloudflare Workers & D1

> Source: <https://dev.to/peterlee5/how-i-built-an-edge-first-ai-cartoon-saas-with-nextjs-cloudflare-workers-d1-3h6b>
> Published: 2026-10-20 08:11:51+00:00

As an indie maker, launching an AI-powered SaaS often presents a brutal dilemma:

I chose the second path.

Over the past few months, I built and launched **[AI Cartoon Generator](https://aicartoongenerator.io)** — a specialized creative studio designed for cartoon, anime, and comic creators. Instead of relying on a monolithic server, the entire platform runs on **Cloudflare Workers** using **Next.js App Router (via OpenNext)**, **Cloudflare D1**, and **R2**.

In this article, I want to show what we built, lay bare the architectural blueprint, and share the hardest battle-tested lessons learned running Next.js at the edge.

Most generic AI image generators produce stunning one-off pictures, but they fail creators who want to tell sequential stories or build consistent IP:

To solve this, we focused on three core pillars at [AI Cartoon Generator](https://aicartoongenerator.io):

`Character Asset`. Using an active reference anchor, users can generate pose, facial expression, and background variations without losing identity.
A typical result — an ordinary photo restyled as a hand-painted anime scene:

*Input: the user's original photo*

*Output: Ghibli-style transformation, generated on the edge*

And here's the character continuity system in action. This is **one locked `Character Asset`** (Mina the Pocket Illustrator) fed through four independent variation requests — same face, same outfit, different pose, expression, and world:

The storybook pipeline then turns a character like this into a multi-scene illustrated book, with an LLM planning the narrative beats before any image is generated:

*A generated storybook cover (character: Iris Vale, a mountain map archivist)*

While the animation pipeline produces multi-shot storyboard sequences from the same identity anchor:

*Four-scene animation storyboard generated from one character reference*

Running a dynamic Next.js App Router site with heavy background AI orchestration on Cloudflare Workers requires strict isolation. A single Worker trying to handle SSR, image resizing, LLM planning, and webhooks will quickly crash into worker limits.

Here is our production topology:

```
                                  ┌─────────────────────────────┐
                                  │      Cloudflare Edge        │
                                  └──────────────┬──────────────┘
                                                 │
                                                 ▼
                                    ┌─────────────────────────┐
                                    │   Main Site Worker      │
                                    │   (Next.js / OpenNext)  │
                                    └────┬──────────────┬─────┘
                     ┌───────────────────┘              └───────────────────┐
                     ▼                                                      ▼
           ┌───────────────────┐                                  ┌───────────────────┐
           │ Cloudflare D1     │                                  │ Cloudflare R2     │
           │ (Serverless SQL)  │                                  │ (Images & Cache)  │
           └───────────────────┘                                  └───────────────────┘
                     ▲                                                      ▲
                     │                                                      │
    ┌────────────────┼──────────────────────────────────────────────────────┼────────────────┐
    │ Background Queues & Workers                                           │                │
    │                                                                       │                │
    │  ┌───────────────────────┐   ┌────────────────────────────────┐       │                │
    │  │ media-thumbnail-jobs  │──▶│ Main Worker Thumbnail Handler  │───────┤ (WebP 512px)   │
    │  └───────────────────────┘   └────────────────────────────────┘       │                │
    │                                                                       │                │
    │  ┌───────────────────────┐   ┌────────────────────────────────┐       │                │
    │  │ storybook-planning    │──▶│ Standalone Planning Worker     │───────┤ (LLM planning) │
    │  └───────────────────────┘   └────────────────────────────────┘       │                │
    │                                                                       │                │
    │  ┌───────────────────────┐   ┌────────────────────────────────┐       │                │
    │  │ animation-planning    │──▶│ Standalone Planning Worker     │       │                │
    │  └───────────────────────┘   └────────────────────────────────┘       │                │
    │                                                                       │                │
    │  ┌───────────────────────┐   ┌────────────────────────────────┐       │                │
    │  │ storybook-jobs        │──▶│ Video Container Worker (Docker)│───────┘ (MP4 renders)  │
    │  └───────────────────────┘   └────────────────────────────────┘                        │
    └────────────────────────────────────────────────────────────────────────────────────────┘
```

`ai-cartoon-generator`)` batch_size = 1` and isolated memory to plan scripts and scene breakdowns.`linux/amd64`).
Deploying Next.js to Cloudflare Workers via OpenNext isn't just "click deploy and relax." Here are the real challenges we hit in production and how we resolved them:

In our early iterations, the main Next.js worker did everything: served HTML, queried D1, streamed LLM responses for story generation, and processed webhooks.

During traffic spikes or heavy narrative planning tasks, the V8 isolate hit Cloudflare's memory ceiling, resulting in fatal `Error 1102: Worker exceeded memory limits`.

**The Solution:**

Treat the main worker purely as an interactive UI and API layer. We offloaded LLM-heavy streaming pipelines into dedicated planning workers (`aicartoon-storybook-planning` and `aicartoon-animation-planning`) managed by Cloudflare Queues with dead-letter queue (DLQ) fallbacks. If an LLM response runs wild, it never degrades website responsiveness.

`populate-cache` Timeout Nightmare
When using `opennextjs-cloudflare deploy`, the built-in deploy step tries to push pre-rendered HTML/RSC cache entries to R2 using an HTTP proxy. As our localized landing pages and style showcase grew to hundreds of static routes, network throttling caused continuous deploy timeouts.

**The Solution:**

We decoupled cache population from deployment:

`pnpm opennextjs-cloudflare build`.` upload-cache.mjs`) that maps `.open-next/cache` entries to OpenNext cache keys (`next-cache/<buildId>/<sha256(path)>`).` wrangler r2 object put` calls with SHA-256 integrity verification.`OPEN_NEXT_DEPLOY=true wrangler deploy`.
Deployments went from 10-minute nail-biters to a reliable 45-second pipeline.

If you're coming from Postgres or traditional SQLite, you might be accustomed to:

``` js
await db.transaction(async (tx) => {
  await tx.update(...);
  await tx.insert(...);
});
```

On Cloudflare D1, **interactive transactions are not supported**. If a step fails halfway, you risk partial state corruption (e.g., deducting user credits without logging the task).

**The Solution:**

We rely on D1's native `batch()` API combined with deterministic idempotency keys:

```
// Atomic batching ensures all-or-nothing execution across statement batches
const statements = [
  db.prepare(`UPDATE user_credits SET balance = balance - ? WHERE user_id = ? AND balance >= ?`)
    .bind(cost, userId, cost),
  db.prepare(`INSERT INTO ai_tasks (id, user_id, status, idempotency_key) VALUES (?, ?, ?, ?)`)
    .bind(taskId, userId, 'PENDING', idempotencyKey)
];

const results = await d1Binding.batch(statements);
```

With Incremental Static Regeneration (ISR) on the edge (`revalidate = 3600`), the edge cache serves fresh pages immediately after deployment. 

However, if your background revalidation handler or memory queue has a subtle bug (e.g., missing worker self-reference bindings), you won't notice it immediately! An hour later, when pages turn stale and trigger background re-renders, visitors will suddenly encounter `500 Internal Server Errors`.

**The Solution:**

We enforced a two-tier verification workflow:

`pnpm cf:check`)
Moving to an Edge-First serverless architecture paid off tremendously:

Building **[AI Cartoon Generator](https://aicartoongenerator.io)** taught me that the Edge is no longer just for static blogs or simple redirect workers. With OpenNext, D1, R2, and Cloudflare Queues, you can build full-fledged, multi-tenant AI SaaS platforms on edge primitives.

If you are an artist or creator looking for consistent anime and cartoon styles, feel free to give **[AI Cartoon Generator](https://aicartoongenerator.io)** a spin!

*One of the newer style presets: Tim Burton-inspired gothic toons*

**To my fellow engineers:**

I'd love to hear your experiences, war stories, and questions in the comments below! 👇
