How I built a highly profitable web platform, but wrote zero backend code to keep my server costs at $0.
Every developer has a folder on their computer filled with abandoned side projects. You know the one-it's where ambitious ideas go to gather dust after the weekend is over. Four years ago, BeGoodTool was just another one of those folders. It didnβt start with a venture capital pitch, a business plan, or a dream to disrupt an industry. It started because I was incredibly frustrated. I was doing some routine development work and needed to quickly format a massive JSON payload and convert a specific image format. I Googled for an online tool, and I was immediately hit with everything we all hate about the modern web: forced sign-ups, features hidden behind sudden paywalls, and the absolute worst offender-having to upload my private, sometimes sensitive files to a random server just to process them. I thought to myself: "I am a frontend developer. Browsers are incredibly powerful computation engines now. Why am I uploading a file to a server so someone else's CPU can parse it? I can build a clean, client-side version of this." So I built a simple utility that ran entirely in the browser. Then I built another one. Then a few colleagues asked for a specific date calculator. Before I knew it, that weekend project snowballed into a massive platform hosting over 70 diverse utilities, supporting 18 languages, and generating a very substantial passive income stream. But scaling a platform with 70+ tools (ranging from heavy WebAssembly-based Optical Character Recognition to complex financial simulators) to a global audience-without paying ridiculous server costs-required some highly unconventional architectural decisions. Instead of writing a generic tutorial, I want to walk you through the exact architecture, the "why" behind the tech stack, and the clever design patterns that allow this massive site to run globally for practically $0 a month. The Architecture Philosophy: "Bring Your Own Compute" When developers scale an application for global traffic, the instinct is to immediately start drawing diagrams filled with Docker containers, Kubernetes clusters, auto-scaling Node.js servers, load balancers, and Redis caches. For a solo indie developer, that is a trap. I didn't want to spend my evenings patching server vulnerabilities. I didn't want to wake up in cold sweats because a sudden spike in traffic crashed my database. And most importantly, I didn't want to pay a massive AWS compute bill just because a tool went viral. So, I made a strict architectural rule: Zero backend processing. Every single tool on the platform-whether it's generating a compressed ZIP file, parsing a massive 50MB CSV, or analyzing the pixels of an image-executes entirely inside the user's browser using their local CPU and RAM. This approach gave me two incredibly unfair advantages: - Absolute Privacy & Zero Compliance Overhead: I don't need complex GDPR infrastructure, secure database compliance, or data-wiping cron jobs because I physically cannot see or store user data. The data never leaves their machine. - Infinite Scale for Free: Whether 100 people or 100,000 people use the site simultaneously, my server compute cost remains exactly zero. The users bring their own compute power to the table. But how do you host a site this large if you have no servers? This is where the magic of stringing together free enterprise tiers comes in. I host the core compiled files on Azure Static Web Apps, which has a generous free tier for static assets. However, Azure Static Web Apps isn't a global CDN by default. To solve global distribution and bandwidth costs, I proxy the entire domain through Cloudflare's Free Tier. Cloudflare acts as my global edge CDN, caching all the static HTML, JS, and CSS assets across its worldwide network. It handles DDoS protection, SSL, and absorbs 99% of the bandwidth. Aside from a very cheap yearly domain name fee, my monthly hosting and bandwidth cost is literally $0. The SEO Scaling Problem: Vue 3 + Vite-SSG The core application is built with Vue 3 (using the Composition API) and Vite. But pure Single Page Applications (SPAs) have a fatal flaw: SEO. If you rely entirely on Javascript to render your DOM on the client side, search engine crawlers often just see a blank . For an indie project relying on organic search, Google traffic is your lifeblood. I needed pre-rendered HTML so crawlers could read my content instantly. However, setting up a traditional SSR (Server-Side Rendering) Node.js server defeated my "zero backend" rule. The solution was Static Site Generation (SSG) using vite-ssg . At build time, my deployment script spins up a headless browser environment, crawls my entire Vue Router configuration, and pre-renders an actual, fully-hydrated index.html file for every single page. Here is where the scale gets crazy. I have 70+ tools, and the site supports 18 languages. 70 tools Γ 18 languages = 1,260+ unique static pages. During npm run build , my pipeline generates over 1,200 perfectly optimized, highly-cacheable HTML files. When a user in Paris searches for a "Timezone Converter," they don't hit my Azure server. Instead, Cloudflare intercepts the request and serves them the pre-rendered French HTML file directly from a local Paris edge node. The Time-to-First-Byte (TTFB) is virtually instant. The Hydration Nightmare Of course, generating 1,200 pages locally comes with severe headaches. When you use SSG, your code is executed in a Node.js environment during the build. This means browser-native APIs like window , document , and HTMLCanvasElement do not exist yet. If you import a third-party library (like a charting tool or an ad script) that immediately tries to touch the DOM on initialization, your entire build crashes with a window is not defined error. I had to architect a rock-solid way to separate "Build-time code" from "Browser-time code." I did this by globally stubbing dangerous components during the SSG build, and dynamically importing them only when the code reaches the client: // A conceptual look at the ViteSSG setup import { ViteSSG } from 'vite-ssg' import App from './App.vue' export const createApp = ViteSSG( App, { routes }, ({ app, isClient }) => { if (!isClient) { // BUILD PHASE: We are in Node.js. // Stub out dangerous components with empty renders to prevent crashes. app.component('ClientOnlyChart', { render: () => null }) app.component('AdSenseWidget', { render: () => null }) } if (isClient) { // BROWSER PHASE: We are on the user's actual device. // Now it is safe to dynamically inject heavy, DOM-dependent libraries. import('heavy-browser-chart-lib').then((module) => { app.component('ClientOnlyChart', module.ChartComponent) }) } } ) Automating Technical SEO at Scale You cannot manually maintain the SEO metadata for 1,200+ pages. If I wanted to update the schema markup structure across the site, editing hundreds of files manually would be a nightmare. I needed a centralized, automated system. I built a standardized wrapper composable that every single tool must use. When I create a new tool, I just pass in its translation key, and the wrapper automatically generates the complex hreflang headers for all 18 languages so Google knows exactly which language version to serve to which country. More importantly, it dynamically injects Googleβs structured data (JSON-LD ), specifically the WebApplication schema, based on the current active locale: // Conceptual logic for the automated SEO composable import { useI18n } from "vue-i18n"; import { useHead } from "@vueuse/head"; export function useToolSEO(toolKey) { const { t, locale } = useI18n(); // Auto-generate Schema.org data for rich Google Snippets const jsonLd = { "@context": "https://schema.org", "@type": "WebApplication", "applicationCategory": "UtilityApplication", "name": t(${toolKey}.title), "description": t(${toolKey}.description), "url": https://begoodtool.com/${toolKey}/${locale.value} }; useHead({ title: t(${toolKey}.title), htmlAttrs: { lang: locale.value }, script: [{ type: 'application/ld+json', children: JSON.stringify(jsonLd) }] }); } By abstracting this at the framework level, Google perfectly understands exactly what each of the 70 tools does, automatically granting the site rich search snippets across global search results without any manual data entry. Taming the Bundle Size with Extreme Chunking If I bundled 70 tools-including WASM OCR binaries, Excel parsers, barcode generators, and charting libraries-into a single app.js file, the payload would be catastrophically large. No user on a mobile network is going to wait 10 seconds to download 15MB of Javascript just to use a simple text character counter. The solution was aggressive, granular chunk splitting via Vue Router. Every single tool is dynamically imported as a completely isolated chunk: // The router splits every tool into an isolated JS file const routes = [ { path: "/tool/regex-tester", component: () => import("@/views/RegexTester/index.vue"), }, { path: "/tool/heavy-image-ocr", component: () => import("@/views/ImageOCR/index.vue"), } ]; Because of this routing architecture, if you visit a lightweight tool, you download zero bytes of the Chart.js or Tesseract.js libraries. Furthermore, I applied this to the vue-i18n localization files. Instead of loading a massive JSON dictionary containing all 18 languages on the initial page load, the app intercepts the router and dynamically fetches only the specific language chunk the user requested. The initial payload remains incredibly small, regardless of how many massive tools I add to the platform. Taming the Main Thread: Web Workers & WASM When you force the client's browser to do all the heavy backend lifting, you run into the classic UI freezing problem. Javascript is natively single-threaded. If a user uploads a 20MB image and you try to run an OCR algorithm on it on the main thread, the browser completely locks up. Vue's reactivity system stops, CSS animations freeze, scrolling breaks, and eventually, the browser throws up
Comments
No comments yet. Start the discussion.