Setup and manage your cloud SKDB database¶
SKDB is a local embedded database that can connect to a cloud replica and synchronise data with other users in real time. Multiple users connected to the same cloud database can communicate simply by reading and writing their local database.
This section of the manual explains how to create the cloud side of the database and how to configure it so that you can control
- the shared data schema
- privacy permissions and rules: who can read and write which rows
- application data invariants: ensure that users cannot change data that would break application rules
Create a cloud database¶
You can create a cloud database using the SKDB self service console.
https://console.skdb.io/
Here you can create an account and sign in.
Under the 'Databases' section, click 'Create a new database', provide a name that you'll use in code to establish a connection, and hit 'Submit'.
Databases are created instantly. You can get started with your new database by finding it in the list of databases in your account and clicking on the name.
On the database detail page, you can click 'Fetch and display root private key' to get hold of the credential you'll need to connect as 'root'. You can also click 'Open Live Notebook...' to interact with your new database directly in the browser using an interface similar to a Jupyter notebook.
Create a user¶
You can create a database user and fetch the credentials using code or a CLI that provides a text-based interface. Both methods are convenient for automation.
Create a user with code¶
You can create a user from the SKDB API. First, create a local database and connect to the cloud database as root - only the root user can create new users. Then can create a user:
const remoteDb = await skdb.connectedRemote();
const newUserCredentials = await remoteDb.createUser();
You do not need to provide any information for the new user. An 'access key' and 'private key' are generated for you. The access key identifies the user and the private key is the shared secret that allows the connection to authenticate with the server.
Let's demonstrate how you would use the credentials to connect:
const newUserSkdb = await createSkdb({asWorker: false});
await newUserSkdb.connect(
"example_db",
newUserCredentials.accessKey, // <==
await crypto.subtle.importKey(
"raw",
newUserCredentials.privateKey, // <==
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
)
);
console.log(
"Successfully connected as:",
await (await newUserSkdb.connectedRemote()).connectedAs()
);
newUserSkdb.closeConnection();
Create a user with the CLI¶
You can use npx skdb
to access the SKDB CLI.
You may first need to add your database root credential to allow connecting:
Then creating a user is as simple as:
$ npx skdb --db example_db --access-key root --create-user
Successfully created user: {
'fGoVtK4eCo1-hV_nbgTKphaEemA': '<NEW_USER_PRIVATE_KEY_BASE_64>'
}
Credentials were added to ~/.skdb/credentials.
Run SQL on the remote database¶
There are three ways of executing SQL on the remote database:
- using code - perfect for automation
- a CLI that provides a text based interface in your terminal - this allows conveniently running SQL scripts or interactive REPL-style debugging
- the web-based Live Notebook - a friendly and convenient way of getting started
Run SQL using code¶
With a local database connected to the server,
you can exec
queries on the remote side by first getting hold of the
RemoteSKDB
instance using connectedRemote()
.
const remoteDb = await skdb.connectedRemote();
await remoteDb.exec("INSERT INTO mirror_demo_table (n, skdb_access) VALUES (1, 'root')");
console.log(await remoteDb.exec("SELECT * FROM mirror_demo_table"));
await remoteDb.exec("DELETE FROM mirror_demo_table");
Run SQL using the CLI¶
This interactive session demonstrates how you can run SQL queries against the remote server.
$ npx skdb --db example_db --access-key root --remote-repl
root@wss://api.skiplabs.io/example_db> INSERT INTO mirror_demo_table (n, skdb_access) VALUES (1, 'root')
root@wss://api.skiplabs.io/example_db> SELECT * FROM mirror_demo_table
┌─────────┬───────────────────────────────┬───┬──────┬──────┬─────────────┐
│ (index) │ id │ n │ f │ t │ skdb_access │
├─────────┼───────────────────────────────┼───┼──────┼──────┼─────────────┤
│ 0 │ 'fGom0oFUvP5jxBb5JIiPT0kD962' │ 1 │ null │ null │ 'root' │
└─────────┴───────────────────────────────┴───┴──────┴──────┴─────────────┘
root@wss://api.skiplabs.io/example_db> DELETE FROM mirror_demo_table
root@wss://api.skiplabs.io/example_db> SELECT * FROM mirror_demo_table
root@wss://api.skiplabs.io/example_db>
Run SQL using the web-based Live Notebook¶
You can run SQL against a database using the SKDB console.
https://console.skdb.io/
On the database detail page, you can click 'Open Live Notebook...' to interact with your a database directly in the browser using an interface similar to a Jupyter notebook.
Define the data schema¶
You can define the remote schema by running SQL statements against the remote database.
With a connected SKDB client, you can define
the schema for a table using exec
on the RemoteSKDB
instance.
const remoteDb = await skdb.connectedRemote();
await remoteDb.exec(
`CREATE TABLE IF NOT EXISTS mirror_demo_table (
id TEXT PRIMARY KEY,
n INTEGER,
f FLOAT,
t TEXT,
skdb_access TEXT NOT NULL
);`
);
There are a few important things to call out from this example:
-
We create an
skdb_access
column. It must always be defined asTEXT NOT NULL
. Theskdb_access
column value defines the read and modify permissions for the row. A table cannot be mirrored without this column defined. See 'control who can see and modify rows' for detail on how this works. -
We use
TEXT
as the type for the primary key. It is best practice in SKDB to useTEXT
primary keys. SKDB will generate globally unique ids to prevent conflict with other users. -
Conflicts will be resolved using a 'last-write wins' policy because we defined a primary key. If no primary key is defined then conflicts are not automatically resolved and the application can decide how it wishes to resolve conflict. See controlling replication for detail on how this works.
-
We use
IF NOT EXISTS
so that this SQL statement can be idempotently executed in a script.
Control who can see and modify rows¶
A table can only be mirrored if it has an skdb_access
column defined
as text NOT NULL
. For example:
CREATE TABLE cannot_be_mirrored (id text PRIMARY KEY);
CREATE TABLE can_be_mirrored (id text PRIMARY KEY, skdb_access text NOT NULL);
The value stored in the skdb_access
column controls who can read or
modify the row.
Privacy in SKDB is not something you opt-in to, but must define in
order to share data with other users. This is why you cannot mirror a
table that does not have an skdb_access
column.
The value that you write in the skdb_access
column should be a:
-
user ID (often also referred to as an 'access key')
- only the user defined by this value is able to read or modify this row.
-
or a group ID
- The group with this ID defines who can read or modify this row.
- To write a row with this value, you must have the 'insert' permission. To read or delete this row, you must have the 'read' or 'delete' permission respectively.
- Please see create and manage a group for a discussion on how groups define access policies.
Track who wrote a row¶
For some applications it is important to have a guarantee on who wrote a row. Consider for example an auction, it would be essential that bids can be attributed to a user and cannot be spoofed.
SKDB provides the skdb_author
column for this purpose. skdb_author
should be defined in a table like this:
When writing a row, you must specify the user ID of the currently
connected and authenticated user as the value of the skdb_author
column. Any other value will generate an error locally. If a malicious
client tried to force replication, the transaction will be rejected by
the cloud server and will not be stored or propagated to other users.
If a user has permission (defined by the skdb_access
column), they may
delete a row that was written by another user (and so has an
skdb_author
value that is not the current user's user id). Updates
are also allowed, but you must update the skdb_author
value with the
current user's user id.
With a connected client, you can write a row
to a table with an skdb_author
column like this:
console.log(await remote.tableSchema("skdb_author_example"));
await skdb.mirror({table: "skdb_author_example", expectedColumns: "*"});
await skdb.exec(
"INSERT INTO skdb_author_example (skdb_author, skdb_access) VALUES (@me, 'read-write')",
{me: skdb.currentUser}
);
try {
await skdb.exec(
"INSERT INTO skdb_author_example (skdb_author, skdb_access) VALUES ('will_not_work', 'read-write')"
);
} catch (ex) {
console.log(ex);
}
CREATE TABLE skdb_author_example (
id TEXT PRIMARY KEY,
skdb_author TEXT NOT NULL,
skdb_access TEXT NOT NULL
);
SkRuntimeExit [Error]: INSERT INTO skdb_author_example (skdb_author, skdb_access) VALUES ('will_not_work', 'read-write')
^
|
----- ERROR
Error: line 1, character 0:
skdb_author is set to the wrong id expected root (found will_not_work) (user root)
at Utils.main (file:///private/var/folders/v3/mnrsv2tn40ncb6699fhczzgc0000gn/T/e68feef3-3bec-486f-b0dd-d7b494227cb44479666635330745170/node_modules/skdb/dist/sk_types.mjs:199:29)
at SKDBSyncImpl.main [as runLocal] (file:///private/var/folders/v3/mnrsv2tn40ncb6699fhczzgc0000gn/T/e68feef3-3bec-486f-b0dd-d7b494227cb44479666635330745170/node_modules/skdb/dist/skdb_skdb.mjs:202:33)
at file:///private/var/folders/v3/mnrsv2tn40ncb6699fhczzgc0000gn/T/e68feef3-3bec-486f-b0dd-d7b494227cb44479666635330745170/node_modules/skdb/dist/skdb_database.mjs:80:29
at SKDBSyncImpl.runner (file:///private/var/folders/v3/mnrsv2tn40ncb6699fhczzgc0000gn/T/e68feef3-3bec-486f-b0dd-d7b494227cb44479666635330745170/node_modules/skdb/dist/skdb_skdb.mjs:193:30)
at SKDBSyncImpl.exec (file:///private/var/folders/v3/mnrsv2tn40ncb6699fhczzgc0000gn/T/e68feef3-3bec-486f-b0dd-d7b494227cb44479666635330745170/node_modules/skdb/dist/skdb_database.mjs:78:25)
at SKDBImpl.exec (file:///private/var/folders/v3/mnrsv2tn40ncb6699fhczzgc0000gn/T/e68feef3-3bec-486f-b0dd-d7b494227cb44479666635330745170/node_modules/skdb/dist/skdb_database.mjs:275:46)
at file:///private/var/folders/v3/mnrsv2tn40ncb6699fhczzgc0000gn/T/e68feef3-3bec-486f-b0dd-d7b494227cb44479666635330745170/src.mjs:30:14
at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
code: 1,
cause: SkException [Error]: skdb_author is set to the wrong id expected root (found will_not_work) (user root)
at Utils.ethrow (file:///private/var/folders/v3/mnrsv2tn40ncb6699fhczzgc0000gn/T/e68feef3-3bec-486f-b0dd-d7b494227cb44479666635330745170/node_modules/skdb/dist/sk_types.mjs:342:23)
at LinksImpl.js_throw (file:///private/var/folders/v3/mnrsv2tn40ncb6699fhczzgc0000gn/T/e68feef3-3bec-486f-b0dd-d7b494227cb44479666635330745170/node_modules/skdb/dist/sk_runtime.mjs:30:53)
at toWasm.js_throw (file:///private/var/folders/v3/mnrsv2tn40ncb6699fhczzgc0000gn/T/e68feef3-3bec-486f-b0dd-d7b494227cb44479666635330745170/node_modules/skdb/dist/sk_runtime.mjs:127:58)
at SKIP_throw (wasm://wasm/010cfefa:wasm-function[7053]:0x146686)
at sk.SKDB_AccessSolver__checkWrite (wasm://wasm/010cfefa:wasm-function[5407]:0xd0d91)
at sk.SKStore_EagerDir__writeEntry (wasm://wasm/010cfefa:wasm-function[3881]:0x7f014)
at sk.SKDB_Evaluator__insertTable (wasm://wasm/010cfefa:wasm-function[4166]:0x91430)
at SKDB.Evaluator__insert__Closure2__call__Closure0__call (wasm://wasm/010cfefa:wasm-function[4165]:0x912a1)
at sk.vtry__Closure0__call.16 (wasm://wasm/010cfefa:wasm-function[4959]:0xbb5c0)
at SKIP_call0 (wasm://wasm/010cfefa:wasm-function[6918]:0x141570)
}
Define application rules to control which data updates are allowed¶
SKDB provides 'CHECK
expressions' that can be used with reactive
views. CHECK
expressions are a
powerful mechanism for expressing and maintaining data constraints in
the database.
When you define your schema, you can create reactive views that use
CHECK
expressions. This allows you to define application rules at
schema definition time and reject data updates from users that do not
meet these rules.
With a client created, here is a simple example of a reactive view that ensures a data constraint:
await skdb.exec(`
CREATE TABLE accounts(id text PRIMARY KEY, balance integer NOT NULL);
`);
await skdb.exec(`
CREATE REACTIVE VIEW account_balance_check AS
SELECT
CHECK(balance >= 0) AS balance_is_non_negative
FROM
accounts
`);
// ok
await skdb.exec("INSERT INTO accounts VALUES ('example_account', 100)");
// not ok
try {
await skdb.exec(`
UPDATE
accounts
SET
balance = balance - 200
WHERE
id = 'example_account'
`);
} catch (ex) {
console.log(ex);
}
SkRuntimeExit [Error]: ^
|
----- ERROR
Error: line 1, character 0:
Failed constraint 'balance_is_non_negative': balance >= 0
at Utils.main (file:///private/var/folders/v3/mnrsv2tn40ncb6699fhczzgc0000gn/T/dfcdbed1-3ac7-4150-b373-2432fb90998918347521571839203848/node_modules/skdb/dist/sk_types.mjs:199:29)
at SKDBSyncImpl.main [as runLocal] (file:///private/var/folders/v3/mnrsv2tn40ncb6699fhczzgc0000gn/T/dfcdbed1-3ac7-4150-b373-2432fb90998918347521571839203848/node_modules/skdb/dist/skdb_skdb.mjs:202:33)
at file:///private/var/folders/v3/mnrsv2tn40ncb6699fhczzgc0000gn/T/dfcdbed1-3ac7-4150-b373-2432fb90998918347521571839203848/node_modules/skdb/dist/skdb_database.mjs:80:29
at SKDBSyncImpl.runner (file:///private/var/folders/v3/mnrsv2tn40ncb6699fhczzgc0000gn/T/dfcdbed1-3ac7-4150-b373-2432fb90998918347521571839203848/node_modules/skdb/dist/skdb_skdb.mjs:193:30)
at SKDBSyncImpl.exec (file:///private/var/folders/v3/mnrsv2tn40ncb6699fhczzgc0000gn/T/dfcdbed1-3ac7-4150-b373-2432fb90998918347521571839203848/node_modules/skdb/dist/skdb_database.mjs:78:25)
at SKDBImpl.exec (file:///private/var/folders/v3/mnrsv2tn40ncb6699fhczzgc0000gn/T/dfcdbed1-3ac7-4150-b373-2432fb90998918347521571839203848/node_modules/skdb/dist/skdb_database.mjs:275:46)
at file:///private/var/folders/v3/mnrsv2tn40ncb6699fhczzgc0000gn/T/dfcdbed1-3ac7-4150-b373-2432fb90998918347521571839203848/src.mjs:19:14 {
code: 1,
cause: SkException [Error]: Failed constraint 'balance_is_non_negative': balance >= 0
at Utils.ethrow (file:///private/var/folders/v3/mnrsv2tn40ncb6699fhczzgc0000gn/T/dfcdbed1-3ac7-4150-b373-2432fb90998918347521571839203848/node_modules/skdb/dist/sk_types.mjs:342:23)
at LinksImpl.js_throw (file:///private/var/folders/v3/mnrsv2tn40ncb6699fhczzgc0000gn/T/dfcdbed1-3ac7-4150-b373-2432fb90998918347521571839203848/node_modules/skdb/dist/sk_runtime.mjs:30:53)
at toWasm.js_throw (file:///private/var/folders/v3/mnrsv2tn40ncb6699fhczzgc0000gn/T/dfcdbed1-3ac7-4150-b373-2432fb90998918347521571839203848/node_modules/skdb/dist/sk_runtime.mjs:127:58)
at SKIP_throw (wasm://wasm/010cfefa:wasm-function[7053]:0x146686)
at sk.SKDB_ExprEvaluator__evalCIExpr (wasm://wasm/010cfefa:wasm-function[3668]:0x753f7)
at sk.SKDB_ExprEvaluator__evalCGExpr (wasm://wasm/010cfefa:wasm-function[3746]:0x786a0)
at sk.SKDB_SelectEvaluator___ConcreteMetaImpl__evalRow (wasm://wasm/010cfefa:wasm-function[3510]:0x6a7bd)
at sk.SKDB_SelectEvaluator__evalSelectRow (wasm://wasm/010cfefa:wasm-function[6659]:0x131ff0)
at sk.SKDB_SelectEvaluator__evalSelectTableRow (wasm://wasm/010cfefa:wasm-function[6655]:0x131c55)
at sk.SKDB_SelectEvaluator__evalSelectTableRows (wasm://wasm/010cfefa:wasm-function[3503]:0x6a085)
}
A CHECK
expression can be used on any SELECT
query. If the
expression is false for any row, the query fails with an error. This
is primarily useful when used with a reactive view. When we use
CHECK
with a reactive view the CHECK
expression is evaluated
-
whenever the database is updated. On any update to a table, SKDB propagates this in a cascade to all dependent reactive views. If a constraint no longer holds it will fail, cascading back up and causing the transaction as a whole to fail.
-
only when it needs to be. The SKDB engine was purpose built to maintain queries incrementally. As data is changed in source table(s) any dependent views are updated very efficiently.
Because the check is expressed in a select query it works with
anything that you can do with SELECT
. You can express constraints on
rows and columns, aggregated data using GROUP BY
, and even across
tables with joins.
You can define reactive views
- remotely to ensure that illegal updates are not accepted and propagated to other users
- locally to prevent errors without going to the server and to ensure the application works entirely offline
- or both locally and remotely, for the best of both worlds
Examples¶
Here are some example queries to get a sense of what you can achieve with this.
We can ensure that new rows are written using a particular skdb_access group id. Here we express that posts for a group of users must use the group's SKDB group.
CREATE REACTIVE VIEW post_access_check AS
SELECT
CHECK(post.skdb_access = grp.members) AS group_posts_use_correct_skdb_access
FROM
posts post
INNER JOIN groups grp ON p.grp = g.group_id;
Notice that we are making use of a join to do this. This is impossible
to express using traditional table CHECK
expressions.
We are only specifying constraints in this query, so it is ephemeral: there is nothing stored outside of what is necessary to make the view incremental, it exists purely for the side effect that will prevent writes from breaking the invariant.
But if we want, we can also materialise data to make this reactive view usable for queries as well as checking writes:
CREATE REACTIVE VIEW post_access_check AS
SELECT
post.title AS post_title,
grp.name AS group_posted_to,
CHECK(post.skdb_access = grp.members) AS group_posts_use_correct_skdb_access
FROM
posts post
INNER JOIN groups grp ON p.grp = g.group_id;
To demonstrate how aggregations can be useful, here's how we could prevent a user spamming they system with requests. After 5, further requests will be rejected.