Skip to content

Backends

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:

app.ts
import {
const fetch: (input: RequestInfo | URL, init?: RequestInit & {
reason?: string;
spread?: boolean;
}) => Promise<Response>
fetch
,
(alias) namespace cloud
import cloud
cloud
,
(alias) namespace extension
import extension
extension
,
(alias) namespace desktop
import desktop
desktop
} from '@fkn/lib'
await
function fetch(input: RequestInfo | URL, init?: RequestInit & {
reason?: string;
spread?: boolean;
}): Promise<Response>
fetch
('https://example.org/api/catalog.json') // the extension when exposed on this page, otherwise the cloud
await
(alias) namespace cloud
import cloud
cloud
.
cloud_d_exports.fetch(input: ProxyFetchInput, init?: ProxyFetchInit): Promise<Response>
export cloud_d_exports.fetch
fetch
('https://example.org/api/catalog.json') // always the cloud, through the proxy
await
(alias) namespace extension
import extension
extension
.
extension_d_exports.fetch(input: RequestInfo | URL, init?: extension.FetchInit): Promise<Response>
export extension_d_exports.fetch

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
(alias) namespace desktop
import desktop
desktop
.
desktop_d_exports.available(): boolean
export desktop_d_exports.available

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:

app.ts
import {
(alias) namespace cloud
import cloud
cloud
} from '@fkn/lib'
const
const response: Response
response
= await
(alias) namespace cloud
import cloud
cloud
.
cloud_d_exports.fetch(input: ProxyFetchInput, init?: ProxyFetchInit): Promise<Response>
export cloud_d_exports.fetch
fetch
('https://example.org/api/catalog.json')
const response: Response
response
.
Response.status: number

The status read-only property of the Response interface contains the HTTP status codes of the response.

MDN Reference

status
// the upstream's, or the proxy's own when it never reached example.org
const
const catalog: any
catalog
= await
const response: Response
response
.
Body.json(): Promise<any>
json
() // the app's own data

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.

In a worker nobody relayed, cloud.fetch, cloud.fs, cloud.quota and dns.lookup wait forever. Only net, dgram and the http requests built on them give up, on the socket’s error event. See connecting.

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:

app.ts
import {
(alias) namespace extension
import extension
extension
} from '@fkn/lib'
(alias) namespace extension
import extension
extension
.
extension_d_exports.available(): boolean
export extension_d_exports.available
available
() // true once the content script has marked this page
try {
const
const response: Response
response
= await
(alias) namespace extension
import extension
extension
.
extension_d_exports.fetch(input: RequestInfo | URL, init?: extension.FetchInit): Promise<Response>
export extension_d_exports.fetch

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/me', {
RequestInit.credentials?: RequestCredentials | undefined

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',
})
const response: Response
response
.
Response.status: number

The status read-only property of the Response interface contains the HTTP status codes of the response.

MDN Reference

status
// the upstream status, the request carried their example.org cookies
} catch (
var error: unknown
error
) {
if (!(
var error: unknown
error
instanceof
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.

When the extension is missing, an extension.* call waits up to 1,000 ms for it. By default it then opens the install card the broker draws, and rejects with The FKN WebExtension is not installed, enabled or not exposed on this page. once the card is dismissed. It never falls back to the cloud.

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.

setMissingExtensionHandler(null) removes the card for the whole app, and promptInstall(reason) opens it when you choose. See when the extension is missing.

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:

app.ts
import {
(alias) namespace desktop
import desktop
desktop
} from '@fkn/lib'
(alias) namespace desktop
import desktop
desktop
.
desktop_d_exports.available(): boolean
export desktop_d_exports.available

Typed placeholder for the planned desktop backend. Always false in this release.

available
() // false
await
(alias) namespace desktop
import desktop
desktop
.
const desktop_d_exports.fs: {
available: () => Promise<boolean>;
readFile: (..._args: unknown[]) => never;
readFileSync: (..._args: unknown[]) => never;
writeFile: (..._args: unknown[]) => never;
writeFileSync: (..._args: unknown[]) => never;
appendFile: (..._args: unknown[]) => never;
appendFileSync: (..._args: unknown[]) => never;
existsSync: (..._args: unknown[]) => never;
stat: (..._args: unknown[]) => never;
statSync: (..._args: unknown[]) => never;
readdir: (..._args: unknown[]) => never;
readdirSync: (..._args: unknown[]) => never;
mkdir: (..._args: unknown[]) => never;
... 9 more ...;
promises: {
readFile: (..._args: unknown[]) => never;
writeFile: (..._args: unknown[]) => never;
unlink: (..._args: unknown[]) => never;
rename: (..._args: unknown[]) => never;
readdir: (..._args: unknown[]) => never;
mkdir: (..._args: unknown[]) => never;
rm: (..._args: unknown[]) => never;
stat: (..._args: unknown[]) => never;
};
}
export desktop_d_exports.fs

Planned desktop storage backend. available() resolves false; storage operations throw.

fs
.
available: () => Promise<boolean>
available
() // false, the other member that does not throw
try {
await
(alias) namespace desktop
import desktop
desktop
.
desktop_d_exports.fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>
export desktop_d_exports.fetch

Planned desktop fetch backend. Throws in this release.

fetch
('https://example.org/api/catalog.json')
} catch (
var error: unknown
error
) {
if (
var error: unknown
error
instanceof
var Error: ErrorConstructor
Error
)
var error: Error
error
.
Error.message: string
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.

Each backend namespace has an available(), and the three answer different questions:

CallWhat it checksWhere it is true
cloud.available()the realm’s shape: a window or a self existsevery window and worker, connected to the broker or not
extension.available()a document exists and carries the extension’s marker right nowa window where the content script has run
desktop.available()nothing: it is a constant while the backend is plannednowhere

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:

app.ts
import {
(alias) namespace extension
import extension
extension
,
class ExtensionOutdatedError

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.

ExtensionOutdatedError
,
const setMissingExtensionHandler: (handler: MissingExtensionHandler | null) => void
setMissingExtensionHandler
,
const waitForExtensionExposure: (timeout?: number) => Promise<void>
waitForExtensionExposure
} from '@fkn/lib'
function setMissingExtensionHandler(handler: MissingExtensionHandler | null): void
setMissingExtensionHandler
(null) // app-wide, a missing extension rejects instead of opening the card
try {
await
function waitForExtensionExposure(timeout?: number): Promise<void>
waitForExtensionExposure
() // resolves on an ok handshake
} catch (
var error: unknown
error
) {
if (
var error: unknown
error
instanceof
class ExtensionOutdatedError

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.

ExtensionOutdatedError
)
var error: extension.ExtensionOutdatedError
error
.
ExtensionOutdatedError.required: number
required
// the ABI this page needs
// otherwise no extension here: the root fetch takes the cloud, and attachFrame({ cookies: 'native' }) rejects
}
(alias) namespace extension
import extension
extension
.
extension_d_exports.available(): boolean
export extension_d_exports.available
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.

The root exports are the names you get without picking a namespace. They do not share one fallback chain:

Root exportBackend
fetchcredentials: '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().
attachFrameits 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.
net, dgram, http, dnsthe cloud, always, as cloud.net, cloud.dgram, cloud.http and cloud.dns under shorter names. See TCP and UDP sockets.
fs, opfstheir 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:

app.ts
import {
const fetch: (input: RequestInfo | URL, init?: RequestInit & {
reason?: string;
spread?: boolean;
}) => Promise<Response>
fetch
,
(alias) namespace cloud
import cloud
cloud
} from '@fkn/lib'
// decided now, by the marker on <html>
const
const auto: Response
auto
= await
function fetch(input: RequestInfo | URL, init?: RequestInit & {
reason?: string;
spread?: boolean;
}): Promise<Response>
fetch
('https://example.org/api/catalog.json')
const auto: Response
auto
.
Response.url: string

The url read-only property of the Response interface contains the URL of the response.

MDN Reference

url
// '' when the cloud answered, the final URL when the extension did
// no decision, always the proxy
const
const pinned: Response
pinned
= await
(alias) namespace cloud
import cloud
cloud
.
cloud_d_exports.fetch(input: ProxyFetchInput, init?: ProxyFetchInit): Promise<Response>
export cloud_d_exports.fetch
fetch
('https://example.org/api/catalog.json')
const pinned: Response
pinned
.
Response.url: string

The url read-only property of the Response interface contains the URL of the response.

MDN Reference

url
// '', always

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.

A page that must know where its requests leave from pins cloud.fetch or extension.fetch, or waits for exposure first. See what available() means.

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:

app.ts
import {
const fetch: (input: RequestInfo | URL, init?: RequestInit & {
reason?: string;
spread?: boolean;
}) => Promise<Response>
fetch
,
(alias) namespace extension
import extension
extension
} from '@fkn/lib'
try {
await
function fetch(input: RequestInfo | URL, init?: RequestInit & {
reason?: string;
spread?: boolean;
}): Promise<Response>
fetch
('https://fkn.app/')
} catch (
var error: unknown
error
) {
if (
var error: unknown
error
instanceof
var Error: ErrorConstructor
Error
)
var error: Error
error
.
Error.message: string
message
// fetch refuses FKN platform domains (fkn.app)
}
try {
await
(alias) namespace extension
import extension
extension
.
extension_d_exports.fetch(input: RequestInfo | URL, init?: extension.FetchInit): Promise<Response>
export extension_d_exports.fetch

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://proxy.sdbx.app/')
} catch (
var error: unknown
error
) {
if (
var error: unknown
error
instanceof
var Error: ErrorConstructor
Error
)
var error: Error
error
.
Error.message: string
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().

The table lists what each backend can do today. It has no desktop column, because the desktop backend is planned and nothing runs there yet:

CapabilityCloudExtensionNotes
Cross-origin fetch✅✅the root fetch picks between them
Fetch with the user’s session❌✅credentials: 'include', the cloud path carries no cookie of the user’s
Origin, Referer and Cookie headers you supply✅✅with a string or URL input on the cloud path, see headers the page cannot set
Local network targets❌✅the proxy’s egress validation refuses a non-public address, where the extension asks for network.fetchLocal
Reading a cookie❌✅cookies.get
Request header rules❌✅setRequestHeaderRule
TCP, UDP, HTTP and DNS✅❌net, dgram and http over the relay, dns.lookup from the broker
Attaching a frame✅✅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
Quota✅❌cloud.quota

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.

Four refusals name the backend rather than the request:

MessageWhat happened
fetch refuses FKN platform domains (<hostname>)The root fetch or cloud.fetch was aimed at a platform host.
fetch: refusing to target the extension's own pages or FKN platform domainsextension.fetch, or the root fetch on the extension path, was aimed at the same hosts or at one of the extension’s own pages.
fetch with credentials needs the FKN extension, which only exists in window realmsThe root fetch got credentials: 'include' in a worker, where no extension can be.
The FKN desktop app is not connected, desktop.<name> is unavailableA desktop.* member other than the two available() calls was called, on a backend that is still planned.

Every other message has its row on every error.