Last week, I launched an AI-powered watermark remover that runs entirely on Cloudflare Workers. In this post, I'll walk you through the architecture, challenges, and lessons learned from building a production-ready SaaS with a single-file backend.
Live Demo: https://watermark-remover-cf.shop
GitHub: https://github.com/zhuwei290/watermark-remover-cf
Why Cloudflare Workers?
I wanted to build a privacy-focused image processing tool where:
- Images are never stored on servers (processed in-memory)
- Global low-latency access (edge computing)
- Minimal operational overhead (no servers to manage)
- Cost-effective at scale
Cloudflare Workers checked all these boxes. The free tier gives you 100,000 requests/day, which is perfect for an MVP.
Architecture Overview
Single-File Design
The entire backend is one JavaScript file (worker.js, ~2000 lines) that handles:
┌─────────────────────────────────────────┐
│ Cloudflare Worker │
├─────────────────────────────────────────┤
│ Frontend (HTML/CSS/JS) │
│ ├─ Landing Page │
│ ├─ Dashboard │
│ ├─ Pricing Page │
│ └─ FAQ Page │
├─────────────────────────────────────────┤
│ API Routes │
│ ├─ POST /api/remove (watermark removal)│
│ ├─ GET /api/user (user info) │
│ ├─ POST /api/create-order (PayPal) │
│ └─ POST /api/capture-order (payment) │
├─────────────────────────────────────────┤
│ OAuth Routes │
│ ├─ GET /auth/google │
│ ├─ GET /auth/callback/google │
│ └─ GET /auth/logout │
├─────────────────────────────────────────┤
│ KV Storage │
│ ├─ SESSIONS (user sessions, 7d TTL) │
│ └─ USERS (user data & usage stats) │
└─────────────────────────────────────────┘
Tech Stack
- Runtime: Cloudflare Workers
- Frontend: Vanilla JavaScript + HTML5 + CSS3 (no framework)
- AI Processing: WaveSpeed AI API v3
- Authentication: Google OAuth 2.0
- Payments: PayPal Checkout API v2
- Storage: Cloudflare KV (sessions + user data)
- Deployment: Wrangler CLI + GitHub Actions
Key Implementation Details
1. Routing Without a Framework
Since Workers don't come with Express-like routing, I implemented a simple router:
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const path = url.pathname;
// Static assets
if (path === '/' || path === '/pricing' || path === '/faq') {
return env.ASSETS.fetch(request);
}
// API routes
if (path === '/api/remove') {
return handleRemoveWatermark(request, env);
}
if (path === '/auth/google') {
return handleGoogleAuth(request, env);
}
if (path === '/auth/callback/google') {
return handleGoogleCallback(request, env);
}
// ... more routes
return new Response('Not Found', { status: 404 });
}
};
2. Session Management with KV
User sessions are stored in Cloudflare KV with a 7-day TTL:
async function createSession(userId) {
const sessionId = crypto.randomUUID();
const session = {
userId,
createdAt: Date.now(),
expiresAt: Date.now() + (7 * 24 * 60 * 60 * 1000) // 7 days
};
await env.SESSIONS.put(sessionId, JSON.stringify(session), {
expirationTtl: 7 * 24 * 60 * 60 // 7 days in seconds
});
return sessionId;
}
Sessions are passed via HttpOnly, Secure cookies to prevent XSS attacks.
3. Google OAuth 2.0 Flow
async function handleGoogleAuth(request, env) {
const state = crypto.randomUUID();
// Store state in KV to prevent CSRF
await env.SESSIONS.put(`oauth_state:${state}`, state, {
expirationTtl: 600 // 10 minutes
});
const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
authUrl.searchParams.set('client_id', env.GOOGLE_CLIENT_ID);
authUrl.searchParams.set('redirect_uri', 'https://watermark-remover-cf.shop/auth/callback/google');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'openid email profile');
authUrl.searchParams.set('state', state);
return Response.redirect(authUrl.toString());
}
4. AI Watermark Removal (WaveSpeed API)
The WaveSpeed AI v3 API uses an async task + polling pattern:
async function removeWatermark(imageBuffer, env) {
// Step 1: Submit task
const submitRes = await fetch('https://api.wavespeed.ai/v3/remove-watermark', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.WAVESPEED_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
image: imageBuffer.toString('base64'),
output_format: 'png'
})
});
const { task_id } = await submitRes.json();
// Step 2: Poll for completion
let result;
for (let i = 0; i < 30; i++) {
await sleep(2000); // Wait 2 seconds
const checkRes = await fetch(
`https://api.wavespeed.ai/v3/tasks/${task_id}`,
{
headers: { 'Authorization': `Bearer ${env.WAVESPEED_API_KEY}` }
}
);
const task = await checkRes.json();
if (task.status === 'completed') {
result = task.output_image;
break;
}
}
return result;
}
5. PayPal Payment Integration
async function createPayPalOrder(userId, plan, billingCycle, env) {
const orderData = {
intent: 'CAPTURE',
purchase_units: [{
amount: {
currency_code: 'USD',
value: plan === 'pro' ? '9.9' : '29.9'
},
description: `${plan.toUpperCase()} Plan - ${billingCycle}`
}]
};
// Get PayPal OAuth token
const tokenRes = await fetch('https://api-m.paypal.com/v1/oauth2/token', {
method: 'POST',
headers: {
'Authorization': 'Basic ' + btoa(`${env.PAYPAL_CLIENT_ID}:${env.PAYPAL_SECRET}`),
'Content-Type': 'application/x-www-form-urlencoded'
},
body: 'grant_type=client_credentials'
});
const { access_token } = await tokenRes.json();
// Create order
const orderRes = await fetch('https://api-m.paypal.com/v2/checkout/orders', {
method: 'POST',
headers: {
'Authorization': `Bearer ${access_token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(orderData)
});
return await orderRes.json();
}
Challenges & Solutions
Challenge 1: Static Assets in Workers
Problem: Workers are designed for dynamic logic, not serving static files.
Solution: Use Wrangler's [assets] configuration:
# wrangler.toml
[assets]
directory = "./public"
This automatically serves static files from the ./public directory.
Challenge 2: Large Image Processing
Problem: Workers have a 128MB memory limit per request.
Solution:
- Stream images instead of loading entirely into memory
- Enforce 10MB upload limit on the client side
- Convert large images to compressed formats before processing
Challenge 3: Payment State Management
Problem: PayPal webhooks can be delayed; users expect instant upgrades.
Solution:
- Frontend polls payment status every 3 seconds
- Order state stored in KV with 1-hour TTL
- Webhook serves as backup to update user status
Cost Breakdown
Monthly Operating Costs (at 1000 users/month)
| Service | Cost |
|---|---|
| Cloudflare Workers | $0 (free tier) |
| Cloudflare KV | $0 (free tier) |
| WaveSpeed AI | ~$120 (10,000 images @ $0.012/image) |
| PayPal Fees | ~$150 (3.9% + $0.30 per transaction) |
| Total | ~$270/month |
Revenue Potential
If 5% of users convert to Pro ($9.9/month):
- 50 paying users × $9.9 = $495/month
- Profit: ~$225/month (45% margin)
Lessons Learned
1. Single-File Architecture Works (for MVPs)
Having everything in one file made deployment trivial and debugging straightforward. However, I'm already hitting the limits—2000 lines is getting hard to navigate. Next iteration might split into modules.
2. Edge Computing is Great for Global Users
Users from Asia, Europe, and US all report sub-100ms latency. Cloudflare's edge network is a huge win for user experience.
3. OAuth + Payments = Complexity
Integrating Google OAuth and PayPal took 60% of development time. If I were to do it again, I'd consider using a SaaS like Clerk or Stripe that bundles auth + payments.
4. Privacy is a Selling Point
Emphasizing "images are never stored" resonates with users. Many competitors store uploads for "processing optimization"—this is a key differentiator.
What's Next?
- [ ] Add batch processing (multiple images at once)
- [ ] Implement rate limiting per user tier
- [ ] Add image comparison slider (before/after)
- [ ] Create Chrome extension
- [ ] Migrate sensitive config to Cloudflare Secrets
Try It Out
I'd love your feedback!
🔗 Live Demo: https://watermark-remover-cf.shop
💻 Source Code: https://github.com/zhuwei290/watermark-remover-cf
Questions? Drop them in the comments! 👇
Originally published at watermark-remover-cf.shop

Top comments (0)