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:
Fetches through the extension. A credentialed fetch (credentials: 'include') and one to the local
network are asked for on the target's host first. A url on the extension's own pages or an FKN
platform host is refused with an Error before anything is asked. A redirect onto an FKN platform
host is not followed: the hop is blocked before it is sent, on both engines and in every spelling,
and should one ever slip the block the final answer is still withheld with an Error. A fetch that
follows redirects is refused before it is sent when the extension could not set the block up.
fetch('https://example.org/api/catalog.json') // always the user's browser, so it needs the extension
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.
An attached frame asks the user here too, through the broker’s card rather than a sheet, and for the same four categories of access. A card answer lasts the tab and is kept in the broker’s own sessionStorage, so there is no Always, no stored denial and no activity log on this backend. cloud.fetch, the sockets and dns.lookup ask for nothing: they carry none of the user’s sessions, and the frame is where a session appears. See consent on the cloud backend.
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:
Fetches through the extension. A credentialed fetch (credentials: 'include') and one to the local
network are asked for on the target's host first. A url on the extension's own pages or an FKN
platform host is refused with an Error before anything is asked. A redirect onto an FKN platform
host is not followed: the hop is blocked before it is sent, on both engines and in every spelling,
and should one ever slip the block the final answer is still withheld with an Error. A fetch that
follows redirects is refused before it is sent when the extension could not set the block up.
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.
status// the upstream status, the request carried their example.org cookies
} catch (
var error:unknown
error) {
if (!(
var error:unknown
errorinstanceof
var Error:ErrorConstructor
Error) ||
var error:Error
error.
Error.name: string
name!=='PermissionDeniedError') throw
var error:unknown
error
var error:Error
error.
Error.message: string
message// Permission denied: network on example.org (network.fetchCredentialed https://example.org)
}
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. What it asks for is a category of access on one website, and the answer is stored under that pair, so a grant for example.org says nothing about example.net.
The extension is where a duration lives. A sheet answer is kept Once, for the browser session or until the user revokes it, a denial is remembered the same way, and every answer and every silent grant lands in the activity log, the on-device record of what an app did. The credentialed fetch, a local network target and a header rule show the reason you pass, cookies.get names the cookie’s host alone, and a plain extension.fetch or an attach is granted silently. See permissions and consent.
The card needs the broker, so on a page whose broker frame never connects the call stays pending instead. See connecting.
Chrome stops the extension’s service worker whenever it has been idle for a while. From the next FKN extension store release, the first after 0.1.54, a call the stop cut off rejects with BackgroundStoppedError and the next one is answered; through 0.1.54 a fetch, a cookie read, a header rule or an attach after such a stop stays pending until the page reloads. See a call the extension never finished.
An extension frame runs on the person’s own browser cookies, cookies: 'native', the one value extension.attachFrame() serves; any other is refused with ExtensionOperationUnsupportedError, operation 'cookies'. From the extension’s first store release after 0.1.54, a window on 'native' is a real browser window on the person’s own browser session, and a sign-in there reaches an iframe attached through the extension on its next goto(). Every store build through 0.1.54 does not open one: the call is refused at once with ExtensionOperationUnsupportedError, operation 'attachWindow', and since nothing has opened yet, the same click can open one on 'persistent' or 'ephemeral', which run on the cloud. A sign-in in a cloud window reaches your cloud frames and never an extension iframe, see a window on the extension.
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 installed but too old.
Carries both numbers so an app can say which and link its listing. Match it by name, which
survives a structured-clone hop.
Thrown instead of the generic "not installed" message when the extension is installed but too old.
Carries both numbers so an app can say which and link its listing. Match it by name, which
survives a structured-clone hop.
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().
its cookies option decides: the render proxy for 'persistent', the default, and 'ephemeral', whatever is installed and with no wait; the extension for 'native', after a short wait for it to expose itself, or with no wait for a window, which an extension that does not serve windows refuses by name. See which jar, which backend.
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 decides from its options rather than from the marker. On 'native' it waits, 150 ms once the document is complete without the marker and up to 10,000 ms on a page that never gets there, then shows the install card when no extension answered. The other two values never wait and never show the card, and a window never waits, since it has to open on the click that asked for it.
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, fetch by the marker and attachFrame by its cookies. 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 other name FKN owns, 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.
From @fkn/lib 0.9.47 your page checks the full list, where 0.9.46 checked only fkn.app, fkn.dev and sdbx.app. The full list is every name FKN owns, each with every subdomain: those three, FKN’s Access team domain fkn.cloudflareaccess.com, the company site horionsoftware.com, the storage endpoint of FKN’s Cloudflare account, the pages.dev name of every Cloudflare Pages project serving FKN’s code (such as web-b3z.pages.dev, which serves fkn.app, and its per-deploy previews), and the public addresses of FKN’s nodes in every spelling a URL folds onto them, such as hex, octal or IPv4-mapped IPv6. The broker on fkn.app and the render proxy refuse the full list today, and the extension refuses it from its first store release after 0.1.54, on every entry: a fetch, a frame src or goto() target, a cookie read, a header rule’s domains, and the host a locator call reaches.
Only a target is checked, never where your app is served from. An app hosted on an FKN domain, such as one on anime.fkn.app or a sdbx.app tenant, keeps every capability it had, aimed at any other host.
The check runs in the library before a backend is chosen, and again in the broker and in the extension’s content script:
Fetches through the extension. A credentialed fetch (credentials: 'include') and one to the local
network are asked for on the target's host first. A url on the extension's own pages or an FKN
platform host is refused with an Error before anything is asked. A redirect onto an FKN platform
host is not followed: the hop is blocked before it is sent, on both engines and in every spelling,
and should one ever slip the block the final answer is still withheld with an Error. A fetch that
follows redirects is refused before it is sent when the extension could not set the block up.
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().
cookies picks the backend: 'persistent' and 'ephemeral' the cloud, 'native' the extension, which alone serves lockdown, see frames
Attaching a frame in a window
✅
✅
on 'native' a real browser window, from the extension’s first store release after 0.1.54; every earlier build refuses it by name, see a window on the extension
A blank page, a storageState seed, addScriptTag, clearCookies
✅
❌
the extension refuses each by name, a seed through the 'ephemeral' it needs, see frames and seeding a fresh jar
Locators and actions
✅
✅
the selectors and the actions are registered on both backends, see locators and actions
Storage
✅
❌
cloud.fs and the account copy behind fs, both with an account connected, where opfs never leaves the device
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.