Guides
Write Data

Write Data

You must define a collection before writing data to Polybase DB.

You can view our example apps Simple CRUD and Polybase DB Social to see it working in action.

Creating a record

You can create a new record for a collection by calling .create([arg1, arg2, etc]) on your collection (as defined by the collection schema constructor function). This will call the constructor function of your collection, and create a new record (as long as the id of the record does not already exist).

import { Polybase } from "@polybase/client"
 
const db = new Polybase({ defaultNamespace: "your-namespace" });
const collectionReference = db.collection("City");
 
async function createRecord () {
  // .create(args) args array is defined by the constructor fn
  const recordData = await collectionReference.create([
    "new-york", 
    "New York", 
    db.collection("Person").record("johnbmahone")
  ]);
}

The data returned would be:

{
  "block": { "hash": "...", ... },
  "data": {
    "id": "new-york",
    "name": "New York",
    "mayor": { "collectionId": "your-namespace/Person", "id": "johnbmahone" }
  }
}

And your collection schema might look like:

@public
collection Person {
  id: name;
  ...
}
 
@public
collection City {
  id: string;
  name: string;
  mayor: Person;
 
  constructor (id: string, name: string, mayor: Person) {
    this.id = id;
    this.name = name;
    this.mayor = mayor;
  }
}

Updating a record

You can update collection data using the collection methods defined in the collection.

import { Polybase } from "@polybase/client"
 
const db = new Polybase({ defaultNamespace: "your-namespace" });
const collectionReference = db.collection("City");
 
async function updateRecord () {
  // .create(functionName, args) args array is defined by the updateName fn in collection schema
  const recordData = await collectionReference
    .record("new-york")
    .call("updateMayor", [db.collection("Person").record("johnbmahone")]);
}

Would require the following collection schema:

@public
collection Person {
  id: name;
  ...
}
 
@public
collection City {
  id: string;
  name: string;
  mayor: Person;
 
  constructor (id: string, name: string, mayor: Person) {
    this.id = id;
    this.name = name;
    this.mayor = mayor;
  }
 
  updateMayor(mayor: Person) {
    this.mayor = mayor;
  }
}

Permissions

You can control who is allowed to make changes to a record by using the ctx.publicKey which is set to the publicKey of the user that signed the request.

A common use case is to store the ctx.publicKey of the user who created the collection, and then use this to determine if the change is valid.

For example:

collection CollectionName () {
  id: string;
  name: string;
  publicKey: PublicKey;
 
  constructor (id: string, name: string) {
    this.id = id;
    this.name = name;
    this.publicKey = ctx.publicKey;
  }
 
  setName (name: string) {
    if (this.publicKey != ctx.publicKey) {
      throw error('invalid public key');
    }
    this.name = name;
  }
}

Signing Requests

To sign requests from the client, you must define a signer function that will be called for every request.

import { Polybase } from "@polybase/client"
import * as eth from "@polybase/eth"
 
// Init
const db = new Polybase({ defaultNamespace: "your-namespace" })
 
// Add signer fn
db.signer(async (data: string) => {
  // A permission dialog will be presented to the user
  const accounts = await eth.requestAccounts();
 
  // If there is more than one account, you may wish to ask the user which
  // account they would like to use
  const account = accounts[0];
 
  const sig = await eth.sign(data, account);
 
  return { h: "eth-personal-sign", sig };
})

If you want to skip signing for specific requests you can return null to skip the signing process.

Using wallet through an extension

You can use the signing process provided by a user's browser extension (e.g. Metamask). Using this approach, every write must be individually approved by the user (i.e. a dialog will appear for them to approve), which may not be an optimal user experience.

import { Polybase } from "@polybase/client"
import * as eth from "@polybase/eth"
 
// Init
const db = new Polybase({ defaultNamespace: "your-namespace" })
 
async function createWalletUsingExtension () {
  // Set data with publicKey
  await db
    .collection("user-info")
    .record("user-1")
    .call("collectionFn", ["Awesome User"])
 
  // Add signer fn
  db.signer(async (data: string) => {
    // A permission dialog will be presented to the user
    const accounts = await eth.requestAccounts()
 
    // If there is more than one account, you may wish to ask the user which
    // account they would like to use
    const account = accounts[0]
 
    const sig = await eth.sign(data, account)
 
    return { h: "eth-personal-sign", sig }
  })
}

Creating your own wallet

To improve the user experience, you can create your own app-owned wallet (public/private key pair), allowing you to sign requests without asking the user every time. The wallet/privateKey can then be encrypted using the browser extension, and stored locally or any other storage system.

That means you only need to ask the user a single time for permission to decrypt the private key, and then use that private key to sign every subsequent request.

import { Polybase } from '@polybase/client'
import { ethPersonalSign } from '@polybase/eth'
import { secp256k1 } from '@polybase/util'
 
async function createWallet () {
  // First time the user signs up to your dapp (generate key AKA wallet)
  const privateKey = await secp256k1.generatePrivateKey()
 
  const db = new Polybase({ defaultNamespace: "your-namespace" })
 
  // Add data with publicKey that will own the record
  db.collection('your-namespace/City').record('london').call("set", [{
    name: 'London',
    country: 'UK',
  }])
 
  // Add signer fn
  db.signer(async (data: string) => {
    return { h: 'eth-personal-sign', sig: ethPersonalSign(privateKey, data) }
  })
 
  return { privateKey, publicKey: secp256k1.getPublicKey64(privateKey) }
}

Encrypt data

All data on Polybase DB is publicly accessible (like a blockchain). Therefore it is important to ensure private information is encrypted. You can encrypt data however you like, including using a user wallet's public key.

Using wallet through an extension

You can send a request to Metamask (or other compatible wallet) to obtain an encryption key which can be used to encrypt values. However, decryption is only possible by sending a secondary request to the wallet (which results in the permission popup) to ask for permission for each value to be decrypted.

This can result in a poor user experience if there are a number of different values to decrypt, as the user will have to give permission separately for each value.

Here is an example:

import { Polybase } from "@polybase/client";
import * as eth from "@polybase/eth";
 
// Init
const db = new Polybase({ defaultNamespace: "your-namespace" });
 
async function encryptUsingExtension () {
    // A permission dialog will be presented to the user
  const accounts = await eth.requestAccounts();
 
  // If there is more than one account, you may wish to ask the user which
  // account they would like to use
  const account = accounts[0];
 
  // A permission dialog will be presented to the user
  const encryptedValue = await eth.encrypt("top secret info", account);
 
  await db
    .collection("user-info")
    .record("user-1")
    .call("functionName", [encryptedValue]);
 
  // Later...
 
  // Get the data from Polybase DB as normal
  const userData = await db.collection("user-info").record("user-1").get();
 
  // Get the encrypted value
  const encryptedValueForUser = userData.data.encryptedValue;
 
  // A permission dialog will be presented to the user every time this method
  // is called
  const decryptedValue = await eth.decrypt(encryptedValueForUser, account);
}

Creating your own wallet

You can create your own app-owned wallet (public/private key) allowing you to encrypt/decrypt values without having to ask the user for explicit permission each time. The private key should be encrypted using a users existing wallet or a password, but rather than encrypting a specific value, you encrypt the new private key.

That means you only need to ask the user a single time for permission to decrypt the private key, and then use that private key to decrypt all other values. It is then your responsibility to ensure that the encrypted private key is kept safe, which could be either stored locally in browser storage or in Polybase DB.

Here is an example:

import { Polybase } from "@polybase/client";
import { secp256k1 } from "@polybase/util";
 
// Init
const db = new Polybase({ defaultNamespace: "your-namespace" });
 
async function createWallet () {
  // First time the user signs up to your dapp (generate key pair AKA wallet)
  const privateKey = secp256k1.generatePrivateKey()
  const publicKey = secp256k1.getPublicKey64(privateKey)
 
  // Encrypted value will be returned as a hex string 0x...
  const encryptedValueAsHexStr = await secp256k1.asymmetricEncryptToEncoding(
    publicKey,
    "top secret info"
  );
 
  await db
    .collection("user-info")
    .record("user-1")
    .call("set", ["Awesome User", encryptedValueAsHexStr]);
 
  // Later...
 
  // Get the data from Polybase DB as normal
  const userData = await db.collection("user-info").record("user-1").get();
 
  // Get the encrypted value
  const encryptedValue = userData.data.secretInfo;
 
  // Original value returned
  const decryptedValue = secp256k1.asymmetricDecryptFromEncoding(
    privateKey,
    encryptedValue
  );
}

There are a number of different places to store the encrypted private key that lead to different trade offs. You must find a tradeoff that is acceptable for your specific application.

Store locally

You could store the encrypted private key locally on the browser device (e.g. in local storage). The tradeoff is that the private key could easily become lost if the user resets their browser (which would make all data unavailable and there would be no recovery method), and it would be difficult for users to work across devices.

Store on Polybase DB

You could store the encrypted private key on Polybase DB, this allow the encrypted private key to obtained by the user and then decrypted on any device.

Multi User Encryption

It's often useful to allow multiple users to decrypt and view data stored in Polybase DB.

To do this, you should:

  • Create a symmetric encryption key and encrypt the data with that key
  • Encrypt the symmetric encryption key with the public key of each user who should have read access

Example

The following shows an example of how you might create a Google Forms product.

Overview

Multi User Encryption

Collection Schema

@public
collection User {
  id: string;
  publicKey: PublicKey;
 
  constructor () {
    this.id = ctx.publicKey.toHex();
    this.publicKey = ctx.publicKey;
  }
}
 
@public
collection Response {
  id: string;
  data: string;
 
  constructor (id: string, data: string) {
    this.id = id;
    this.data = data;
  }
}
 
@public
collection ResponseUser {
  id: string;
  userId: string;
  responseId: string;
  // symmetric key encrypted with users public key
  encryptedSymmetricKey: string;
 
  constructor (userId: string, responseId: string, encryptedSymmetricKey: string) {
    this.id = userId + "-" + responseId;
    this.userId = userId;
    this.responseId = responseId;
    this.encryptedSymmetricKey = encryptedSymmetricKey;
  }
}

New Form Response Code

import { Polybase } from "@polybase/client";
import { secp256k1, aescbc, encodeToString } from "@polybase/util";
 
// Init
const db = new Polybase({ defaultNamespace: "your-namespace" });
 
// Obtain form response
const data = JSON.stringify({ doYouLikePenguins: true });
 
// Upload form response
addFormResponse(data);
 
async function addFormResponse(data: string) {
  // Generate a symmetric encryption key (to encrypt form response)
  const symmetricKey = await aescbc.generateSecretKey();
  const symmetricEncryptedStr = await aescbc.symmetricEncryptToEncoding(
    symmetricKey,
    data,
    "base64"
  );
 
  // Store the encrypted response
  await db.collection("Response").create(["resp-1", symmetricEncryptedStr]);
 
  // Get list of users who will have access to the form response
  // (you will probably want to use a .where() here to filter for a subset
  //  of users)
  const users = await db.collection("Users").get();
 
  // Encrypt with each users public key and store
  const p = users.data.map(async (user) => {
    const { id: userId, publicKey } = user.data;
 
    // Encrypt the symmetric key
    const encryptedSymmetricKeyWithUsersPublicKeyStr =
      secp256k1.asymmetricEncryptToEncoding(
        publicKey, 
        encodeToString(symmetricKey, "base64"),
        "base64"
      );
 
    // Store the encrypted symmetric key
    return db
      .collection("ResponseUser")
      .create([userId, "resp-1", encryptedSymmetricKeyWithUsersPublicKeyStr]);
  });
 
  await Promise.all(p);
}

Read a response for a user who was given permission

import { Polybase } from "@polybase/client";
import { secp256k1, aescbc, decodeFromString } from "@polybase/util";
 
// Init
const db = new Polybase({ defaultNamespace: "your-namespace" });
 
async function readFormResponse(
  userId: string,
  respId: string,
 
  // If your private key is stored as a string, you can
  // use decodeFromString to convert it to Uint8Array
  userPrivateKey: Uint8Array
) {
  const response = await db.collection("Response").record(respId).get();
  const { data: encryptedData } = response.data;
 
  const responseUser = await db
    .collection("ResponseUser")
    .record(userId + "-" + respId)
    .get();
 
  const { encryptedSymmetricKey } = responseUser.data;
 
  const symmetricKey = await secp256k1.asymmetricDecryptFromEncoding(
    userPrivateKey,
    encryptedSymmetricKey,
    "base64"
  );
  const decrypted = await aescbc.symmetricDecryptFromEncoding(
    decodeFromString(symmetricKey, "base64"),
    encryptedData,
    "base64"
  );
 
  // Parse JSON string into form response object
  return JSON.parse(decrypted);
}

Polybase DB Docs