<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: quickconv</title>
    <description>The latest articles on DEV Community by quickconv (@cc_quickconv_ff5b94a1d015).</description>
    <link>https://dev.to/cc_quickconv_ff5b94a1d015</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3884489%2Fdeeeed19-f1ff-48e7-bd96-4e922c2c08a6.jpg</url>
      <title>DEV Community: quickconv</title>
      <link>https://dev.to/cc_quickconv_ff5b94a1d015</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cc_quickconv_ff5b94a1d015"/>
    <language>en</language>
    <item>
      <title>I Built an Image Conversion SaaS on (Almost) $0/Month — Here's the Full Stack</title>
      <dc:creator>quickconv</dc:creator>
      <pubDate>Fri, 17 Apr 2026 12:37:03 +0000</pubDate>
      <link>https://dev.to/cc_quickconv_ff5b94a1d015/i-built-an-image-conversion-saas-on-almost-0month-heres-the-full-stack-4o2i</link>
      <guid>https://dev.to/cc_quickconv_ff5b94a1d015/i-built-an-image-conversion-saas-on-almost-0month-heres-the-full-stack-4o2i</guid>
      <description>&lt;h1&gt;
  
  
  I Built an Image Conversion SaaS on (Almost) $0/Month — Here's the Full Stack
&lt;/h1&gt;

&lt;p&gt;A few months ago I got frustrated trying to convert a HEIC photo on my iPhone into something my client could actually open. The tools I found were either slow, ugly, or locked behind a paywall after one use. So I built &lt;a href="https://quickconv.cc" rel="noopener noreferrer"&gt;QuickConv&lt;/a&gt; — a file conversion service focused on next-generation image formats like WebP, AVIF, and HEIC.&lt;/p&gt;

&lt;p&gt;This post is a full technical breakdown: what I chose, why I chose it, and what I'd do differently. No marketing fluff.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;Here's the bird's-eye view:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser (Next.js static)
        │  HTTPS
        ▼
Cloudflare Pages  (CDN edge, static HTML/JS)
        │
        │  API calls
        ▼
Cloudflare Workers  (Hono — api.quickconv.cc)
        │               │
        │ R2 presign     │ convert job dispatch
        ▼               ▼
Cloudflare R2     GCP Cloud Run  (Sharp / FFmpeg / Ghostscript)
(file storage)          │
        ▲               │ callback on completion
        └───────────────┘
              │
              ▼
        Cloudflare D1
        (job state, rate limits, users)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key design decision: &lt;strong&gt;split sharp-based conversion into a separate container&lt;/strong&gt; rather than running it inside Workers. That single choice drove most of the rest of the architecture.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frontend: Next.js with Static Export
&lt;/h2&gt;

&lt;p&gt;The frontend is a Next.js 15 App Router app. The entire build is configured as a static export:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/web/next.config.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;export&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;productionBrowserSourceMaps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;output: "export"&lt;/code&gt; means &lt;code&gt;next build&lt;/code&gt; produces a directory of static HTML, JS, and CSS — no Node.js server required. That output gets deployed directly to Cloudflare Pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why static export?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cloudflare Pages serves static assets from its global CDN at no compute cost&lt;/li&gt;
&lt;li&gt;No cold starts, no server to manage&lt;/li&gt;
&lt;li&gt;Pages has a generous free tier (unlimited requests for static assets)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tradeoff: no server-side rendering per request. For a conversion tool this is fine — the page content doesn't change per user. Dynamic data (job status, user account) is fetched client-side from the API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Internationalization&lt;/strong&gt; is handled by &lt;code&gt;next-intl&lt;/code&gt; with &lt;code&gt;ja&lt;/code&gt; and &lt;code&gt;en&lt;/code&gt; locale support baked into the static export. The locale is resolved from the URL path (&lt;code&gt;/en/&lt;/code&gt;, &lt;code&gt;/ja/&lt;/code&gt;).&lt;/p&gt;




&lt;h2&gt;
  
  
  API Layer: Hono on Cloudflare Workers
&lt;/h2&gt;

&lt;p&gt;The API is a &lt;a href="https://hono.dev/" rel="noopener noreferrer"&gt;Hono&lt;/a&gt; application deployed to Cloudflare Workers at &lt;code&gt;api.quickconv.cc&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/api/upload    — receive file, store in R2
/api/convert   — create job, dispatch to converter
/api/status    — poll job state from D1
/api/download  — stream converted file from R2
/api/auth      — Google OAuth (JWT cookie)
/api/checkout  — Stripe checkout session
/api/webhook   — Stripe webhook handler
/api/account   — subscription info
/v1/convert    — public API for developers (API key auth)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why Hono instead of Express or Fastify?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Three reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Workers compatibility.&lt;/strong&gt; Express and Fastify are built around Node.js APIs (&lt;code&gt;http.IncomingMessage&lt;/code&gt;, &lt;code&gt;Buffer&lt;/code&gt;, etc.) that don't exist in the Workers runtime. Hono is built on the Web Standard APIs (&lt;code&gt;Request&lt;/code&gt;, &lt;code&gt;Response&lt;/code&gt;, &lt;code&gt;Headers&lt;/code&gt;) that Workers natively support. No polyfills, no shims.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;TypeScript ergonomics.&lt;/strong&gt; Hono has excellent type inference for route handlers, middleware, and context variables. The &lt;code&gt;Bindings&lt;/code&gt; and &lt;code&gt;Variables&lt;/code&gt; generics let me type-check R2 bindings, D1 bindings, and middleware-set values at compile time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Size.&lt;/strong&gt; Workers have a 1MB (compressed) script size limit. Hono is tiny.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The middleware stack in order: Sentry → CORS → identification → optional auth → cost guard → rate limit.&lt;/p&gt;

&lt;p&gt;The identification middleware generates a stable &lt;code&gt;clientHash&lt;/code&gt; from IP + User-Agent (hashed, no PII stored) to track anonymous usage for rate limiting without requiring login.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conversion Engine: Sharp on GCP Cloud Run
&lt;/h2&gt;

&lt;p&gt;This is where things get interesting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not run Sharp in Workers?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Workers run in V8 isolates — a JavaScript-only sandbox. Sharp is a native Node.js addon built on &lt;code&gt;libvips&lt;/code&gt;. Native binaries cannot run in V8 isolates. You can run WebAssembly in Workers, and there is a &lt;code&gt;sharp&lt;/code&gt; WASM build, but as of writing it doesn't support AVIF encoding and is significantly slower for large images.&lt;/p&gt;

&lt;p&gt;For a service where the core value proposition is "convert HEIC/AVIF fast," that's a non-starter.&lt;/p&gt;

&lt;p&gt;The solution: a separate container on Cloud Run.&lt;/p&gt;

&lt;p&gt;The converter is itself a small Hono app (&lt;code&gt;@hono/node-server&lt;/code&gt;) that runs on Node.js 22 with native Sharp:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:22-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="c"&gt;# ... build stage ...&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:22-slim&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    libvips-dev &lt;span class="se"&gt;\
&lt;/span&gt;    ffmpeg &lt;span class="se"&gt;\
&lt;/span&gt;    ghostscript
&lt;span class="c"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note &lt;code&gt;ffmpeg&lt;/code&gt; and &lt;code&gt;ghostscript&lt;/code&gt; are also installed — this handles video conversions and PDF operations respectively.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The conversion flow:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Worker receives &lt;code&gt;/api/convert&lt;/code&gt; request&lt;/li&gt;
&lt;li&gt;For images/audio: Worker fetches the file from R2, POSTs it to Cloud Run (&lt;code&gt;/convert/direct&lt;/code&gt;), streams back the result&lt;/li&gt;
&lt;li&gt;For videos (which can take minutes): Worker dispatches an async job via &lt;code&gt;c.executionCtx.waitUntil()&lt;/code&gt;, Cloud Run pulls the file from R2 directly, converts, writes back to R2, then POSTs a callback to the Worker to update the D1 job record&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Workers have a 30-second CPU time limit. &lt;code&gt;waitUntil()&lt;/code&gt; extends this for background work, but video conversions can still exceed it. The async R2-based flow for video sidesteps this entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Image quality defaults (from the actual code):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_QUALITY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;jpg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// mozjpeg&lt;/span&gt;
  &lt;span class="na"&gt;webp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;avif&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// AVIF compresses aggressively; 65 looks good&lt;/span&gt;
  &lt;span class="na"&gt;tiff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AVIF at quality 65 typically produces files 50–70% smaller than JPEG at equivalent perceptual quality. That compression ratio is why I focused on next-gen formats.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloud Run configuration:&lt;/strong&gt; &lt;code&gt;max-instances=1&lt;/code&gt;. This is intentional cost control during early stages. A single instance handles queue sequentially. As traffic grows this will increase, but starting at 1 means zero idle cost outside the free tier.&lt;/p&gt;




&lt;h2&gt;
  
  
  Storage: Cloudflare R2
&lt;/h2&gt;

&lt;p&gt;All files — uploaded originals and converted outputs — live in R2 (&lt;code&gt;quickconv-files&lt;/code&gt; bucket).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why R2 over S3?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One reason: &lt;strong&gt;zero egress fees&lt;/strong&gt;. S3 charges ~$0.09/GB for data transferred out. For a conversion service, every download is egress. R2 charges $0 for egress to the internet.&lt;/p&gt;

&lt;p&gt;The math:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1,000 conversions/day × avg 2MB output = 2GB/day egress&lt;/li&gt;
&lt;li&gt;S3: ~$5.40/month just for egress&lt;/li&gt;
&lt;li&gt;R2: $0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Storage cost is $0.015/GB/month after the 10GB free tier. At current scale, I'm in the free tier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;24-hour auto-delete:&lt;/strong&gt; Files are automatically expired after 24 hours. This is configured as an R2 lifecycle rule, not application-level deletion. It runs even if the API is down.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;FILE_EXPIRY_HOURS = 24&lt;/code&gt; constant in the shared package is purely for setting the &lt;code&gt;expiresAt&lt;/code&gt; field in D1 (for display purposes). The actual deletion is infrastructure-level.&lt;/p&gt;




&lt;h2&gt;
  
  
  Database: Cloudflare D1
&lt;/h2&gt;

&lt;p&gt;D1 is Cloudflare's serverless SQLite. I use it for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Job records (status, input/output R2 keys, timestamps)&lt;/li&gt;
&lt;li&gt;Rate limit counters (daily conversions per &lt;code&gt;clientHash&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;User accounts (email, Stripe customer ID, plan)&lt;/li&gt;
&lt;li&gt;Video conversion monthly counters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why D1 and not Postgres/PlanetScale/Turso?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For a Workers-native app, D1 is the obvious choice: zero latency to bind, no connection pooling issues (SQLite has no connection limit), and it's free for the first 5M rows/month.&lt;/p&gt;

&lt;p&gt;The schema is managed with Wrangler migrations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx wrangler d1 migrations apply quickconv-db &lt;span class="nt"&gt;--remote&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SQLite's single-writer model is fine for this workload. Rate limit increments use &lt;code&gt;INSERT OR REPLACE&lt;/code&gt; patterns that are safe under SQLite's serialized writes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Payments: Stripe
&lt;/h2&gt;

&lt;p&gt;Stripe handles all billing. The checkout flow: Workers creates a Stripe Checkout Session, redirects the user, Stripe POSTs to &lt;code&gt;/api/webhook&lt;/code&gt; on completion, Worker updates the D1 user record with the plan.&lt;/p&gt;




&lt;h2&gt;
  
  
  Developer API
&lt;/h2&gt;

&lt;p&gt;One thing I added recently that I'm excited about: a public &lt;code&gt;/v1/convert&lt;/code&gt; endpoint for developers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.quickconv.cc/v1/convert &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer qc_YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"file=@photo.jpg"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"output_format=webp"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Developers get an API key from their account dashboard. The key is authenticated via middleware, rate-limited per plan, and the conversion hits the same Cloud Run backend. No separate infrastructure — the same Sharp container serves both the UI and the API.&lt;/p&gt;

&lt;p&gt;This was about 2 days of work on top of the existing backend. The hard part (conversion, storage, rate limiting) was already done.&lt;/p&gt;




&lt;h2&gt;
  
  
  Monitoring: Sentry
&lt;/h2&gt;

&lt;p&gt;Sentry is initialized in all three components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt; (&lt;code&gt;@sentry/nextjs&lt;/code&gt;) — captures unhandled errors and route transitions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Workers&lt;/strong&gt; (&lt;code&gt;@sentry/cloudflare&lt;/code&gt;) — captures Worker exceptions with breadcrumbs per conversion&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Converter&lt;/strong&gt; (&lt;code&gt;@sentry/node&lt;/code&gt;) — captures conversion failures with file size/format context&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The converter adds structured breadcrumbs for every conversion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;addConversionBreadcrumb&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;conversionFormat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;inputFormat&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-to-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;outputFormat&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;durationMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;fileSizeInput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inputBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;fileSizeOutput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes it easy to diagnose which format pairs are slow or failing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost Breakdown
&lt;/h2&gt;

&lt;p&gt;Monthly costs at current (early) scale:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Workers&lt;/td&gt;
&lt;td&gt;$0 (free tier: 100K req/day)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Pages&lt;/td&gt;
&lt;td&gt;$0 (static assets, unlimited)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare R2&lt;/td&gt;
&lt;td&gt;$0 (under 10GB free tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare D1&lt;/td&gt;
&lt;td&gt;$0 (under 5M rows free tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GCP Cloud Run&lt;/td&gt;
&lt;td&gt;$0 (under ~180K vCPU-seconds free/month)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain (quickconv.cc)&lt;/td&gt;
&lt;td&gt;~$1/month amortized&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sentry&lt;/td&gt;
&lt;td&gt;$0 (free tier: 5K errors/month)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$1/month&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The GCP free tier for Cloud Run is 180,000 vCPU-seconds per month. A typical image conversion takes ~0.5–2 seconds of CPU. That's roughly 90,000–360,000 conversions before I pay anything on Cloud Run.&lt;/p&gt;

&lt;p&gt;When I exceed free tiers, the marginal cost is still low: Workers Paid plan is $5/month for 10M requests. R2 storage is $0.015/GB. Cloud Run is ~$0.024 per vCPU-hour.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Don't fight the platform.&lt;/strong&gt; When I first designed the converter, I tried to shoehorn Sharp into a Worker via WASM. It took two days to discover AVIF encoding wasn't supported. Accepting that Workers can't run native binaries and reaching for Cloud Run took an afternoon. Know your runtime's constraints upfront.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Shared types are worth the monorepo overhead.&lt;/strong&gt; The &lt;code&gt;@quickconv/shared&lt;/code&gt; package contains types, format constants, and validation schemas used by both the API Worker and the converter container. Without it, I'd have duplicated ~500 lines and diverged them within a week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Static export is underrated.&lt;/strong&gt; The combination of Next.js static export + Cloudflare Pages is genuinely fast — Time to First Byte from edge nodes is under 50ms globally. For content that doesn't change per request, there's no reason to pay for server-side rendering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. R2 egress pricing changes the math entirely.&lt;/strong&gt; If you're building anything where users download files, compare R2 vs S3 egress costs before assuming S3 is the default. For download-heavy workloads, R2 is often 5–10x cheaper.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. &lt;code&gt;waitUntil()&lt;/code&gt; is not a queue.&lt;/strong&gt; Workers' &lt;code&gt;executionCtx.waitUntil()&lt;/code&gt; lets you run background work after returning a response, but it still has limits (30-second CPU, subject to Workers runtime constraints). For video conversions that might run 5+ minutes, I moved to a true callback pattern: dispatch to Cloud Run, Cloud Run calls back when done.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Format expansion: SVG → PNG, PDF → image batch&lt;/li&gt;
&lt;li&gt;Quality comparison preview (side-by-side before/after slider)&lt;/li&gt;
&lt;li&gt;More language support beyond ja/en&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://quickconv.cc" rel="noopener noreferrer"&gt;quickconv.cc&lt;/a&gt; — free tier is 10 conversions/day, no account required.&lt;/p&gt;

&lt;p&gt;If you're building something that needs image conversion, the &lt;a href="https://quickconv.cc/en/developers" rel="noopener noreferrer"&gt;developer API&lt;/a&gt; is live. Free tier includes 100 conversions/month.&lt;/p&gt;

&lt;p&gt;The stack is deliberately unsexy: Next.js, Hono, Sharp, SQLite. No Kubernetes, no microservices, no Kafka. It serves the use case, runs on almost nothing, and I can reason about the whole thing in my head. That feels like the right place to start.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with Next.js 15, Hono 4, Sharp 0.33, Cloudflare Workers/Pages/R2/D1, GCP Cloud Run, Stripe, Sentry.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>nextjs</category>
      <category>typescript</category>
      <category>saas</category>
    </item>
  </channel>
</rss>
