Skip to content
The SDK and node releases can be downloaded only by accounts invited to their repositories, while Cloudoak is in its private alpha.

SDK reference

Build once, run against any node. You publish the app’s signed manifest to a registry; a node’s owner enables it there. The user types their node URL into the gear, signs in on that node’s own page and approves the app, and the node holds their account and the app’s data. See examples/notes in the node’s repository for the full sample, and spec/openapi.json (pinned from the spec in cloudoakhq/cloudoak-developer) for the wire contract.

The SDK is not on npm yet. It is developed in this repository, cloudoakhq/cloudoak-sdk, and ships as a tarball with each of its releases (cloudoak-sdk-2.0.0.tgz, with the release’s SHA256SUMS). Download it and install the file:

Terminal window
npm install ./cloudoak-sdk-2.0.0.tgz

It has no dependencies. React is an optional peer, needed only for @cloudoak/sdk/ui.

New here? From zero to an app on a node takes the starter app from an empty folder to signed, published, installed and upgraded, with every command.

Then the guide: sign-in, data and sync with the Cloudoak class, which is what most apps use. This page is the reference for the pieces underneath it.

import { CloudoakClient, tables } from '@cloudoak/sdk';
const client = await CloudoakClient.connect({
nodeUrl: 'https://cloud.example.com', // normalized to https; http only for localhost
appId: 'com.example.notes',
appVersion: '1.0.0', // this build's version; required
});
// Discovery (/.well-known/cloudoak) is verified before anything else;
// credentials only ever go to this URL afterwards.

appVersion is required and must be semver (1.2.0, 1.2.0-rc.1). Use the version of this build, which should match app.version in the manifest the build was made for. The SDK sends it as X-Cloudoak-App-Version on every request, along with its own version in X-Cloudoak-SDK-Version, and as app_version on the browser sign-in URL. See app versions for what the node does with it.

Sign-in happens on the node’s own page. The app sends the user there, the node asks for the password and for approval, and the app gets a one-time code back. The app never sees the password, and the approval screen is drawn by the node, not by the app being approved.

The address the node returns to must be listed, exactly, in the manifest:

"auth": {
"redirect_uris": ["https://notes.example.dev/", "com.example.notes:/callback"],
"origins": ["https://notes.example.dev"]
}

redirect_uris are https addresses, http://localhost addresses for development, or a custom scheme in reverse-DNS form for native apps. origins are the web origins allowed to call the node for this app; a browser app on any other origin is refused. Native apps need none.

Web:

const client = await CloudoakClient.connect({
nodeUrl, appId: 'com.example.notes', appVersion: '1.2.0',
redirectUri: 'https://notes.example.dev/',
});
// On every page load: finish a sign-in the node just sent back.
if (!(await client.completeSignIn())) {
// From the gear: leave for the node's page.
await client.signIn();
}

Native apps pass the system browser session and get the tokens from one call:

const client = await CloudoakClient.connect({
nodeUrl, appId: 'com.example.notes', appVersion: '1.2.0',
redirectUri: 'com.example.notes:/callback',
openAuthSession: async (url, redirectUri) => {
const result = await WebBrowser.openAuthSessionAsync(url, redirectUri);
if (result.type !== 'success') throw new Error('sign-in was cancelled');
return result.url;
},
});
await client.signIn();

client.session() says who signed in (user.username, never an email) and which node this is.

An app the node has not seen before is installed on first sign-in only if the node’s operator trusts its publisher; otherwise signIn fails with pending_approval until an admin approves it.

A node operator can turn on the older password grant (CLOUDOAK_APP_PASSWORD_LOGIN=on), where an app collects the password itself. It is off by default and this SDK does not use it: no function or component in the SDK takes a password, and a test fails if one ever does.

Refresh tokens rotate on every use; reuse revokes the family (refresh_revoked: sign in again). logout() revokes the family.

await tables(client, 'notes').list();
await documents(client, 'settings').create({ theme: 'dark' });
await vectors(client, 'note_embeddings').query({ vector, k: 5 });
const { putUrl } = await files(client, 'attachments').reserve('a.pdf', size, 'application/pdf');
await changes(client, sinceCursor);

Live updates are GET /changes polling only. Start with no cursor, keep the next value each poll returns and send it back; it is an opaque string, so never build or compare one. Poll on an interval that fits the app (5 to 30 seconds while visible), back off on failures, and keep the cursor across restarts.

The feed does not skip entries, including when two writes overlap. A change appears once every write that started before it has finished, so your own write can take a moment to show up in the feed. If a poll answers resync: true the cursor could not be continued (the node was restored from a backup): reload what you show and carry on from next.

Files: files(client, bucket).reserve(name, size, contentType) returns an upload URL that only accepts exactly that size and content type. Upload with uploadBytes, then call finalize(id); the file can be downloaded only after the node has checked what arrived. Downloads are served as attachments. An upload that is never finalized stops counting against quota after 20 minutes and is cleared soon after.

Short access tokens stay in memory; refresh tokens live in a TokenStore — memory default for web, localStorage adapter, or a native secure-store adapter (Keychain/Keystore). Apps may supply their own store.

A user who approves your app gives it all of their data in your app on that node, and nothing else. Manifest scopes are shown on the node’s sign-in page as your description of what the app uses; the node does not enforce them one by one, and the page says so. An app token never works on another app, or on the user’s account, grants or developer routes.

Every account can mint API tokens (oak_pat_...) in the console (Account > API tokens), for scripts and devices that use its own data. A token is limited to its scopes: data:read or data:write for every app, app:<app-id>:read or app:<app-id>:write for one app. It reaches what the account can reach in the app and no more, and no token manages apps: that is done in the terminal on the node. Send it as Authorization: Bearer. It needs no app version header.

Every failure is a typed CloudoakError (consent_required, pending_approval, refresh_revoked, not_installed, …) with the HTTP status and node request id. pending_approval means an admin must approve the install first — show it, don’t retry blindly.

A node has one version of your app’s manifest installed, and it accepts builds from the manifest’s min_client up to that installed version. Outside that range a call fails with one of two errors:

  • client_update_required (426): this build is older than the node accepts. Ask the user to update the app.
  • app_upgrade_required (409): this build is newer than what the node has installed. The node’s admin has to upgrade the app; the user cannot fix it from the device.

Both carry clientVersion, installedVersion and minClientVersion (unset when the manifest has no minimum). Tell them apart from other failures with isVersionMismatch, and check error.code, not the status: 409 is also an ordinary write conflict.

import { CloudoakClient, isVersionMismatch } from '@cloudoak/sdk';
const client = await CloudoakClient.connect({
nodeUrl, appId: 'com.example.notes', appVersion: '1.2.0',
// Called for every version mismatch, whichever call hit it.
onVersionMismatch: (error) => showUpdateBanner(error),
});
try {
await tables(client, 'notes').list();
} catch (error) {
if (isVersionMismatch(error)) { /* already reported to the hook */ }
}

The user stays signed in through a mismatch. Tokens are kept, and a refresh that is turned away does not use up the refresh token, so the app works again as soon as it is updated or the node is upgraded.

@cloudoak/sdk/ui ships the gear button (React peer, optional); the headless entry never loads React. There is no sign-in form or consent sheet in it: both are on the node’s page. CloudoakVersionNotice takes any caught error and renders an update or ask-your-admin message when it is a version mismatch, and nothing otherwise:

<CloudoakVersionNotice error={error} appName="Notes" />

Built from spec 2.1.0, SDK 2.0.0 and the node's guide at v0.6.0-alpha.1.