Skip to content
anouk GitHub

← Back to writing

What ships in 5 minutes of scaffolding vs 5 days of hand-rolling

Skelf-Research · ·
scaffoldingdeveloper-experience

The tagline “build AI-powered browser extensions in minutes, not days” sounds like marketing because it is marketing. It is also accurate, and the accounting is more concrete than you might expect. This post walks through the difference, line by line, between what anouk init hands you in roughly five minutes and what you would build over roughly five days starting from manifest.json.

This is not a benchmark. It is a budgeting exercise. The point is to be honest about which work is being skipped and which work is genuinely shared.

The five-day version

Imagine you have decided to ship an AI feature inside a browser extension and you are starting from a blank directory. Day-by-day, here is what you are actually doing.

Day one — manifest and bundling. You write a Manifest V3 file, decide what to put in host_permissions, set up web_accessible_resources for any script you want to inject, and add a content_scripts block scoped to the page you want to augment. You wire up esbuild (or Vite, or Rollup) to bundle your content script and background worker into the dist/ folder the manifest is pointing at. You discover that you forgot the source map flag and add it. You load the extension unpacked, find the typo in matches, and fix it.

By the end of the day you have an extension that injects a console.log into the right pages.

Day two — fetch wrapper and provider config. You pick a provider, hardcode an endpoint, and write a fetch wrapper. You realize that you want to support multiple providers, so you back the URL out into a config object. You realize that the providers have slightly different request shapes, so you write a small adapter — or, more pragmatically, you only support the OpenAI chat-completions shape and rely on the fact that most providers ship a compatible endpoint. You add a system prompt field. You add a model field. You hardcode an API key for testing and tell yourself you will fix it before you ship.

By the end of the day you have a working call from the extension to a model.

Day three — settings panel. You decide that hardcoding the key was a bad idea and build a settings panel. You add a button to your sidebar that opens it. You decide where to store the settings (chrome.storage.local, probably). You write the load and save functions. You add a form. You discover that updating the system prompt requires reloading the extension because the AIService cached the old config, so you write a re-read shim. You add a “Test connection” button. You add a “Reset to defaults” button. You decide which fields are required and add validation. You ship it.

By the end of the day, users can configure their own provider at runtime.

Day four — caching and rate limiting. You notice that your extension is calling the model every time the sidebar renders, which is too often. You add a cache, keyed by some combination of the page URL and the prompt. You realize that the cache key should be stable, so you add a per-request id that your extension controls. You notice that a click-heavy user can fire ten requests in a second, blowing through your rate limit, so you add a per-provider queue. You decide whether the queue should serialize or limit by concurrency. You pick concurrency. You decide what happens to in-flight requests when the user navigates away. You handle that. You add a small in-memory cache eviction policy because the cache is going to grow unbounded otherwise.

By the end of the day your extension is well-behaved under load.

Day five — packaging and review. You read the Chrome Web Store review guidelines. You discover that your host_permissions are too broad. You scope them down to the specific provider domains. You discover that your content security policy is implicit and probably wrong. You fix it. You write a privacy policy. You take screenshots. You write store copy. You submit. You wait. You fix the one thing the reviewer flagged. You ship.

By the end of the day, the extension is live.

That is five days, and it assumes nothing weird happens. It is not unrealistic. It is approximately what shipping a small AI extension looks like, from blank directory to store listing.

The five-minute version

anouk init my-ai-extension and cd my-ai-extension. Three commands later you have:

  • A working Manifest V3 file with web_accessible_resources already wired up for the bundled scripts.
  • An aiService.js that speaks the OpenAI chat-completions request shape, so it works with OpenAI, Anthropic, Together.xyz, Ollama, and Hugging Face Inference with just a URL change.
  • A configManager.js that owns the provider, key, model, and system prompt and persists them to extension storage.
  • A settingsPanel.js that gives the user a runtime form for those fields. Add a button to your UI and they can open it.
  • A bundled cache, keyed per request id and cache key, so re-running the same prompt does not re-spend.
  • A per-provider queue so a click-heavy user does not stampede the API.
  • An esbuild script that bundles the content script and the AI service into dist/.
  • A working unpacked extension that you can load into chrome://extensions right now.

npm run build, toggle developer mode, load unpacked. You’re running. You spent five minutes.

What is honestly the same

The framework does not skip the parts that are actually about your product.

You still write the extension logic. The content script that decides when to call the model, what slice of the DOM to send, and where to put the answer — that is yours. The CSS for the sidebar is yours. The decision about what the system prompt should be is yours. The decision about which page to inject into is yours. The decision about whether you publish to the Chrome Web Store and whether you charge for it is yours.

The framework does not write your prompt. It does not pick your model. It does not decide your pricing. It does not know what your feature does.

It does the boilerplate. That’s it. But the boilerplate, for this category of product, is the part that ate the five days.

What you save by not deferring

The most underrated thing about scaffolding is that it lets you make decisions in the right order. When you spend five days on plumbing, you make all your AI feature decisions in the last hour, when you are tired and your taste is low. When you spend five minutes on plumbing, you spend the next five days on the feature itself, when your taste is best.

The first version of your sidebar copy will be better. The first prompt will be better. The first model choice will be better. You will notice that the response is too long and add a length constraint, instead of shipping a version with a 600-token blob in a 300-pixel sidebar because you ran out of time.

This is the actual value proposition. Not “we built fetch for you” — anyone can build fetch. The value is that the plumbing was a tax on the part of your product that you have a comparative advantage in. Removing the tax means more of the part that matters gets your attention.

What we still recommend you customize

Two things are worth touching even on a five-minute build.

First, the host_permissions in the manifest. The default scaffold lists the common provider endpoints. If you only use one, strip the rest. It speeds up the store review and reduces the surface area you have to defend in a privacy policy.

Second, the cache key shape. The default cache key combines a request id and a cache key string you supply. Think about what makes a “same request” in your feature. If the answer should be re-fetched when the page changes, include something page-derived in the cache key. If the answer is stable for a piece of content, key it by a hash of the content. Defaults are sane; the right keys are domain-specific.

Both of these take minutes, not days. They are the kind of decision worth making early because they are easy to forget later.

The point

The framework is not magic. The five-day version is not impossibly hard. The difference is where you spend the days. anouk init is a forcing function to make sure you spend them on the feature.