<?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: fu bront</title>
    <description>The latest articles on DEV Community by fu bront (@fu_bront_a2bc935343b8b06e).</description>
    <link>https://dev.to/fu_bront_a2bc935343b8b06e</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4055377%2F68198635-6bc8-41d2-8395-b51d056ffe7a.png</url>
      <title>DEV Community: fu bront</title>
      <link>https://dev.to/fu_bront_a2bc935343b8b06e</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fu_bront_a2bc935343b8b06e"/>
    <language>en</language>
    <item>
      <title>What I Learned Building an AI Background Remover on Cloudflare</title>
      <dc:creator>fu bront</dc:creator>
      <pubDate>Thu, 30 Jul 2026 16:15:01 +0000</pubDate>
      <link>https://dev.to/fu_bront_a2bc935343b8b06e/what-i-learned-building-an-ai-background-remover-on-cloudflare-2l74</link>
      <guid>https://dev.to/fu_bront_a2bc935343b8b06e/what-i-learned-building-an-ai-background-remover-on-cloudflare-2l74</guid>
      <description>&lt;p&gt;I recently shipped &lt;a href="https://imagebackground.site/" rel="noopener noreferrer"&gt;CutoutKit&lt;/a&gt;, a small web app that removes image backgrounds and returns a transparent PNG.&lt;/p&gt;

&lt;p&gt;The interface is intentionally simple: upload an image, wait a few seconds, and download the result. The implementation behind that flow was less simple. Once I moved past the prototype, I needed authentication, usage limits, payments, webhook reliability, privacy boundaries, and an architecture that could run comfortably on Cloudflare.&lt;/p&gt;

&lt;p&gt;This post covers the decisions and mistakes that were most useful to me. It is not a tutorial for a specific starter repository; it is a set of patterns you can reuse in other API-backed SaaS products.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraints shaped the architecture
&lt;/h2&gt;

&lt;p&gt;I started with a few deliberate constraints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The frontend would use Next.js, React, and Tailwind CSS.&lt;/li&gt;
&lt;li&gt;The application would run on Cloudflare Pages and its server-side runtime.&lt;/li&gt;
&lt;li&gt;Uploaded images would not be persisted by the application.&lt;/li&gt;
&lt;li&gt;Background removal would be delegated to the remove.bg API.&lt;/li&gt;
&lt;li&gt;Google OAuth would identify users.&lt;/li&gt;
&lt;li&gt;Cloudflare D1 would store users, sessions, usage, and purchases.&lt;/li&gt;
&lt;li&gt;Paid access would use one-time credit packs instead of recurring subscriptions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The resulting request flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser
  |
  +--&amp;gt; Next.js UI on Cloudflare
  |
  +--&amp;gt; Cloudflare server endpoint
          |-- verify session and request
          |-- reserve one usage credit in D1
          |-- send the image to remove.bg
          |-- stream the processed PNG back
          `-- release the reservation if processing fails

Payment provider webhook
  `--&amp;gt; verify signature --&amp;gt; record event --&amp;gt; grant credits in D1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One important privacy detail: “not stored” does not mean “never leaves the browser.” The uploaded file is sent through my server endpoint to the image-processing provider. My application does not write the original or processed image to a database, object store, or disk, but the processor still receives it to perform the requested operation. That distinction belongs in both the product copy and the privacy policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep third-party API keys behind a server boundary
&lt;/h2&gt;

&lt;p&gt;The first production rule is obvious but easy to violate during a quick prototype: never call a paid processing API directly from browser code if it requires a secret key.&lt;/p&gt;

&lt;p&gt;The browser uploads a &lt;code&gt;multipart/form-data&lt;/code&gt; request to a same-origin server endpoint. That endpoint reads the remove.bg key from a Cloudflare secret, adds it to the upstream request, and returns the image response. The key is never included in the JavaScript bundle or exposed through a public configuration endpoint.&lt;/p&gt;

&lt;p&gt;The proxy also became the right place to enforce the application’s rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Require an authenticated user.&lt;/li&gt;
&lt;li&gt;Reject cross-origin requests.&lt;/li&gt;
&lt;li&gt;Accept only &lt;code&gt;multipart/form-data&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Limit the request to 22 MB.&lt;/li&gt;
&lt;li&gt;Allow only JPEG, PNG, and WebP MIME types.&lt;/li&gt;
&lt;li&gt;Optionally verify a Turnstile token.&lt;/li&gt;
&lt;li&gt;Apply a short-term IP rate limit.&lt;/li&gt;
&lt;li&gt;Check and reserve the user’s available credit.&lt;/li&gt;
&lt;li&gt;Add a timeout to the upstream API request.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Client-side validation still improves the experience, but it is not a security boundary. Every meaningful rule is repeated on the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reserve usage before doing expensive work
&lt;/h2&gt;

&lt;p&gt;Usage accounting looks trivial until two requests arrive at nearly the same time.&lt;/p&gt;

&lt;p&gt;A fragile implementation follows this sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;read remaining credits
call expensive API
increment usage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two concurrent requests can both see the same remaining credit and both proceed. The better pattern is to reserve usage before calling the paid API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;reserve credit atomically
call upstream processor
confirm reservation on success
release reservation on failure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives the request a small transaction-like lifecycle even though the database operation and external API call cannot share a real database transaction.&lt;/p&gt;

&lt;p&gt;The failure path matters as much as the success path. A timeout, rejected file, upstream outage, or exhausted provider balance should not charge the user. In my handler, upstream failures trigger a best-effort release of the reservation before returning a normalized error to the client.&lt;/p&gt;

&lt;p&gt;This pattern is useful for any metered API product: image generation, transcription, document conversion, email verification, or LLM calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Google OAuth on an edge deployment
&lt;/h2&gt;

&lt;p&gt;Google OAuth is conventional, but custom domains and preview domains create a sharp edge: the redirect URI must match exactly.&lt;/p&gt;

&lt;p&gt;My application builds the callback from a single canonical application origin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://imagebackground.site/api/auth/google/callback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That exact URI must appear in the Google OAuth client configuration. Differences in protocol, hostname, port, path, or trailing characters cause &lt;code&gt;redirect_uri_mismatch&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The login flow uses an OAuth state value stored in a short-lived, HTTP-only cookie. The callback compares that cookie with the returned state before exchanging the authorization code. After retrieving the Google profile, the app upserts the user in D1 and creates a server-side session.&lt;/p&gt;

&lt;p&gt;The browser receives a signed session identifier in a secure, HTTP-only cookie. It does not receive the Google access token, and the application does not need to keep that token after fetching the profile.&lt;/p&gt;

&lt;p&gt;D1 stores only the durable product data the app needs: the Google subject identifier, email, display information, sessions, usage events, and purchases.&lt;/p&gt;

&lt;h2&gt;
  
  
  One-time credit packs simplified the billing model
&lt;/h2&gt;

&lt;p&gt;I chose 30-day credit packs rather than automatic subscriptions. The current model has a small free allowance and two paid packs. A purchase adds a fixed number of credits with an expiry date; it does not create recurring billing.&lt;/p&gt;

&lt;p&gt;Supporting both PayPal and Creem helped me separate the payment provider from the entitlement model. The provider collects money, but my own database decides whether a user may process an image.&lt;/p&gt;

&lt;p&gt;The common flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The signed-in user selects a plan.&lt;/li&gt;
&lt;li&gt;The server maps the public plan ID to a server-owned price and credit count.&lt;/li&gt;
&lt;li&gt;The server creates a checkout or order with an internal purchase ID.&lt;/li&gt;
&lt;li&gt;D1 records the purchase as &lt;code&gt;pending&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The provider completes the payment.&lt;/li&gt;
&lt;li&gt;A verified server callback marks the purchase complete and grants the entitlement.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The browser never submits an authoritative amount or number of credits. It submits only a plan identifier, and the server looks up the plan details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat payment webhooks as hostile and repetitive
&lt;/h2&gt;

&lt;p&gt;A payment success page is useful for the user experience, but it is not a reliable source of truth. The customer can close the tab, lose connectivity, or manipulate a browser request. Webhooks are the durable confirmation channel.&lt;/p&gt;

&lt;p&gt;My webhook handlers follow four rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verify the provider signature before doing any work.&lt;/li&gt;
&lt;li&gt;Store every provider event ID.&lt;/li&gt;
&lt;li&gt;Make an already completed event return success without granting credits again.&lt;/li&gt;
&lt;li&gt;Link the event back to a purchase created by the server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is idempotency in practical form. Payment providers retry webhooks, and your endpoint must assume the same event can arrive more than once.&lt;/p&gt;

&lt;p&gt;I also store processing status for each webhook event. If an event fails halfway through, it is visible and can be retried without pretending it never arrived.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloudflare was more than static hosting
&lt;/h2&gt;

&lt;p&gt;The public pages are statically rendered, while the authenticated and image-processing routes run at the edge. Cloudflare D1 provides the relational state, and environment secrets keep provider credentials out of source control.&lt;/p&gt;

&lt;p&gt;The deployment also has one canonical host. Requests for the &lt;code&gt;www&lt;/code&gt; hostname and the Pages preview hostname redirect to the apex domain. This fixed several subtle issues at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OAuth callbacks consistently use one origin.&lt;/li&gt;
&lt;li&gt;Session cookies behave predictably.&lt;/li&gt;
&lt;li&gt;Search engines see one canonical version of each page.&lt;/li&gt;
&lt;li&gt;Analytics does not fragment the same product across hostnames.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I added a generated sitemap, &lt;code&gt;robots.txt&lt;/code&gt;, canonical metadata, structured data, a favicon, and focused landing pages for real use cases such as product photos and transparent PNG creation. SEO did not change the application architecture, but the canonical-host decision definitely did.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do earlier next time
&lt;/h2&gt;

&lt;p&gt;If I built another API-backed SaaS, I would make these decisions before polishing the interface:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Define the privacy boundary precisely.&lt;/strong&gt; List every system that receives user content and distinguish processing from persistence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design usage as a state machine.&lt;/strong&gt; Think in terms of reserved, completed, and released usage—not a single counter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose the canonical origin immediately.&lt;/strong&gt; Use it for OAuth, cookies, payment return URLs, metadata, and redirects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make webhooks idempotent from day one.&lt;/strong&gt; Retrofitting event deduplication after real payments is unnecessarily stressful.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep plans on the server.&lt;/strong&gt; Prices, currencies, credits, and expiration rules should never be trusted from the browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Normalize third-party failures.&lt;/strong&gt; Users need actionable product errors, not raw provider responses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test the failure path.&lt;/strong&gt; Timeouts, duplicate callbacks, invalid signatures, and exhausted quotas are normal production states.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;The background-removal model was not the hardest part of this product because I deliberately used a specialized API. The interesting engineering work was everything around it: protecting the API boundary, accounting for usage correctly, handling identity at the edge, and turning payment notifications into reliable entitlements.&lt;/p&gt;

&lt;p&gt;That is also encouraging. A small product does not need a huge infrastructure stack, but it does need clear trust boundaries and careful state transitions.&lt;/p&gt;

&lt;p&gt;You can try the finished implementation at &lt;a href="https://imagebackground.site/" rel="noopener noreferrer"&gt;CutoutKit&lt;/a&gt;. If you are building a similar API-backed tool, I would be interested to hear which part caused the most trouble for you.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>cloudflare</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
