Skip to content

Getting Started

The Zapp SDK lets you build JavaScript apps which connect to Zupass, giving you access to programmable cryptography tools which enable secure and private interaction with the user’s personal data.

In this guide we’ll look at how to get started as an app developer, connect to Zupass, and interact with the user’s data store.

Installation

To get started with the Zapp SDK, you will need to install the @parcnet-js/app-connector package, using your preferred package manager:

npm i @parcnet-js/app-connector

Import the connector

Next, import the connector package in your application code:

src/main.ts
import { connect } from "@parcnet-js/app-connector";

Connect to Zupass

To connect, you will need to define some data about your Zapp, select a host HTML element, and choose a Zupass URL to connect to.

Defining your Zapp

To define your Zapp:

src/main.ts
import { connect, Zapp } from "@parcnet-js/app-connector";
const myZapp: Zapp = {
name: "My Zapp Name",
permissions: {
REQUEST_PROOF: { collections: ["Apples", "Bananas"] },
SIGN_POD: {},
READ_POD: { collections: ["Apples", "Bananas"] },
INSERT_POD: { collections: ["Apples", "Bananas"] },
DELETE_POD: { collections: ["Bananas"] },
READ_PUBLIC_IDENTIFIERS: {}
}
}

Try to give your Zapp a simple, memorable name that is clearly tied to its branding or domain name.

Permissions

When connecting for the first time, your Zapp will request permissions from Zupass. These permissions determine what the Zapp will be allowed to do with user data:

PermissionEffectsParameters
READ_PUBLIC_IDENTIFIERSThis permission allows your Zapp to read the user’s public key and Semaphore commitments.

None.

REQUEST_PROOFThis permission allows your Zapp to request zero-knowledge proofs from the user.

collections: The names of the collections that your Zapp requires access to.

SIGN_PODThis permission allows your Zapp to request the signing of a POD with the user’s identity.

None.

READ_PODThis permission allows your Zapp to read any POD from the specified collections.

collections: The names of the collections that your Zapp requires access to.

INSERT_PODThis permission allows your Zapp to insert PODs to the specified collections.

collections: The names of the collections that your Zapp requires access to.

DELETE_PODThis permission allows your Zapp to delete any POD from the specified collections.

collections: The names of the collections that your Zapp requires access to.

“Collections” are distinct groups of PODs, and allow the user to grant different levels of access to different Zapps.

Select an HTML element

The app connector works by inserting an <iframe> element into the web page, and uses this to exchange messages with Zupass. To avoid interfering with other elements on your page, you should add an element that the app connector can use. For example, your HTML might look something like this:

public/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My First Zapp</title>
</head>
<body>
<div id="app-connector">
<!-- This element will be used by the app connector -->
</div>
<div id="app">
<!-- Your HTML or mounted components go here -->
</div>
<script type="module" src="/src/app.ts"></script>
</body>
</html>

Here we’re using an element with the ID app-connector to host the iframe. It’s important that your application does not change this element once the app connector has started.

Choose a Zupass URL

Finally, you must choose the Zupass URL you want to connect to. The client is identified by the URL it’s hosted on. For Zupass production, the URL is https://zupass.org, but you might be running a local development version on something like http://localhost:3000. Or you might be running another client altogether, in which case use the URL that it’s hosted at.

Putting it all together

Now we’re ready to connect:

src/main.ts
import { connect, Zapp } from "@parcnet-js/app-connector";
// The details of our Zapp
const myZapp: Zapp = {
name: "My Zapp Name",
permissions: {
READ_PUBLIC_IDENTIFIERS: {}
}
}
// The HTML element to host the <iframe>
const element = document.getElementById("parcnet-app-connector") as HTMLElement;
// The URL to Zupass
const clientUrl = "http://localhost:3000";
// Connect!
const z = await connect(myZapp, element, clientUrl);

The resulting z variable will contain an API wrapper. The API reference for this is here.

Creating and reading data

User data is stored in POD format. This is a cryptography-friendly data format which enables the creation of proofs about the structure, content, and provenance of individual data objects, or groups of data objects.

Signing PODs

New PODs are created by signing a data object. This is straightforward:

src/main.ts
const data: PODEntries = {
greeting: { type: "string", value: "Hello, world" },
magic_number: { type: "int", value: 1337n },
email_address: { type: "string", value: "test@example.com" }
}
const signedPOD = await z.pod.sign(data);
// Will print the entries we defined above:
console.log(signedPOD.content.asEntries());
// Will print the unique signature of the POD:
console.log(signedPOD.signature);

The result of the sign method is a POD data object. Internally, this stores the data entries as a Merkle tree, but it’s not necessary to understand how this works.

The POD’s unique signature is the signed hash of the Merkle root, but you can treat it as an opaque identifier. The signature is created using a private key belonging to the user, which is managed by Zupass - your app does not have direct access to the private key.

Inserting PODs to the data store

Once you have a POD, you can insert it to the data store:

src/main.ts
await z.pod.collection("CollectionName").insert(signedPOD);

Zupass is responsible for saving the data and synchronizing it between devices, so your app doesn’t need to worry about how this data is persisted.

The collection name should be one of the collections that your Zapp requested permission to insert PODs to. Attempting to insert to a collection without permissions will fail.

In the above example, we’re inserting the POD that we created by requesting a signature from the client, but your app could also have a back-end service which signs PODs using your own private key. Those PODs can be inserted using the insert API. For example:

src/main.ts
// Fetch the serialized POD from the server
const response = await fetch('https://your-api.com/get-pod');
const serializedPOD = await response.text();
// Deserialize the POD
const pod = POD.deserialize(serializedPOD);
// Now you can insert the POD into the data store
await z.pod.collection("CollectionName").insert(pod);

The details of your backend may be different, but this approach works for PODs produced by any external source.

Deleting PODs

Your app can also delete PODs for which it knows the signature:

src/main.ts
await z.pod.collection("CollectionName").delete(signature);

This tells the client to drop the POD from the user’s data store.

Querying for PODs

To read PODs from the data store, you can define queries which describe certain criteria for PODs:

src/main.ts
import * as p from "@parcnet-js/podspec";
const query = p.pod({
entries: {
greeting: { type: "string" },
magic_number: { type: "int" }
}
});
const queryResult = await z.pod.collection("CollectionName").query();

This will return any PODs which have entries matching the description. The result is an array of POD objects, which will be empty if no matching results are found.

Queries can contain more advanced constraints:

src/main.ts
import * as p from "@parcnet-js/podspec";
const validGreetings = [
{ type: "string", value: "Hello, world" },
{ type: "string", value: "Ahoy there!" },
{ type: "string", value: "Good morning, starshine. The Earth says hello!" }
];
const validNumberRange = { min: 1000n, max: 1500n };
const query = p.pod({
entries: {
greeting: { type: "string", isMemberOf: validGreetings },
magic_number: { type: "int", inRange: validNumberRange }
}
});
const queryResult = await z.pod.collection("CollectionName").query(query);

This query provides a list of valid strings for the greeting entry, and a range of valid numbers for the magic_number entry. You can use these features to construct very specific queries for specific PODs.

Subscribing to queries

TODO

Making and verifying proofs about data

PODs are designed to make it easy to create cryptographic proofs about their structure, content, and provenance. These proofs are created using General Purpose Circuits, or GPCs.

GPCs allow you to describe aspects of a POD, and make proofs about that POD without revealing all of the underlying data. For example, you could prove that you have a POD signed by a particular public key without revealing the POD’s content, or prove that you have a POD with a particular entry whose value lies in a certain range or belongs to a list, without revealing the exact value.

Making proofs

To create a proof, you must create a proof request. The proof request contains information about what you would like to prove, and the user can decide whether to allow the proof. Here’s an example:

src/main.ts
const validGreetings = [
{ type: "string", value: "Hello, world" },
{ type: "string", value: "Ahoy there!" },
{ type: "string", value: "Good morning, starshine. The Earth says hello!" }
];
const validNumberRange = { min: 1000n, max: 1500n };
const result = await z.gpc.prove({
request: {
pods: {
pod1: {
pod: {
entries: {
greeting: { type: "string", isMemberOf: validGreetings },
magic_number: { type: "int", inRange: validNumberRange },
email_address: { type: "string" }
}
}
},
{
revealed: { email_address: true }
}
}
}
});

This proof will require a POD with a valid greeting and magic_number, and will also reveal the value of the email_address entry.

The result has a field success which is set to true if the proof was created successfully.

If success is false then the result also contains an error field containing an error message.

If success is true then the result has a further three fields:

  • proof, which contains the cryptographic proof for later verification
  • boundConfig, which contains the proof configuration
  • revealedClaims, which contains the “claims” about the data which can be verified with reference to the proof

In the example above, the revealedClaims field would contain something like this:

{
pods: {
pod1: {
entries: {
email_address: {
type: "string",
value: "test@example.com"
}
}
}
},
membershipLists: {
{ type: "string", value: "Hello, world" },
{ type: "string", value: "Ahoy there!" },
{ type: "string", value: "Good morning, starshine. The Earth says hello!" }
}
}

The membershipLists contains information about the list membership rules that apply to some entries, and contains information that was part of the input. However, the pods section contains details about the specific POD that the user selected, which was not part of your input.

Verifying proofs

TODO

User identity

Users have identity credentials, which their clients help them to manage. You can request certain public information about the user’s identity using the API.

Semaphore v3 commitment

The user’s Semaphore v3 commitment can be retrieved like this:

src/main.ts
const commitment = await z.identity.getSemaphoreV3Commitment();

The commitment is a bigint and uniquely identifies the user.

Semaphore v4 commitment

The user’s Semaphore v4 commitment can be retrieved like this:

src/main.ts
const commitment = await z.identity.getSemaphoreV4Commitment();

The commitment is a bigint and uniquely identifies the user.

Public key

The user’s Semaphore v4 public key can be retrieved like this:

src/main.ts
const commitment = await z.identity.getPublicKey();

The public key is a string and uniquely identifies the user.