Skip to content

Account and quota

An account is the FKN identity a person carries from site to site, with the same subscription, storage allowance and daily volume on each. This page covers how your app asks for it with connect(), what it may read and follow once it has it, and how it reads the metered volume with cloud.quota().

The account, relay and quota calls on this page go through the broker, the connection your page holds into FKN, reached through the broker frame, a hidden fkn.app iframe the library mounts. None of them uses the FKN browser extension or has a deadline: a call made before the broker is reachable waits for it, and account.onChange returns at once instead (see connection and lifecycle). The two button helpers, connectButtonUrl() and ConnectButton, build a URL and an element locally and never wait.

connect() asks the person to use their FKN account on this site, and resolves with whether they did:

app.ts
import {
(alias) namespace account
import account
account
,
const connect: () => Promise<boolean>
connect
} from '@fkn/lib'
var document: Document

window.document returns a reference to the document contained in the window.

MDN Reference

document
.
ParentNode.querySelector<Element>(selectors: string): Element | null (+4 overloads)

Returns the first element that is a descendant of node that matches selectors.

MDN Reference

querySelector
('#connect')!.
Element.addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void (+1 overload)

The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.

MDN Reference

addEventListener
('click', async () => {
const
const connected: boolean
connected
= await
function connect(): Promise<boolean>
connect
() // true, once the person pressed Connect
if (!
const connected: boolean
connected
) return
const
const info: account.AccountInfo | null
info
= await
(alias) namespace account
import account
account
.
account_d_exports.info(): Promise<account.AccountInfo | null>
export account_d_exports.info
info
()
var document: Document

window.document returns a reference to the document contained in the window.

MDN Reference

document
.
ParentNode.querySelector<Element>(selectors: string): Element | null (+4 overloads)

Returns the first element that is a descendant of node that matches selectors.

MDN Reference

querySelector
('#who')!.
Element.textContent: string | null
textContent
=
const info: account.AccountInfo | null
info
?.
name: string | undefined

the account's display name (a username, not necessarily a real name)

name
?? '' // the username, on screen
})

When the site already holds a connection, which a site under fkn.app does from load, connect() resolves true at once and shows nothing. Otherwise the broker draws a card over your page, titled Connect your account, with one Connect button.

On another site that button opens a fkn.app window where the person confirms. Under fkn.app it connects in place first, and opens the window only when that does not answer in time.

It resolves true once the account is accepted, and false for everything else: a dismissed card, a call from a worker, or any failure on the way, since the library turns a broker rejection into false. So false is not proof that the person declined.

The click that opens the window is the one inside the card, so connect() may run from a click handler or on page load. Concurrent callers share the one card, and account.login() is the same call.

A package, an npm module FKN loads on a sandbox origin of its own, cannot connect for itself. Inside one, connect() connects the host app that installed it, and answers false with no card when that host is unknown or is itself a package (see packages).

The token never reaches your app: it stays in the broker’s storage on fkn.app.

account.info() resolves with the connected account, or null when this site is not connected:

app.ts
import {
(alias) namespace account
import account
account
,
const connect: () => Promise<boolean>
connect
} from '@fkn/lib'
await
(alias) namespace account
import account
account
.
account_d_exports.info(): Promise<account.AccountInfo | null>
export account_d_exports.info
info
() // null, this site is not connected yet
await
function connect(): Promise<boolean>
connect
() // true, once the person pressed Connect
const
const info: account.AccountInfo | null
info
= await
(alias) namespace account
import account
account
.
account_d_exports.info(): Promise<account.AccountInfo | null>
export account_d_exports.info
info
()
const info: account.AccountInfo | null
info
?.
name: string | undefined

the account's display name (a username, not necessarily a real name)

name
// 'ada', the username
const info: account.AccountInfo | null
info
?.
premium: boolean | undefined

the account has an active paid subscription

premium
// false, no subscription

AccountInfo carries four fields, and @fkn/lib/account exports the type (see TypeScript):

FieldWhat it holds
nameThe account’s display name, a username.
imageThe profile image URL, or null when none is set.
premiumWhether a paid subscription is active right now.
premiumUntilWhen the subscription lapses, a millisecond epoch, or null when not subscribed.

It carries no id and no email, by design. The connect card promises that this site sees only the username and premium status, and the service, the FKN server that keeps account files and meters cloud egress, issues nothing more.

The broker caches the answer for 5 seconds and drops it on every connection change, so calling info() often is cheap. It resolves null on any failure and in a worker, never rejects, and inside a package answers for the host app.

account.onChange(callback) calls you whenever the connection changes: an account connected, its token refreshed or refused, or a disconnect:

app.ts
const
const stop: () => void
stop
= await
(alias) namespace account
import account
account
.
account_d_exports.onChange(callback: () => void): Promise<() => void>
export account_d_exports.onChange
onChange
(async () => {
const
const info: account.AccountInfo | null
info
= await
(alias) namespace account
import account
account
.
account_d_exports.info(): Promise<account.AccountInfo | null>
export account_d_exports.info
info
()
const render: (name: string | null) => void
render
(
const info: account.AccountInfo | null
info
?
const info: account.AccountInfo
info
.
name: string

the account's display name (a username, not necessarily a real name)

name
: null)
}) // the unsubscribe, returned at once, without waiting for the broker
// later: stop() removes this callback only, the broker keeps its registration

The callback receives no argument, so call info() again for the new state. A connect through connect() or the connect button fires it too, and a callback that throws is swallowed without stopping the others.

onChange returns its unsubscribe through a promise. Hold that promise and await it in your cleanup, or the cleanup can run before the unsubscribe exists.

The library holds one registration in the broker per broker connection, renews it when the broker is replaced, and fans it out to every callback locally, so your unsubscribe only removes your callback from the local set (see a replaced broker).

info() has no deadline, so any cap is yours, such as a Promise.race against a timer. A timer that wins means the call was not observed, never that the account is gone, so keep the state you had.

account.logout() disconnects the account from this site:

app.ts
import {
(alias) namespace account
import account
account
} from '@fkn/lib'
await
(alias) namespace account
import account
account
.
account_d_exports.logout(): Promise<void>
export account_d_exports.logout
logout
()
await
(alias) namespace account
import account
account
.
account_d_exports.info(): Promise<account.AccountInfo | null>
export account_d_exports.info
info
() // null

It is not a global sign-out: the person stays signed in on fkn.app and every other site. The broker remembers the choice, so a site under fkn.app, which otherwise connects on load, stays disconnected next time. A package that calls logout() disconnects its host app (see packages).

logout() resolves void, has no deadline and reports no failure, so info() going null is the signal to read. It also clears what the connection unlocked: cloud.fs calls answer storage: not connected afterwards, and the encryption keys the broker remembered for this app are forgotten (see encryption).

The availability probe behind fs, the file system with one copy on this device and one in the account, reports a sign-out as 'disconnected'. Of the three ConnectAvailability answers, 'connected' | 'disconnected' | 'unknown', 'disconnected' is the only one that drops work owed to the account (see the write rule).

@fkn/lib/react ships a ConnectButton for React apps. It renders an iframe of fkn.app’s connect button, which opens a fkn.app window the way connect() does:

app.ts
import {
function createElement(type: "input", props?: (React.InputHTMLAttributes<HTMLInputElement> & React.ClassAttributes<HTMLInputElement>) | null, ...children: React.ReactNode[]): React.DetailedReactHTMLElement<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> (+6 overloads)
createElement
} from 'react'
import {
const ConnectButton: ({ className, style }?: ConnectButtonProps) => React.ReactElement
ConnectButton
, type
type ConnectButtonProps = {
className?: string;
style?: React.CSSProperties;
}
ConnectButtonProps
} from '@fkn/lib/react'
export const
const Header: () => React.DetailedReactHTMLElement<React.HTMLAttributes<HTMLElement>, HTMLElement>
Header
= () =>
createElement<React.HTMLAttributes<HTMLElement>, HTMLElement>(type: React.HTMLElementType, props?: (React.ClassAttributes<HTMLElement> & React.HTMLAttributes<HTMLElement>) | null | undefined, ...children: React.ReactNode[]): React.DetailedReactHTMLElement<React.HTMLAttributes<HTMLElement>, HTMLElement> (+6 overloads)
createElement
('header', null,
createElement<ConnectButtonProps>(type: React.FunctionComponent<ConnectButtonProps>, props?: (React.Attributes & ConnectButtonProps) | null | undefined, ...children: React.ReactNode[]): React.FunctionComponentElement<ConnectButtonProps> (+6 overloads)
createElement
<
type ConnectButtonProps = {
className?: string;
style?: React.CSSProperties;
}
ConnectButtonProps
>(
const ConnectButton: ({ className, style }?: ConnectButtonProps) => React.ReactElement
ConnectButton
, {
style?: React.CSSProperties | undefined
style
: {
StandardLonghandProperties<string | number, string & {}>.width?: Property.Width<string | number> | undefined

This feature is well established and works across many devices and browser versions. It’s been available across browsers since July 2015.

Syntax: auto | <length-percentage [0,∞]> | min-content | max-content | fit-content | fit-content(<length-percentage [0,∞]>) | <calc-size()> | <anchor-size()>

Initial value: auto

| Chrome | Firefox | Safari | Edge | IE | | :----: | :-----: | :----: | :----: | :---: | | 1 | 1 | 1 | 12 | 4 |

width
: 160 } })) // a 160 by 40 iframe, over the 150 by 40 default

React is an optional peer dependency that only this entry point imports (see install). Your style merges over the defaults: no border, 150 by 40 pixels, a transparent background and color-scheme: normal, so reserve that box in your layout. With createElement the props type has to be named, since the optional parameter gives TypeScript nothing to infer it from.

The button obtains the connection token only, never the encryption key, so the account connects locked and the first cloud.fs read or write raises the unlock card (see encryption).

Its labels are Connect FKN, Connecting… and, after a blocked popup, Allow popups, retry. It never reflects the account state, so swap it for your own signed-in widget from an onChange callback.

Without React, connectButtonUrl() gives you the same page to frame by hand:

app.ts
import {
const connectButtonUrl: () => string
connectButtonUrl
} from '@fkn/lib'
const
const frame: HTMLIFrameElement
frame
=
var document: Document

window.document returns a reference to the document contained in the window.

MDN Reference

document
.
Document.createElement<"iframe">(tagName: "iframe", options?: ElementCreationOptions): HTMLIFrameElement (+2 overloads)

In an HTML document, the document.createElement() method creates the HTML element specified by localName, or an HTMLUnknownElement if localName isn't recognized.

MDN Reference

createElement
('iframe')
const frame: HTMLIFrameElement
frame
.
HTMLIFrameElement.src: string

The HTMLIFrameElement.src A string that reflects the src HTML attribute, containing the address of the content to be embedded.

MDN Reference

src
=
function connectButtonUrl(): string
connectButtonUrl
() // 'https://fkn.app/connect-button'
const frame: HTMLIFrameElement
frame
.
HTMLElement.title: string

The HTMLElement.title property represents the title of the element: the text usually displayed in a 'tooltip' popup when the mouse is over the node.

MDN Reference

title
= 'Connect your FKN account'
const frame: HTMLIFrameElement
frame
.
ElementCSSInlineStyle.style: CSSStyleDeclaration
style
.
CSSStyleDeclaration.cssText: string

The cssText property of the CSSStyleDeclaration interface returns or sets the text of the element's inline style declaration only.

MDN Reference

cssText
= 'border:none;width:150px;height:40px;background:transparent;color-scheme:normal'
var document: Document

window.document returns a reference to the document contained in the window.

MDN Reference

document
.
ParentNode.querySelector<"nav">(selectors: "nav"): HTMLElement | null (+4 overloads)

Returns the first element that is a descendant of node that matches selectors.

MDN Reference

querySelector
('nav')!.
ParentNode.append(...nodes: (Node | string)[]): void

Inserts nodes after the last child of node, while replacing strings in nodes with equivalent Text nodes.

Throws a "HierarchyRequestError" DOMException if the constraints of the node tree are violated.

MDN Reference

append
(
const frame: HTMLIFrameElement
frame
)

It takes no argument on purpose: however it is framed, the button reads which site it is on from the browser-set referrer, never from your page. The default referrer policy sends the origin, and a page whose policy strips it gets an inert button and a console warning. A package’s referrer leaves it inert too, since a package connects through its host.

promptRelay() opens the broker’s relay picker, the card where a person chooses which relay carries their sockets and proxied requests. The relay holds the real socket at the far end of net and dgram:

app.ts
import {
const promptRelay: () => Promise<string | null>
promptRelay
} from '@fkn/lib'
const
const preference: string | null
preference
= await
function promptRelay(): Promise<string | null>
promptRelay
() // 'auto', or the id of the relay the person picked
if (
const preference: string | null
preference
)
var document: Document

window.document returns a reference to the document contained in the window.

MDN Reference

document
.
ParentNode.querySelector<Element>(selectors: string): Element | null (+4 overloads)

Returns the first element that is a descendant of node that matches selectors.

MDN Reference

querySelector
('#relay')!.
Element.textContent: string | null
textContent
=
const preference: string
preference
// the label for your settings entry

It resolves with the preference in effect when the card closes, by a row, Done or the close button: 'auto', the default, or a relay id. Auto picks the closest relay with the most free capacity, and a choice applies to this site and reconnects active transfers. In a worker, and on any failure, it resolves null.

Rows appear only when a relay beyond the built-in fallback was measured. With nothing but the fallback the card says so, and Done still resolves with the stored preference. A package cannot draw the card, so the picker is forwarded to its host app’s broker.

Traffic through FKN’s relay and proxy is metered: net, dgram and http sockets through the relay, and cloud.fetch through the proxy, the server it sends every request through. The free volume is 5,000,000,000 bytes per UTC day, per connected account or per anonymized IP address when nobody is connected. Past it transfers are throttled until the day resets, and a subscription lifts the throttle. cloud.quota() reads the state:

app.ts
import {
(alias) namespace cloud
import cloud
cloud
} from '@fkn/lib'
const
const status: cloud.QuotaStatus
status
= await
(alias) namespace cloud
import cloud
cloud
.
cloud_d_exports.quota(): Promise<cloud.QuotaStatus>
export cloud_d_exports.quota
quota
()
const status: cloud.QuotaStatus
status
.
throttled: boolean

transfers are actually being rate-limited right now (overQuota and not premium)

throttled
// false
const status: cloud.QuotaStatus
status
.
remainingBytes: number

bytes of free-tier volume left today

remainingBytes
// 5000000000 at the start of a UTC day, 0 once spent

QuotaStatus carries eight fields, and @fkn/lib/cloud exports the type (see TypeScript):

FieldWhat it holds
premiumA paid subscription is active, so never throttled.
overQuotaToday’s free volume is spent, premium included.
throttledOver quota and not premium.
usedBytesBytes through the relay and proxy today, capped at limitBytes.
limitBytesThe free daily volume.
remainingBytesFree volume left today, 0 once over quota.
bytesPerSecondThe rate the service reports, in bytes per second.
bitsPerSecondThe same rate in bits, for display.

usedBytes saturates at limitBytes, so a meter never shows more than full. limitBytes is the same free figure on a premium account, so a premium account past the free volume reads overQuota: true, usedBytes === limitBytes and remainingBytes: 0 while throttled stays false. Key a meter on throttled, and hide a remaining label when premium.

bytesPerSecond is the rate the service reports for this account or anonymized address, which the relay and the proxy are meant to apply, so never hard-code a platform constant. As deployed that is 10,485,760 bytes per second under the free volume, 1,048,576 once throttled and 1,073,741,824 with a subscription.

Extension traffic is never metered, and the desktop app handles no traffic. quota lives under cloud, with no root alias.

The answer follows the caller’s own connection: a connected app reads the account’s counter, and an unconnected caller, app or package, reads its anonymized IP address’s counter. An account has one counter, shared by every app it connects. A package meters under its own scope, never the host’s (see packages).

Unlike the account calls above, quota() rejects. A failed query rejects, and so does a call still pending when the shell, the FKN surface that can update and reload the page, replaces the broker frame during an update. The second case reads FKN: the broker was replaced while this call was pending; retry it (see a replaced broker), which the poll below catches.

The broker caches the answer for 5 seconds per caller and drops it on every connection change, so a poll costs at most one request every 5 seconds whatever its interval:

app.ts
import {
(alias) namespace cloud
import cloud
cloud
} from '@fkn/lib'
const
const meter: HTMLProgressElement
meter
=
var document: Document

window.document returns a reference to the document contained in the window.

MDN Reference

document
.
ParentNode.querySelector<HTMLProgressElement>(selectors: string): HTMLProgressElement | null (+4 overloads)

Returns the first element that is a descendant of node that matches selectors.

MDN Reference

querySelector
<
interface HTMLProgressElement

The HTMLProgressElement interface provides special properties and methods (beyond the regular HTMLElement interface it also has available to it by inheritance) for manipulating the layout and presentation of progress elements.

MDN Reference

HTMLProgressElement
>('#egress')!
const
const note: HTMLElement
note
=
var document: Document

window.document returns a reference to the document contained in the window.

MDN Reference

document
.
ParentNode.querySelector<HTMLElement>(selectors: string): HTMLElement | null (+4 overloads)

Returns the first element that is a descendant of node that matches selectors.

MDN Reference

querySelector
<
interface HTMLElement

The HTMLElement interface represents any HTML element.

MDN Reference

HTMLElement
>('#egress-note')!
const
const refresh: () => Promise<void>
refresh
= async () => {
const
const status: cloud.QuotaStatus
status
= await
(alias) namespace cloud
import cloud
cloud
.
cloud_d_exports.quota(): Promise<cloud.QuotaStatus>
export cloud_d_exports.quota
quota
()
const meter: HTMLProgressElement
meter
.
HTMLProgressElement.max: number

The max property of the HTMLProgressElement interface represents the upper bound of the progress element's range.

MDN Reference

max
=
const status: cloud.QuotaStatus
status
.
limitBytes: number

free-tier daily volume before throttling kicks in

limitBytes
const meter: HTMLProgressElement
meter
.
HTMLProgressElement.value: number

The value property of the HTMLProgressElement interface represents the current progress of the progress element.

MDN Reference

value
=
const status: cloud.QuotaStatus
status
.
usedBytes: number

bytes egressed through FKN relays today (per connected account or anonymized IP), saturates at limitBytes: consumption past the free-tier volume is private and never reported

usedBytes
// never above meter.max
const note: HTMLElement
note
.
Element.textContent: string | null
textContent
=
const status: cloud.QuotaStatus
status
.
throttled: boolean

transfers are actually being rate-limited right now (overQuota and not premium)

throttled
? `Throttled to ${
var Math: Math

An intrinsic object that provides basic mathematics functionality and constants.

Math
.
Math.round(x: number): number

Returns a supplied numeric expression rounded to the nearest integer.

@paramx The value to be rounded to the nearest integer.

round
(
const status: cloud.QuotaStatus
status
.
bitsPerSecond: number

current effective egress rate cap, bits per second (for display)

bitsPerSecond
/ 1e6)} Mbit/s`
:
const status: cloud.QuotaStatus
status
.
premium: boolean

the user has a paid subscription, throttle never applies

premium
? 'Premium, no daily cap'
: `${(
const status: cloud.QuotaStatus
status
.
remainingBytes: number

bytes of free-tier volume left today

remainingBytes
/ 1e9).
Number.toFixed(fractionDigits?: number): string

Returns a string representing a number in fixed-point notation.

@paramfractionDigits Number of digits after the decimal point. Must be in the range 0 - 20, inclusive.

toFixed
(2)} GB left today`
}
await
const refresh: () => Promise<void>
refresh
()
const
const timer: NodeJS.Timeout
timer
=
function setInterval<[]>(callback: () => void, delay?: number): NodeJS.Timeout (+1 overload)
setInterval
(() => {
const refresh: () => Promise<void>
refresh
().
Promise<void>.catch<void>(onrejected?: ((reason: any) => void | PromiseLike<void>) | null | undefined): Promise<void>

Attaches a callback for only the rejection of the Promise.

@paramonrejected The callback to execute when the Promise is rejected.

@returnsA Promise for the completion of the callback.

catch
(() => {}) }, 2_000) // at most one request every 5 seconds, the cache answers the rest
// later: clearInterval(timer) stops the poll, not the metering

Branch on throttled and premium before the remaining label, since a premium account past the free volume reads remainingBytes: 0 while not throttled.

quota() has no window check, so it works in a relayed worker (see workers). In a worker nobody relayed it waits forever, where the account calls above answer false, null or a no-op.

Three rows these calls lead to:

MessageWhat happened
Not signed inThe token this site holds no longer names a live session, so the service refused a cloud.fs call: call connect() again.
@fkn/lib: no broker connection within <ms>ms, so <what> could not be requestedThe bounded wait that sockets use missed its deadline. No call on this page carries one, so a cap you impose only says the call was not observed.
Storage quota exceededThe account’s storage byte limit, not the daily volume: cloud.fs.quota() reports the headroom.

Every other message has its row on every error. The account calls never throw: they answer false, null or nothing (see handling errors). The exact signatures live in the generated reference for @fkn/lib/account, @fkn/lib/react and @fkn/lib/cloud.