Quickstart
You want to go from nothing to a build you can hand to someone else. This walk does that: create, run, connect, build, share, publish.
1. Create an extension
This walk starts from a template. One other lane reaches the same place: import a repository you already have.
npx extension@latest create my-extension --template=init
cd my-extension
npm installThe template lane runs in the browser and works signed out until the repository gets created. extension.dev/new lists the catalog and links each entry out to templates.extension.dev, a separate app where the template runs live in the page. The Import to extension.dev button there sends you back to extension.dev/import?template=<slug>, which is where step 3 continues. The catalog covers popups, sidebars, content scripts, and framework combinations. See Templates.
2. Run it locally
npm run devThe dev server launches a browser with the extension loaded and reloads it on every file change. It binds to 127.0.0.1:8080, and --port moves it.
On a machine with no browser, a container or a CI runner, add --no-browser. The dev server still starts, builds, and watches. It opens nothing.
npm run dev -- --no-browser --port 9000This step is the Extension.js framework, documented at extension.js.org. Nothing has touched the platform yet.
3. Connect the project
Step 1 already left a repository. The scaffold commits the template for you, on main, so the tree is clean and there is nothing of your own to commit yet. What is missing is the remote, because the platform builds from a repository GitHub can serve.
gh repo create my-extension --private --source=. --pushAny way of creating the remote works, and gh is the shortest. There is no git add and no git commit step here, and running either one now answers "nothing to commit, working tree clean". Commit again once you change a file. A repository that reaches the import with no commits at all is the one case with nothing to build, and the import says so instead of starting a build. See Import.
Now open extension.dev/new, sign in with GitHub, and choose the repository from the repo lane. The import analyzes the tree, finds the manifest, detects the browsers the extension declares, and infers a build command and output directory per browser.
/import needs a GitHub App installation before it can write anything, and it asks for one in the middle of the flow rather than up front. The App selector lists the installations you already have and offers Authorize new installation, which sends you to github.com/apps/extensiondev/installations/new, asks which account or organization and which repositories the App may touch, and returns you to the same import page with the installation selected. The action that then does the work is Create repository and deploy in the template lane, Create project in the repository lane.
Sign-in is gated during the alpha. An account that is not on the invite list lands on /subscribe with an explanation instead of reaching the console.
See Import for what the analysis reads, Projects for what a project owns, and Workspaces for who can see it.
4. Build for every browser
Push a commit. The project builds one artifact per configured browser from that commit, in the project's managed build repository, and records the build under Builds in the console. Build minutes burn on the platform's org, never on yours.
A build that starts from a push runs without you, so it is metered on its own. Free carries 25 of them a month. Past that the push is refused and you click Build in the console to run the same commit by hand, free. See extension.dev/pricing.
Each build carries the commit it came from, the browsers it produced, and the integrity hash of every artifact, so a build can be verified later without trusting the console that served it.
5. Share the build
A build does not need to be released to be looked at. Publish a share link from the build in the console, or from an agent with the MCP server configured, call extension_preview_web with share: true. Either way the link renders the exact bytes of that build in a browser emulator. A link lives 30 days on Free and 365 days on Pro, and revoking one is permanent.
The platform hands out two share links and they run on two clocks. This one is the uploaded preview: a ?preview= address that lives for your plan's window and can be revoked. The other belongs to extension publish, which uploads nothing and answers with your project's own build page, carrying a ?share= token of 1 to 168 hours when the project is private. Different verb, different link, different lifetime.
See Share an unpublished build for review and Publish from the CLI.
6. Promote and publish
A new project has one release channel, preview, and every build lands there. stable always accepts a promotion, and any other channel is one you create. Promotion checks the build's environment stamp against the target channel's binding, see Environments and Promote gates.
When a build sits in stable, submit it to the stores from the project's Stores tab in the console, from an agent with extension_submit, or with POST /api/cli/stores/submit and a project token. Store credentials are held by the project, entered once in project settings. See Channels and Publishing.
When it fails
npm run deverrors before the platform is involved. That is the framework. Debug it with the docs at extension.js.org.- The project exists but shows no builds. Builds start on push. Push a commit, then watch Builds in the console.
- Pushes built earlier this month and stopped. Free carries 25 builds from a push a month, and you have used them. The refusal says so and names the way out: click Build in the console to run the same commit now, free. The count resets at the start of the next month.
- A share link stopped opening because it expired. An uploaded preview link lasts 30 days on Free and 365 days on Pro. Share the same build again and the expired address is reused with a fresh window, so the link you already sent starts working rather than being replaced. A
?share=token fromextension publishis the other link on the other clock, at most 168 hours, and you get a new one by running the command again. - A share link stopped opening because it was revoked. A revoked address is dead for good. The platform keeps the revocation on record so the id is never resolved again, and re-sharing the same build mints a different address. Send the new link; the old one never comes back.
- Promotion to
stableis refused. The build's environment stamp does not match the channel's binding. Build for the target channel, then promote. See Promote gates.
Next
- Access tokens to run these steps from CI
- MCP server to run them from a coding agent