On the web
A portable Egglet is the same bundle whether it runs inside the Egg desktop app or as an ordinary web page in any Chromium browser. Your code does not change. What changes is how the page finds the Gateway, what it does when Egg is not installed, and how it gets authorized. This page covers all three, plus how to get an app listed in the public gallery.
You write the Egglet once, with defineEgglet, and build it as a standalone bundle. Hosted on any static host, the bundle’s bootstrap discovers the local Gateway, runs the authorization handshake, reconciles its schema, and activates, exactly as the in-app host would. To run on the web an Egglet must be runtime: portable (the default) and delivery: iframe; host-bound Egglets depend on desktop-only surfaces and cannot.
Running in any browser
Code on the same machine as the Gateway reads its port and bearer token from daemon_info.json. A web page cannot read a file off the user’s disk, so it discovers the Gateway over the network instead.
The Gateway listens on a small fixed port window starting at 4747. The SDK probes each port in that window with a GET /health request and matches on the Gateway’s identity in the response:
GET http://127.0.0.1:4747/health
{ "ok": true, "service": "egg-gateway", ... }
The first port that answers with service: "egg-gateway" is the Gateway. A fixed, identity-checked window is what makes discovery possible without the page reading anything off disk. You do not call this yourself; the runtime does it during bootstrap, and the discovered base URL is used for every later call.
Private Network Access
Your page is served from a public origin over HTTPS. The Gateway lives on 127.0.0.1, a private address. A request from a public page to a private address is a Private Network Access request, and Chromium gates it: it sends a preflight first, and the browser may show the user a one-time permission prompt for the local connection.
The Gateway answers the preflight with Access-Control-Allow-Private-Network: true, so once the user allows it the calls go through. There is nothing to configure on your side. Account for the prompt in your first-run experience, since a user may have to approve it before the app can reach the Gateway.
When Egg isn’t installed
If no Gateway answers on the port window, the SDK throws EggNotAvailable. The SDK renders no UI of its own; it hands you a typed error and lets your app decide what to show. The error carries everything you need to decide:
| Field | Meaning |
|---|---|
| reason | "unreachable" when Egg could run here but did not answer, or "unsupported-platform" when Egg does not ship for this OS. A web page cannot tell “never installed” from “installed but not running”; both look like a refused connection, so one reason covers both. |
| supported | true on macOS and Windows, where there is something to install; false elsewhere. |
| installUrl | The download page (https://eggbrowser.com) when supported is true, otherwise null. |
Egg ships for macOS and Windows today. On Linux, Android, or iOS, isSupportedPlatform() returns false and the error’s reason is "unsupported-platform" with no download to offer. Your message there should explain that Egg is not available yet rather than push an install.
Catch the error and branch on supported:
import { EggNotAvailable } from "@egg/sdk";
try {
await bootstrap(module);
} catch (err) {
if (err instanceof EggNotAvailable) {
if (err.supported) showGetEggLink(err.installUrl);
else showUnsupportedPlatformNotice();
} else {
throw err;
}
}
If you don’t want to build that UI, the SDK ships a default install card. runWithInstallPrompt bootstraps the Egglet and, only on EggNotAvailable, renders the card (a download offer on supported platforms, an explanation elsewhere). Any other error is rethrown untouched. Use showInstallPrompt(err, opts) to trigger the same card from your own catch block.
import { runWithInstallPrompt } from "@egg/sdk";
import module from "./index";
await runWithInstallPrompt(module, { appName: "Notes" });
Authorization
A web Egglet never receives the Gateway’s master token. On first use the SDK runs the authorization handshake: it opens the Gateway’s /authorize page, the user sees what the app is asking for and approves it, and the Gateway mints a scoped token limited to that Egglet’s own tables and the capabilities its manifest declares. Every call after that carries the scoped token, so the page can reach exactly what it was granted and nothing else. Revoking the app in Egg invalidates the token.
This is the same trust boundary as an in-app Egglet. The scoped credential is minted by the Gateway, not assembled on the client, and it is bound to the manifest’s uses grant. An app that asks for nothing it didn’t declare gets a token that can do nothing it didn’t declare.
Some apps work with the page you are viewing (saving a profile into People, for example) and need the Egg Extension, a companion browser add-on. The Gateway bundles it and serves it at GET /extension/download, so users add it without a separate store install. An app that needs it should detect its absence and point the user at Egg’s settings, where the extension is installed.
Publishing & the gallery
An app that runs on the web is already usable by anyone who has Egg: they open its URL. Publishing is about discovery. The gallery at eggbrowser.com/apps is where people find apps.
It has two sections. Made by Egg are the apps we build and host. From the community are apps built by other developers, each running from the developer’s own site. The two are kept visibly distinct so a user always knows who is behind an app. A listing is discovery, not endorsement: listed or not, an app still runs from its own origin and still has to pass the on-device authorization handshake before the Gateway gives it anything.
What the card discloses
Each card shows what the app wants, derived from the uses block in its manifest. Declaring capabilities honestly is part of being listable, because that declaration is what the card discloses and what the authorization grant is bound to.
| Manifest | Shown as |
|---|---|
| uses.llm | Use AI |
| uses.extension | Work with your open browser tabs |
| uses.gateway.writes | Save data on your device |
| uses.network.fetch | Make network requests |
| credentials | Use stored account logins |
Before you submit
To be listable an app needs to actually run on a stranger’s machine: portable and iframe-delivered, hosted over HTTPS with a reachable manifest, handling EggNotAvailable with something useful, and declaring what it uses so the disclosure and grant are accurate.
Submitting
Submit your app by its manifest URL. The catalog fetches the manifest, checks that it is a valid Egglet manifest, derives the capability summary from its uses block, and queues the app for review. Approved apps appear in the From the community section on the next page load.
curl -X POST "https://egg-apps-catalog-68925516043.us-central1.run.app/submit" \
-H "Content-Type: application/json" \
-d '{
"manifestUrl": "https://your-app.example/manifest.json",
"publisher": "Your name or company",
"homepage": "https://your-app.example"
}'
Only manifestUrl is required, and it must be an https URL. publisher is the name shown on the card; if you omit it the app’s origin is used. homepage is where the card’s open link points; it defaults to the manifest’s origin. Review is manual, and we decide whether to list an app; we never edit your app or its manifest. If you move where the app is hosted, submit the new manifest URL.
It is worth repeating, because it is the whole model: listing an app gives it nothing. The user is still the one who approves what any app can reach, per app, on their own device, through the authorization handshake. The gallery is a directory; the Gateway is the gate.