@skystate/core
@skystate/core is the framework-agnostic SkyState client. It loads public state, stores SkyState auth tokens through a pluggable storage adapter, and reads or writes user state after login.
Basic Example
ts
import { createSkyStateClient } from '@skystate/core';
const client = createSkyStateClient({
account: 'acc_example',
project: 'my-app',
environment: 'production',
});
await client.init();
const banner = client.publicState.get('banner', { enabled: false });
client.setAuthTokens({ idToken, refreshToken });
const theme = client.userState.get('theme', 'dark');
client.userState.set('theme', 'light');Installation
See Installation - SDKs.
Creating A Client
ts
import { createSkyStateClient } from '@skystate/core';
const client = createSkyStateClient({
apiUrl: 'https://api.skystate.io',
account: 'acc_example',
project: 'my-app',
environment: 'production',
});
await client.init();account, project, and environment are required. fetch and storage can be supplied for tests, non-browser runtimes, or custom token persistence.
createSkyStateClient(options)
ts
function createSkyStateClient(options: SkyStateClientOptions): SkyStateClient| Option | Type | Required | Description |
|---|---|---|---|
account | string | Yes | Account route identifier, for example acc_example. |
project | string | Yes | Project identifier. |
environment | string | Yes | Environment slug (full name only, no aliases). See Environments. |
apiUrl | string | No | SkyState API base URL. Defaults to https://api.skystate.io. |
fetch | typeof globalThis.fetch | No | Custom fetch implementation. |
storage | AuthTokenStorage | No | Custom token storage adapter. |
Every request the client sends (public-state loads, user-state reads and writes, and auth token refresh) runs under a hard 10-second deadline. A request that stalls past it is aborted and classified as a retryable no_response network failure. The deadline is fixed; there is no option to override it.
Public State
Public state is read during init() and is available without end-user auth. See what public state is for.
ts
const banner = client.publicState.get('banner', { enabled: false });Keys are top-level state keys. They are not nested paths.
client.publicState.get(key, fallback?)
ts
get(key: string, fallback?: unknown): unknown| Parameter | Description |
|---|---|
key | Top-level public-state key. |
fallback | Returned when public state is not ready or the key is absent; see Fallback semantics. |
Auth
The core client does not open browser windows. Pass tokens from your hosted auth flow into the client:
ts
client.setAuthTokens({
idToken,
refreshToken,
});clearAuthTokens() clears local auth and user state without contacting SkyState servers. logout() signs the user out through SkyState and also clears local auth and user state.
Auth methods
| Method | Signature | Description |
|---|---|---|
setAuthTokens | (tokens: { idToken: string; refreshToken: string }) => void | Attaches hosted-auth tokens to the client. |
clearAuthTokens | () => void | Clears local auth and user state. |
logout | () => Promise<void> | Signs out through SkyState and clears local auth state. |
beginAuthenticate | () => void | Marks the client as authenticating while a hosted login flow is in progress. |
User State
User state requires an authenticated end-user bearer scoped to the same account, project, and environment.
ts
const theme = client.userState.get('theme', 'dark');
client.userState.set('theme', 'light');
const unsubscribe = client.userState.subscribe('theme', () => {
console.log(client.userState.get('theme'));
});Writes update local subscribers immediately. A write the server accepts is persisted; a write the server rejects is rolled back and reported through the user-state status surface, and temporary network, sign-in renewal, and quota issues are retried in the background. See Error Handling for the per-status contract.
Keys are top-level state keys such as theme, profile, or preferences. Keys must be non-empty and cannot contain / or ~; these rules apply to every keyed accessor and hook.
API
| Method | Signature | Description |
|---|---|---|
get | (key: string, fallback?: unknown) => unknown | Reads the current user-state value for a key. |
set | (key: string, value: NonNullable<unknown> | null) => void | Writes a user-state value or updater function. undefined is a type error: removal goes through clear. |
clear | (key: string) => void | Removes the key for the signed-in user. |
subscribe | (key: string, listener: () => void) => () => void | Subscribes to changes for a key. Returns an unsubscribe function. |
isKeyWriting | (key: string) => boolean | Whether a write for the key is still awaiting server confirmation. |
syncStatus | (key: string) => UserStateSyncStatus | Sync state of the key's stored value: 'unset', 'syncing', or 'synced'. |
subscribeWriting | (key: string, listener: () => void) => () => void | Subscribes to changes in the key's writing status. Returns an unsubscribe function. |
draft | <T>(key: string, fallback?: T) => UserStateDraftHandle<T | null> | Creates a draft handle for edit-then-save UI. Both overloads return UserStateDraftHandle<T | null>. |
client.userState.get(key, fallback?)
ts
const theme = client.userState.get('theme', 'dark');| Parameter | Description |
|---|---|
key | Top-level user-state key. |
fallback | Returned when user state is not ready or the key is absent; see Fallback semantics. |
client.userState.set(key, value)
ts
client.userState.set('theme', 'light');| Parameter | Description |
|---|---|
key | Top-level user-state key. |
value | JSON-serializable value to save for the signed-in user. undefined is a type error: removing a key goes through clear(key). null is a deliberate stored-null write and stays allowed. |
Returns void. The local value updates immediately; save errors are reported through user-state status and client subscribers. A value-form write whose value equals the current server-confirmed value is dropped before sending, so no new version is created and no usage is counted for it; an updater-function write always sends and lets the server decide whether it is a no-op.
client.userState.subscribe(key, listener)
ts
const unsubscribe = client.userState.subscribe('theme', () => {
console.log(client.userState.get('theme', 'dark'));
});
unsubscribe();| Parameter | Description |
|---|---|
key | Top-level user-state key. |
listener | Callback invoked when the key's visible value changes. |
Returns a function that unsubscribes the listener.
client.userState.draft(key, fallback?)
Creates a local draft handle for staged edits:
ts
const profile = client.userState.draft('profile', { displayName: '' });
profile.set((current) => ({ ...current, displayName: 'Ada' }));
profile.save();Use profile.save() to save the drafted value, or profile.discard() to drop local form edits.
Both overloads return UserStateDraftHandle<T | null>; see Fallback semantics for why a fallback does not narrow the type. The rows below write the handle's value type as V = T | null.
| Draft member | Type | Description |
|---|---|---|
get() | () => V | Drafted value if present, otherwise committed value or fallback. |
committed() | () => V | Committed value, ignoring local draft edits. |
hasDraft() | () => boolean | Whether a local draft value exists. |
set(value) | (value: V | ((displayValue: V) => V)) => void | Updates the local draft only. |
save() | () => void | Saves the current draft value. |
discard() | () => void | Drops the local draft value. |
subscribe(listener) | (listener: () => void) => () => void | Subscribes to draft or committed-value changes. |
dispose() | () => void | Releases handle-local subscriptions. |
Fallback Semantics
Every keyed read takes an optional fallback: client.publicState.get(key, fallback?), client.userState.get(key, fallback?), the get() and committed() methods of the handle returned by client.userState.draft(key, fallback?), and the React hooks built on them (usePublicState, useUserState). The rule is the same everywhere:
- When the state is not ready or the key is absent, the read returns
fallback, ornullwhen no fallback is given. - A stored
nullis a value, not an absence. It is returned as-is and is never replaced by the fallback. - Because of that, a fallback never narrows the type: keyed reads, hooks, and draft handles are typed
T | nulleven when a fallback is passed.
The exported applyFallback(raw, fallback) helper implements this rule: it returns fallback when raw is undefined, null when raw is undefined and no fallback is given, and any other value, including null, unchanged.
Cross-Tab Sync
When a write commits in one tab, other same-origin tabs on the same account, project, and environment refetch user state to pick up the change. Sync is active only once the tab is authenticated. Where the runtime offers no cross-tab messaging (server-side rendering, older browsers, sandboxed frames), the client behaves as a single tab.
Snapshot And Lifecycle
Use client.getSnapshot() and client.subscribe() when building an integration layer:
ts
const unsubscribe = client.subscribe(() => {
const snapshot = client.getSnapshot();
console.log(snapshot.publicState.status, snapshot.auth.status, snapshot.userState.status);
});The snapshot contains lifecycle, publicState, auth, and userState subtrees.
When auth.status is 'authenticated', the snapshot also carries the signed-in user's name and email (each string | null). A userState snapshot in the 'error' state always includes data alongside error: the values visible when the error occurred, typed Readonly<Record<string, unknown>> | null.
Failure Handling
Transient failures (network, 5xx, rate limits) retry with capped backoff and quota failures on a slower pace; content rejections (patch_* validation, 415) are dropped and rolled back; a version conflict (412) is resolved by transparent refetch-and-replay; an authentication rejection resets the session unless the rejected credential was already superseded. See Error Handling for the full per-status contract. Auth failures surface on the auth snapshot, operational failures share one set of health codes (isHealthError(error) narrows to them), and write rejections surface on the user-state snapshot with the failing key in error.path.
Errors
SkyStateError is a discriminated union: narrow on error.code before reading variant fields. The full code list, HTTP-status mapping, and per-variant fields live in Error Handling.
Value-Only Metadata Contract
The SDK returns application values and subsystem status only. Use the console, CLI, or REST API when you need inspection, audit, or history metadata.
Exports
| Export | Description |
|---|---|
createSkyStateClient(options) | Create a framework-agnostic client. |
SkyStateClient, SkyStateClientOptions | Client interface and construction options. |
ClientSnapshot, ClientLifecycle | Client lifecycle and combined snapshot types. |
PublicStateSnapshot, AuthSnapshot, UserStateSnapshot | Public, auth, and user-state snapshot types. |
PublicStateAccessor, UserStateAccessor, UserStateDraftHandle, UserStateSyncStatus | Accessor, draft-handle, and sync-status types. |
SkyStateError, SkyStateErrorCode | Discriminated error union and its code set; see Error Handling. |
AuthenticationError, ResponseError, RateLimitError, WriteError, TransportError, UsageError, AsyncMisuseError | Member types of the SkyStateError union. |
isHealthError(), SkyStateHealthError, SkyStateAuthError | Operational-health and auth facets of the error union. |
applyFallback(raw, fallback) | Fallback helper used by the state accessors; implements Fallback semantics. |
decodeJwtPayload() | Decode JWT payloads for inspection. |
createMemoryStorage() / createLocalStorage() / AuthTokenStorage | Auth token storage adapters and interface. |
buildStorageKey() | Build the SDK auth storage key for an account/project/environment. |
toJsonPointer() / resolvePointer() | Lower-level JSON Pointer helpers. Normal state accessors take top-level keys. |
INITIAL_SNAPSHOT | The initial unauthenticated client snapshot. |
assertNever() | Exhaustiveness helper for TypeScript discriminated unions. |
RefreshOutcome | Token-refresh outcome type used by advanced integrations. |