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 guide: sign-in, data and sync

You build the app once. Each user points it at their own node and signs in there; the node holds their account and the app’s data. This guide is the path most apps take, using the Cloudoak class, which looks after accounts and sync for you. The lower-level pieces it is built from are in the reference.

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.

The complete, runnable version of everything below is examples/notes in the node’s repository (about 100 lines). examples/starter is smaller still and is the one to copy.

Sign-in happens on the node’s own page, and the node returns the user only to an address your manifest lists, exactly:

{
"app": { "id": "com.example.notes", "name": "Notes", "version": "1.0.0", "publisher": "example" },
"auth": {
"redirect_uris": ["https://notes.example.dev/", "com.example.notes:/callback"],
"origins": ["https://notes.example.dev"]
}
}

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

import { Cloudoak } from '@cloudoak/sdk';
const oak = await Cloudoak.init({
appId: 'com.example.notes',
appVersion: '1.0.0', // this build; matches the manifest it was made for
redirectUri: 'https://notes.example.dev/', // one of auth.redirect_uris
storage: localStorage, // where accounts are remembered
});

Call it once, on every start. It loads the accounts this device knows, finishes a sign-in if the node has just sent the user back, and makes the account that was active last active again. After it, oak.accounts.active is either an account or null.

storage is anything with getItem, setItem and removeItem. Without it accounts live in memory and are gone on reload. On a phone, pass a secure store (Keychain, Keystore).

The user types the address of their node. You never ask for a password: the node does, on its own page, and it also draws the screen where the user approves your app.

await oak.accounts.add('cloud.example.com');

On the web this leaves your page for the node’s and comes back to redirectUri; Cloudoak.init finishes the job on the way back. In a native app, pass openAuthSession to init to open the system browser, and add resolves with the new account:

const oak = await Cloudoak.init({
appId, appVersion, storage,
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;
},
});

Things that can come back instead of an account, as a CloudoakError with a code:

  • pending_approval: this node has not seen your app before and its owner has to approve it. Tell the user to ask them.
  • client_update_required, app_upgrade_required: this build and the version installed on the node do not fit. CloudoakVersionNotice renders the right message for either.

A write can also fail with out_of_credits on a node that charges for writes, when the user’s balance there is used up. Reading still works and nothing is lost; tell the user to ask whoever runs the node.

A user can be signed in to several nodes, or as several people on one:

oak.accounts.list(); // every account on this device, most recent first
await oak.accounts.switchTo(id); // local and instant
await oak.accounts.signOut(id); // revokes its tokens on its node, forgets it here

Show the user which account they are in: account.user.username and account.nodeName. The SDK never gives you an email address.

const notes = oak.tables('notes');
await notes.create({ title: 'Hello' });
const page = await notes.list(undefined, 50);
oak.documents('settings'); // JSON documents
oak.vectors('note_embeddings'); // vector search
oak.files('attachments'); // files

Every call reads and writes the signed-in user’s own data. Nobody else on the node reaches it through your app, and nobody reads it without signing in. In the manifest a read rule is "owner" (or "none"); "create": ["authenticated"] lets any signed-in user add rows of their own.

Tokens are refreshed for you. Two tabs refreshing at once is fine.

There is no push channel. The node keeps a change feed per app, and an app follows it by polling:

oak.on('sync', ({ changes, resync }) => {
if (resync) reloadEverything(); // the first poll, or the node says start over
else apply(changes); // [{ table, rowId, op, at }, …]
});
oak.startSync(5000); // poll every 5 seconds
// oak.stopSync();

What you can rely on:

  • Nothing is skipped. A change appears in the feed once every write that began before it has finished, so two overlapping writes can never hide one another. This also means your own write may take a moment to show up in the feed; update the screen from the write’s own answer, not from the feed.
  • Your place is remembered per account, across restarts. You never handle a cursor.
  • resync: true means “load in full”, and then carry on. You get it on the first poll, after the node was restored from a backup, and if the app has not synced for longer than the node keeps its feed (30 days by default). Treat it as ordinary: it is how the node says the short way is not available.
  • Poll while the app is visible, at an interval that suits it (5 to 30 seconds). Stop when it is hidden. startSync stops by itself on sign-out.
oak.on('account:signed-out', ({ account, reason }) => {
// 'user' they chose to
// 'revoked' the node ended it: password changed, access removed, account closed
// 'expired' 90 days since they signed in
});

For revoked and expired, say what happened and offer to sign in again. Do not retry silently.

@cloudoak/sdk/ui has a hook and two small components. The main entry never loads React.

import { AccountMenu, CloudoakVersionNotice, useSession } from '@cloudoak/sdk/ui';
function App({ oak }) {
const { account } = useSession(oak); // re-renders when accounts change
return (
<>
<AccountMenu oak={oak} /> {/* the gear: node, user, switch, add, sign out */}
{account && <Notes oak={oak} />}
</>
);
}

There is no sign-in form and no approval sheet in the SDK. Both are the node’s.

A user who approves your app gives it all of their data in your app on their node, and nothing else: not other apps, not their account. The scopes in your manifest describe what you use and are shown at approval; they are not enforced one by one today.

  • The manifest lists every address you return to, and every origin your web build runs on.
  • appVersion is this build’s version.
  • Accounts are kept in durable storage (a secure store on native).
  • The screen always says which node and which user.
  • resync reloads; account:signed-out tells the user something.
  • A failed write keeps what the user typed.

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