Skip to main content

Delegating Data Storage

Only for reference

The code on this page is only here for reference and illustrative reasons (e.g. if you would build custom interactions with our storage layer). If you're just considering to integrate your users and data, checkout the Authentication Apps and Users docs.

Schemas

Welshare is hosting Nillion owned and standard collections on behalf of authorized applications. Our goal is to abstract away the cryptographic complexities of the underlying storage layer for applications who just want to safely store their users' information. Right now applications can only store data in collections that we're providing, but we're planning to allow them to bring their own schemas or even reuse Welshare derived keys to help their users write into schemas that they manage.

Authorization

To be able to issue Nillion delegate tokens (NUCs) for our users, we must ensure that they're legitimate controllers over their keys and actually have a plaubsible intent to write that data in the context of an application that they're using. For that, users present a self signed JWT to our delegation endpoints. Since self signed signature verification of ES256K keys inside JWTs was abandoned in early 2025, they're not trivial to create with standard libraries. Here's a code snippet showing how we get the job done nevertheless:

creating self signed jwts

import { secp256k1 } from "@noble/curves/secp256k1";
import { hexToBytes } from "@noble/hashes/utils";

/**
* Represents a secp256k1 elliptic curve key pair with secure key handling
*/
declare class NillionKeypair {
constructor(privateKey: Uint8Array);
toDidString(): DidString;
sign(msg: string, signatureFormat?: "bytes"): Uint8Array;
sign(msg: string, signatureFormat: "hex"): string;
}

function base64urlEncode(data: Uint8Array | string): string {
const bytes = typeof data === 'string'
? new TextEncoder().encode(data)
: data;

let binary = '';
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]!);
}

return btoa(binary)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}

export async function createJWTForStorageKeys(
userKeypair: NillionKeypair,
payload: Record<string, unknown>
): Promise<string> {
const header = {
alg: "ES256K",
typ: "JWT",
};

const now = Math.floor(Date.now() / 1000);

const _payload = {
iss: userKeypair.toDidString(),
iat: now,
exp: now + 3600,
nonce: Math.random().toString(36).substring(2, 15),
...payload,
};

const encodedHeader = base64urlEncode(JSON.stringify(header));
const encodedPayload = base64urlEncode(JSON.stringify(_payload));
const message = `${encodedHeader}.${encodedPayload}`;

const signatureBytes = userKeypair.sign(message, "bytes");
const encodedSignature = base64urlEncode(signatureBytes);

return `${message}.${encodedSignature}`;
}

Create a write scoped JWT for users like so:

  const selfSignedJWT = await createJWTForStorageKeys(userKeypair, {
scopes: ["write"],
});

Users present this JWT when calling welshare's delegation endpoint /api/auth/delegate, and submit their storage key did in the request's body. The delegation endpoint's response contains a delegation object.

// create a nuc for users
const delegateResponse = await fetch("/api/auth/delegate", {
method: "POST",
headers: {
Authorization: `Bearer ${selfSignedJWT}`,
},
body: JSON.stringify({
audienceDid: keypair.toDidString(),
}),
});
if (!delegateResponse.ok) {
throw new Error("Failed to delegate");
}

const { delegation, audienceDid } = await delegateResponse.json();

That delegation allows users to write (and read) on Welshare's Nillion collections directly, e.g. using the secretvaults SDK.


//that's the builder who should be able to read the user's data
//const builderDid = `did:nil:...`;

//to grant access to the HPMP, for now you grant access to welshare:
const builderDid = `did:nil:027a3fcce7f7b12061bb7d872d685b7cbcab838d4e74036ca394f504ea89169a9d`;

//the nodes to store data on
const nillionNodes = "https://nildb-stg-n1.nillion.network"

const userClient = await SecretVaultUserClient.from({
baseUrls: nillionNodes,
keypair: userKeypair,
});


const uploadResults = await userClient.createData(delegation, {
collection: schemaId,
owner: audienceDid,
data: <your-data>,
acl: {
grantee: builderDid, // Grant access to the application
read: true, // app can read the data
write: false, // app cannot modify the data
execute: true, // app can run queries on the data
},
});

Storing data on Nillion using delegated welshare NUCs

Applications use Welshare to Access Owned Collections

TBD: Applications Access Owned Collections Directly