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;
render?: 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;
render?: 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
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.

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
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.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.

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.

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.

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

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 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.

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 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.

ExtensionOutdatedError
)
var error: extension.ExtensionOutdatedError
error
.
ExtensionOutdatedError.required: number
required
// the ABI this page needs
// otherwise no extension here, and the root fetch and attachFrame take the cloud
}
(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().
attachFramethe 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.
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 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:

app.ts
import {
const fetch: (input: RequestInfo | URL, init?: RequestInit & {
reason?: string;
render?: 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;
render?: 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. 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 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:

app.ts
import {
const fetch: (input: RequestInfo | URL, init?: RequestInit & {
reason?: string;
render?: boolean;
}) => Promise<Response>
fetch
,
(alias) namespace extension
import extension
extension
} from '@fkn/lib'
try {
await
function fetch(input: RequestInfo | URL, init?: RequestInit & {
reason?: string;
render?: 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
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 fetchthe root fetch picks between them
Fetch with the user’s sessioncredentials: 'include', the cloud path carries no cookie of the user’s
Origin, Referer and Cookie headers you supplywith a string or URL input on the cloud path, see headers the page cannot set
Local network targetsthe proxy’s egress validation refuses a non-public address, where the extension asks for network.fetchLocal
Reading a cookiecookies.get
Request header rulessetRequestHeaderRule
TCP, UDP, HTTP and DNSnet, dgram and http over the relay, dns.lookup from the broker
Attaching a framethe cloud’s render proxy refuses lockdown, see frames
Locators and actionsthe selectors and the actions are registered on both backends, see locators and actions
Storagecloud.fs and the account copy behind fs, both with an account connected, where opfs never leaves the device
Quotacloud.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.