DEV Community

Cover image for I Just Shipped My First Chrome Extension (And It Was Harder Than the Code)
Alex Cloudstar
Alex Cloudstar

Posted on

I Just Shipped My First Chrome Extension (And It Was Harder Than the Code)

I just shipped my first Chrome extension.

Not a tutorial toy. Not a "hello world" that never left chrome://extensions. A real Manifest V3 product with Sign in with X, local XP tracking, cloud sync, a privacy policy, and a listing live on the Chrome Web Store.

The product is called XPilot. It turns your activity on X into a game: XP for posts, replies, likes, and reposts. Levels. Streaks. A popup that shows your progress in one click.

And yes: it is free. Add it from the Chrome Web Store, sign in with X, and start earning XP on the stuff you already do. No waitlist. No credit card.

This post is the story of what I expected, what actually broke, and what I would tell myself on day one.

If you are about to build your first extension, steal the lessons. Skip the landmines. If you just want the product: install XPilot free.

Why an extension (and not another web app)

I care about growth on X. Most tools in that space try to write for you. That was never the itch.

The real problem, for me and for a lot of founders I talk to, is consistency. You know you should reply more. You know streaks matter. You still flake when the day gets loud.

So the bet was simple:

Make X feel like a game people already understand: XP, levels, streaks.

And the distribution bet was equally simple:

Put the product where the habit already happens. Inside Chrome. On x.com.

A dashboard you have to remember to open loses to a popup that opens when you are already posting.

That is why XPilot is an extension first, website second.

The stack (keep it boring)

I did not invent a framework.

Extension: WXT + TypeScript, Manifest V3.

UI: plain HTML, CSS, and JS in the popup. Fast, no React tax for v0.1.

Content script: detect my own activity on x.com and twitter.com.

Backend: Next.js on Vercel.

Auth: X OAuth 2.0 with PKCE, then an app JWT.

DB: Postgres on Neon, Drizzle ORM.

Sync: local-first, with a backup about every 12 hours.

WXT was the right call. It handles the annoying MV3 packaging, hot reload in development, and zip for the store. If you are starting today, do not hand-roll Webpack for an extension unless you enjoy pain.

What "done" meant for v0.1

I forced a tiny definition of done:

  1. Sign in with X works
  2. Popup shows XP, level, streak
  3. Activity on X awards XP
  4. Progress survives a reload
  5. Privacy + Terms exist (store requirement)
  6. A ZIP builds without localhost host permissions
  7. A stranger can install from the store and log in

Everything else was a later problem. Opportunity radar, AI coach, seasons: later. Shipping was the boss fight.

Hard part 1: Manifest V3 is not "Chrome apps 2.0"

If your last mental model of extensions is Manifest V2, throw it away.

MV3 service workers go idle. You cannot assume a long-lived background page. Alarms exist for a reason. Persistent connections are not free.

Permissions matter more than you think. Every host permission is a trust tax on the listing review and on users. I kept production hosts to:

  • x.com / twitter.com (where the game runs)
  • xpilot.so / www.xpilot.so (where the API lives)

Dev-only: localhost:3000.

That last detail almost bit me in production. More on that below.

Hard part 2: Sign in with X inside Chrome

Browser OAuth is familiar. Extension OAuth is a different animal.

XPilot uses chrome.identity.launchWebAuthFlow. The flow looks clean on a whiteboard:

  1. Extension opens your site's /api/auth/x
  2. Site redirects to X
  3. X redirects back to your callback
  4. Callback redirects to https://<extension-id>.chromiumapp.org/?code=...
  5. Extension exchanges the code for a JWT

In practice, three things tried to murder me.

1. Callback URL must match APP_URL exactly

Our production site redirects xpilot.sowww.xpilot.so.

If your OAuth redirect_uri is registered as one host and your app builds the other, X shows a vague "you weren't able to give access to the App" page. No stack trace. No mercy.

Fix: pick one canonical origin. Put it in APP_URL. Register that exact callback on developer.x.com. Stop arguing with DNS.

2. Do not put the exchange code in a URL hash

I originally returned the short-lived exchange code as a hash fragment (#code=...). Fragments are cute until redirects strip them and chrome.identity hands you an empty URL.

Fix: put the code in the query string (?code=...). Parse query first, hash as fallback.

3. Host permissions and CORS will gaslight you

Vercel logs told the truth:

  • OPTIONS /api/auth/exchange → 204
  • No POST after it

The store build talked to www, while the manifest only allowed the apex. Chrome fell back to CORS. Preflight "succeeded." The real POST never flew. The popup still showed Sign in with X like nothing happened.

Fix:

  • Default the production API base to https://www.xpilot.so
  • Allow both apex and www in host permissions
  • Add CORS for your published extension ID on the exchange and sync routes as a belt-and-suspenders layer
  • Pin ALLOWED_EXTENSION_IDS in production so random extensions cannot finish OAuth

Also: the popup often closes when the auth window opens. Your background script must finish login even if the UI is gone. When the user reopens the popup, auth should already be in chrome.storage.

Hard part 3: Detecting activity on a hostile DOM

X's frontend is not an API contract. It is a living organism.

Content scripts that award XP for replies have to survive:

  • Timeline composers
  • Modal composers
  • Cmd/Ctrl+Enter submits
  • Buttons that unmount before your observer finishes
  • False positives when someone cancels

I shipped a fail-closed version that broke real replies. Then a looser version that missed intermittent posts. Then a multi-signal version: busy state, clear state, dialog stack, keyboard submit.

If you are building anything on top of a third-party SPA, budget time for "the UI changed and nothing is wrong in your TypeScript."

Hard part 4: Local-first is why a free tier can stay free

I did not want a backend call on every like. I also did not want "free" to mean "runs my GPU bill into the ground."

So XP lives in the extension first. Sync is a backup, not the gameplay loop.

Rules that kept costs and complexity down:

  • Score actions locally
  • Sync when due (about every 12 hours), on startup, and after login
  • Cap what a sync can inflate on the server
  • Never make the free product depend on continuous API traffic

That architecture is why I can ship v0.1 as a free Chrome extension without feeling like every install is a liability. The game still feels instant when the network is sad. The expensive ideas (AI opportunity radar, deep coaching) can wait until people are actually addicted to the loop.

Hard part 5: The Chrome Web Store is a product surface

Shipping code is half the job. The listing is the other half.

Things that mattered more than I wanted them to:

  • Privacy policy URL (live on the site, not a Google Doc)
  • Terms of service
  • Clear permission justifications
  • Screenshots that show the actual popup
  • Category that matches how people browse (we used Social Networking)
  • A ZIP built for production (no localhost permission leftovers)
  • Version bumps when you fix auth (0.1.00.1.1 was not vanity)

Also: trader verification and regional phone issues can block you in weird ways. Budget calendar time for account admin, not just engineering.

What I would do differently next time

  1. Canonical domain on day one. Decide www vs apex before OAuth.
  2. Ship the store ZIP earlier to a private tester. Unpacked success is not store success.
  3. Log the OAuth exchange path. "Still signed out" usually means the token never landed, not that the UI is haunted.
  4. Write privacy/terms while the product is small. It is easier when the data story is short.
  5. Keep v0.1 brutally small. Gamification of real actions beat five AI features I did not ship.

The emotional part (because shipping is emotional)

There is a specific moment when the listing flips from draft to public and the install link works for someone who is not you.

That moment hit harder than any green CI check.

I have shipped websites before. An extension feels different. It lives in the browser chrome. It asks for trust. It sits next to passwords and ad blockers. When someone clicks Add to Chrome, they are not bouncing through a landing page. They are installing you into their daily toolbelt.

That is terrifying. It is also the point.

If you want to try XPilot (it is free)

XPilot is live on the Chrome Web Store. Free to install. Free to use for XP, levels, and streaks.

One minute setup:

  1. Click Add to Chrome
  2. Sign in with X
  3. Use X like you already do
  4. Open the popup when you want to see XP, level, and streak

No waitlist. No "book a demo." No paid wall in front of the core loop.

I am building the game layer for people who are done growing on guilt. v0.1 is the foundation and it is free on purpose: if the habit is not sticky when it costs nothing, it will not be sticky when it costs €29.

Quick FAQ for builders

Do I need React for a Chrome extension?

No. For a small popup, plain HTML is fine. Use React when the UI complexity earns it.

Should I use the official browser APIs or a framework?

Use a framework like WXT unless you have a reason not to. Packaging and MV3 details are not where your product moat lives.

Is scraping a social network's DOM a good idea?

It is fragile and you must stay on the right side of ToS and user expectations. Track the user's own actions in their session. Do not build a stealth scraper and call it a growth tool.

What blocks store approval most often?

Missing privacy policy, unclear permissions, remote code, broken auth, and listings that do not match the binary.

How long did this take?

Longer than the happy-path tutorial. Shorter than "rewrite everything because MV3." Most of the calendar time was OAuth, store packaging, and edge cases on X's UI, not the XP math.

Closing

If you have been waiting to build your first Chrome extension, start.

Pick a problem that only makes sense inside the browser. Keep the first version insultingly small. Assume OAuth and packaging will take longer than your ego wants. Ship anyway.

I just did. The extension is free. The lessons above are free too.

Add XPilot to Chrome and let the game begin.


Written by Alex Cloudstar (@alexcloudstar). Building XPilot — free on Chrome.

Top comments (0)