A backend is where a call into @fkn/lib runs: the FKN cloud, the user’s own browser through the extension, or, once it ships, their own machine through the desktop app. This page covers the three backends one by one, what available() answers on each, how the root exports choose between them, the hosts none of them reach, and where each capability runs.
The root exports pick a backend for you. Each namespace lets you pin one:
Typed placeholder for the planned desktop backend. Always false in this release.
available() // false, the desktop backend is planned
cloud.fetch never looks at the page, and extension.fetch never falls back to the cloud. Without the extension, the extension.fetch call opens the install card and rejects once the card is dismissed. See extension.
The cloud backend is FKN’s own infrastructure. The library reaches it through the broker frame, a hidden fkn.app iframe that @fkn/lib mounts on your page at import, or adopts when the page already has one. See the broker frame.
It needs no install. Fetch, sockets, DNS and frames need no account either. cloud.fs is the exception: without a connected account its reads and writes reject with storage: not connected, see storage.
cloud.fetch goes through the proxy, the FKN server that makes the request on your behalf. net, dgram and http go through the relay, the server that holds the real socket at the far end. cloud.attachFrame goes through the render proxy, the cloud’s frame backend. dns.lookup, cloud.fs and cloud.quota leave the page through the broker, the connection your page holds into FKN, and the broker carries them the rest of the way:
The request leaves from the proxy, so the page’s cross-origin rules do not apply to it and none of the user’s cookies travel with it. See cloud.fetch().
When the proxy refuses a request itself, the result is still a Response, with a JSON body naming the error. See errors you might see.
The cloud backend also works in a worker the page has relayed. See workers.
The extension backend is the FKN extension in the user’s browser. A request runs in the extension’s service worker, and a frame is an iframe in the user’s own tab. The user’s own logged-in sessions are within reach once they consent.
The calls that reach it (extension.fetch, cookies.get, the header rules, extension.attachFrame and permissions) need the extension and a window realm. A realm is one JavaScript execution context, such as a window or a worker. The extension announces itself by marking the page’s <html> element, and a worker has no page to mark.
What the extension adds over the cloud is the user. Here is a fetch that carries their own example.org session:
A string indicating whether credentials will be sent with the request always, never, or only when sent to a same-origin URL. Sets request's credentials.
credentials: 'include',
reason?: string |undefined
reason: 'Load your watch history',
})
constresponse:Response
response.
Response.status: number
The status read-only property of the Response interface contains the HTTP status codes of the response.
Each thing the extension alone provides, listed in where each capability runs, spends something of the user’s, so the extension asks first through the consent sheet, the dialog it shows the user before such an action.
The credentialed fetch, a local network target and a header rule show the reason you pass. cookies.get takes no reason and names the origin alone. A plain extension.fetch or an attach is granted silently, with a row in the activity log, the on-device record of what an app did. See permissions and consent.
The desktop backend is a typed placeholder for the desktop app, which is planned and not yet shipped. desktop.available() is false, and desktop.fs.available() resolves false. Every other member throws synchronously the moment you call it, naming what you asked for:
message// The FKN desktop app is not connected, desktop.fetch is unavailable
}
It exists so an app can be written against all three backends today. Its exact shape is in @fkn/lib/desktop. The throw is synchronous: await desktop.fetch(url) inside a try catches it, but desktop.fetch(url).catch(...) does not, because there is never a promise to attach to.
nothing: it is a constant while the backend is planned
nowhere
cloud.available() is a realm check, never a connection or health check. It is true in a worker nobody relayed and on a page whose broker frame never connected.
Whether a cloud call issued there waits or gives up is on connecting.
extension.available() is a point in time. The content script marks <html> a tick after the document starts, so a call issued while your first module evaluates can read false on a page that reads true a moment later. When it matters which backend a root call takes, wait for the marker first:
Thrown instead of the generic "not installed" message when the extension IS there and merely too
old. Carries both numbers so an app can say which, and link its listing.
A named class rather than a message match: only the name survives a structured-clone hop, and the
codebase has been bitten before by an error whose identity was its text.
Thrown instead of the generic "not installed" message when the extension IS there and merely too
old. Carries both numbers so an app can say which, and link its listing.
A named class rather than a message match: only the name survives a structured-clone hop, and the
codebase has been bitten before by an error whose identity was its text.
available() // settled for the startup race, extension.events reports a later change
waitForExtensionExposure() resolves at once on an ok handshake. Otherwise it waits for one up to the exposure deadline, shortened to a grace period once the document is complete. The marker is read live on every call, so the wait settles the startup race and nothing more. extension.events reports a later change as a statuschange event.
credentials: 'include' on the init pins the extension and never falls back. Otherwise the marker decides at the moment of the call, extension or cloud. See fetch().
the extension when exposed, asked for with lockdown, or the realm’s only frame backend, otherwise a short wait for it, then the render proxy. See frames.
their own thing: this device’s OPFS, and behind fs the account copy, the replicated copy of a file in the account, when one is connected. opfs never leaves the device. See storage.
fetch decides per call and waits for nothing. It reads the marker at the moment you call, so a call issued before the content script lands goes to the cloud even with the extension installed.
attachFrame does wait: 150 ms once the document is complete without the marker, and up to 10,000 ms on a page that never gets there. It raises no install card on its way to the cloud. Neither picks the desktop today. The root fetch asks desktop.available() and always gets false, and attachFrame has no desktop branch at all.
The difference shows on the Response, because the cloud rebuilds it on your side:
The pinned call has no choice to make, and neither does anything else on the root. fetch and attachFrame are the two exports that choose at the call. Every other name is bound to its backend the moment you import it.
Every fetch backend refuses FKN’s own hosts as a target: fkn.app, fkn.dev, sdbx.app, every subdomain of them, and spellings with trailing dots. sdbx.app is where the render proxy and every package tenant live. A package is an npm module FKN loads on a sandbox origin of its own, and the tenant is the realm it runs in. An app cannot reach into those through the channel it uses to reach the web.
The check runs in the library before a backend is chosen, and again in the broker and in the extension’s content script:
message// fetch: refusing to target the extension's own pages or FKN platform domains
}
Each backend refuses in its own words, before anything leaves the page. The match is anchored on a dot, so notfkn.app passes. The extension’s message also covers chrome-extension: and moz-extension: URLs, which are its own pages.
An input that does not parse as an absolute URL is let through on purpose, since a relative URL can only resolve against your own origin. extension.fetch resolves it through new Request first, so its check runs on the resolved URL. See extension.fetch().
Anything the extension does, it does in a window realm only. The cloud’s fetch, sockets, DNS, storage and quota also work in a worker the page relayed. Attaching a frame needs a window on either backend. See what works in a relayed worker.