<!-- mobian-agent-page publisher="dailydev" canonical="https://daily.dev/posts/i-shipped-my-first-chrome-extension-kqiqfemcp" -->

---
title: I Shipped My First Chrome Extension | daily.dev
description: A developer shares lessons from shipping their first Chrome extension (XPilot) — a gamification layer for X/Twitter activity. The post covers the Manifest V3...
canonical: https://daily.dev/posts/i-shipped-my-first-chrome-extension-kqiqfemcp
twitter:card: summary_large_image
twitter:site: @dailydotdev
og:type: website
og:site_name: daily.dev
og:title: I Shipped My First Chrome Extension | daily.dev
og:description: A developer shares lessons from shipping their first Chrome extension (XPilot) — a gamification layer for X/Twitter activity. The post covers the Manifest V3...
og:url: https://daily.dev/posts/i-shipped-my-first-chrome-extension-kqiqfemcp
og:image: https://api.daily.dev/og/posts/KQIqfEmcp.png
og:image:alt: I Shipped My First Chrome Extension
og:image:width: 1200
og:image:height: 630
og:locale: en
---

> ## Documentation Index
> Fetch the complete documentation index at: https://daily.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# I Shipped My First Chrome Extension

**[Alex CloudStar](https://daily.dev/sources/alexcloudstar)** · [@alexcloudstar](https://daily.dev/alexcloudstar) · 10 min read · 1 upvotes · 1 comments

## Summary

A developer shares lessons from shipping their first Chrome extension (XPilot) — a gamification layer for X/Twitter activity. The post covers the Manifest V3 service worker model, OAuth 2.0 with PKCE inside a Chrome extension using chrome.identity.launchWebAuthFlow, pitfalls around canonical domain mismatches and CORS, detecting activity on X's dynamic DOM, local-first data architecture to keep costs low, and Chrome Web Store submission requirements. Key takeaways: decide www vs apex before OAuth, test the store ZIP early with private testers, keep v0.1 brutally small, and budget time for store admin beyond engineering.

## Full article

daily.dev links to this article rather than hosting it. Read it at the original source: <https://www.alexcloudstar.com/blog/i-just-shipped-my-first-chrome-extension>

## Questions this post answers

### How do I fix chrome.identity.launchWebAuthFlow OAuth failing silently after the preflight OPTIONS request succeeds?

The issue is usually a host mismatch between the extension's API base and the manifest's allowed host permissions, causing Chrome to fall back to CORS and drop the actual POST after a successful OPTIONS preflight. Default the production API base to the canonical host (e.g. www), allow both apex and www in host permissions, add CORS scoped to the published extension ID, and pin allowed extension IDs on the server.

_Developers debugging extension OAuth flows can compare notes on daily.dev before shipping the fix._

### Why does chrome.identity OAuth callback fail with a vague X (Twitter) authorization error even though the flow looks correct?

This typically happens when the registered OAuth redirect_uri host does not exactly match the host the app actually builds at runtime, such as a mismatch between xpilot.so and www.xpilot.so. X shows a generic access-denied page with no stack trace. The fix is picking one canonical origin, setting it as APP_URL, and registering that exact callback URL on developer.x.com.

_Anyone wiring up extension-based social OAuth can cross-check gotchas like this on daily.dev before launch._

### Should I return an OAuth exchange code as a URL hash fragment in a Chrome extension auth flow?

No, putting the short-lived exchange code in a hash fragment (#code=...) is unreliable because redirects can strip fragments, leaving chrome.identity.launchWebAuthFlow with an empty URL. Passing the code in the query string instead (?code=...), with hash as a fallback only, avoids this failure mode entirely.

_Builders wiring OAuth into browser extensions can find this kind of implementation detail via daily.dev before it costs debugging time._

## Community discussion

Top comments from developers on daily.dev.

**@agustinbarrientos** · 0 upvotes

> Did private testers catch anything else that unpacked Chrome missed?

## Similar posts on daily.dev

- [How I Built a Chrome Extension Wrapper \(and Everything That Tried to Stop Me\)](https://daily.dev/posts/how-i-built-a-chrome-extension-wrapper-and-everything-that-tried-to-stop-me--zn0uywaqq) · thoughbot · 1 upvotes · 0 comments
- [What's new in web extensions: I/O 2026 recap](https://daily.dev/posts/what-s-new-in-web-extensions-i-o-2026-recap-inibvvssg) · Chrome Developers · 4 upvotes · 1 comments
- [How to Use Transformers.js in a Chrome Extension](https://daily.dev/posts/how-to-use-transformers-js-in-a-chrome-extension-cljxqw6u8) · Hugging Face · 1 upvotes · 0 comments
- [Extension Migration Progress Update – Part 3 – Shin’s blog](https://daily.dev/posts/extension-migration-progress-update-part-3-shin-s-blog-xfkkyhi6w) · Planet Igalia · 0 upvotes · 0 comments

---

Tags: [#devtools](https://daily.dev/tags/devtools), [#typescript](https://daily.dev/tags/typescript), [#oauth](https://daily.dev/tags/oauth)

[View this post on daily.dev](https://daily.dev/posts/i-shipped-my-first-chrome-extension-kqiqfemcp)

```json
{"@context":"https://schema.org","@graph":[{"@type":"Organization","@id":"https://daily.dev/#organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180},"sameAs":["https://twitter.com/dailydotdev","https://github.com/dailydotdev","https://www.linkedin.com/company/daily-dev-ltd"]},{"@type":"WebSite","@id":"https://daily.dev/#website","url":"https://daily.dev","name":"daily.dev","publisher":{"@id":"https://daily.dev/#organization"},"potentialAction":{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https://daily.dev/search?q={search_term_string}"},"query-input":"required name=search_term_string"}}]}
{"@context":"https://schema.org","@type":"TechArticle","headline":"I Shipped My First Chrome Extension","url":"https://daily.dev/posts/i-shipped-my-first-chrome-extension-kqiqfemcp","mainEntityOfPage":{"@type":"WebPage","@id":"https://daily.dev/posts/i-shipped-my-first-chrome-extension-kqiqfemcp"},"datePublished":"2026-07-27T10:37:22.503Z","dateModified":"2026-09-13T20:00:16.732Z","description":"A developer shares lessons from shipping their first Chrome extension (XPilot) — a gamification layer for X/Twitter activity. The post covers the Manifest V3...","image":"https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/c815388bbc8c01eeb63ec65aa2e74515?_a=AQAEuop","thumbnailUrl":"https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/c815388bbc8c01eeb63ec65aa2e74515?_a=AQAEuop","isAccessibleForFree":true,"articleSection":"Alex CloudStar","inLanguage":"en","publisher":{"@type":"Organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180}},"author":{"@type":"Person","name":"Alex Cloudstar","url":"https://daily.dev/alexcloudstar","image":"https://media.daily.dev/image/upload/s--xA-1aKK7--/f_auto/v1767564650/avatars/avatar_ZUjetzkwG7V5behQOpH0X?_a=BAMAK+ZW0","description":"Hi there! 👋🏻  I'm a Senior Full-Stack Developer oriented on Javascript","interactionStatistic":{"@type":"InteractionCounter","interactionType":{"@type":"EndorseAction"},"userInteractionCount":990}},"commentCount":1,"discussionUrl":"https://daily.dev/posts/i-shipped-my-first-chrome-extension-kqiqfemcp","interactionStatistic":[{"@type":"InteractionCounter","interactionType":{"@type":"LikeAction"},"userInteractionCount":1},{"@type":"InteractionCounter","interactionType":{"@type":"CommentAction"},"userInteractionCount":1}],"keywords":"devtools,typescript,oauth","timeRequired":"PT10M"}
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://daily.dev"},{"@type":"ListItem","position":2,"name":"Alex CloudStar","item":"https://daily.dev/sources/alexcloudstar"},{"@type":"ListItem","position":3,"name":"I Shipped My First Chrome Extension"}]}
{"@context":"https://schema.org","@type":"WebPage","@id":"https://daily.dev/posts/i-shipped-my-first-chrome-extension-kqiqfemcp","comment":[{"@type":"Comment","text":"Did private testers catch anything else that unpacked Chrome missed?","datePublished":"2026-08-01T13:13:54.645Z","url":"https://daily.dev/posts/KQIqfEmcp#c-g9xBy2fEV","author":{"@type":"Person","name":"Agustin Barrientos","url":"https://daily.dev/agustinbarrientos","image":"https://media.daily.dev/image/upload/s--5ayxQnqn--/f_auto/v1788281802/avatars/avatar_wQYYVe5Tbj0NJ7C7qPoa8?_a=BAMAMicg0"}}]}
{"@context":"https://schema.org","@type":"FAQPage","@id":"https://daily.dev/posts/i-shipped-my-first-chrome-extension-kqiqfemcp#faq","mainEntity":[{"@type":"Question","name":"How do I fix chrome.identity.launchWebAuthFlow OAuth failing silently after the preflight OPTIONS request succeeds?","acceptedAnswer":{"@type":"Answer","text":"The issue is usually a host mismatch between the extension's API base and the manifest's allowed host permissions, causing Chrome to fall back to CORS and drop the actual POST after a successful OPTIONS preflight. Default the production API base to the canonical host (e.g. www), allow both apex and www in host permissions, add CORS scoped to the published extension ID, and pin allowed extension IDs on the server. Developers debugging extension OAuth flows can compare notes on daily.dev before shipping the fix."}},{"@type":"Question","name":"Why does chrome.identity OAuth callback fail with a vague X (Twitter) authorization error even though the flow looks correct?","acceptedAnswer":{"@type":"Answer","text":"This typically happens when the registered OAuth redirect_uri host does not exactly match the host the app actually builds at runtime, such as a mismatch between xpilot.so and www.xpilot.so. X shows a generic access-denied page with no stack trace. The fix is picking one canonical origin, setting it as APP_URL, and registering that exact callback URL on developer.x.com. Anyone wiring up extension-based social OAuth can cross-check gotchas like this on daily.dev before launch."}},{"@type":"Question","name":"Should I return an OAuth exchange code as a URL hash fragment in a Chrome extension auth flow?","acceptedAnswer":{"@type":"Answer","text":"No, putting the short-lived exchange code in a hash fragment (#code=...) is unreliable because redirects can strip fragments, leaving chrome.identity.launchWebAuthFlow with an empty URL. Passing the code in the query string instead (?code=...), with hash as a fallback only, avoids this failure mode entirely. Builders wiring OAuth into browser extensions can find this kind of implementation detail via daily.dev before it costs debugging time."}}]}
```

