This SDK contains provides functions, types, schemas and utilities to interact with Welshare's sovereign health data sharing protocol. It includes FHIR resource handling, Nillion data storage abstractions, API clients and validation schemas.
If you're just looking for an easy way to submit data from your own frontend, the @welshare/react package might be a better fit.
Use this SDK if you want to build infrastructure to manage, authenticate and profile users yourself. The bare minimum requirement is to support a signer function that users use to deterministically derive storage keypairs from their control wallets.
The package wraps Nillion's secretvaults / nuc / blindfold libraries. Some of which have server side dependencies (pino-pretty) that can create quirky side effects when running in browsers if not deliberately treeshaken. Make sure to not expose server facing features in this library (i.e. code that makes use of Nillion "standard" collections)
We're using tshy to build ESM compatible outputs. CommonJS is not supported.
You can find compiled API docs here: https://docs.welshare.app/sdk-docs/
This Welshare barrel object export contains functions that talk to the Welshare backend infrastructure. While user data is stored in owned collections that conceptually aren't directly under our control, the application data (e.g. questionnaire definitions) is stored on Nillion collections that we have write control over.
WelshareApi:
fetchWelshareApp() - Fetches the AppRecord of a given applicationIdfetchDelegation() - users call this to request a delegation NUC that allows them to access to owned collections.submitData() - A facade that helps submitting (questionnaire) data correctly. Fetches apps and delegations in the background, interpolates fields with client side knowledge, and pushes data to Nillion nodes.submitBinaryData() - Stores encrypted binary file metadata in Nillion. See Binary File Uploads below.fetchS3WriteDelegation() - Requests a presigned S3 URL for uploading encrypted files.fetchS3ReadDelegation() - Requests a presigned S3 URL for downloading encrypted files.resolveStorageKey() - Converts welshare:// URLs to storage keys.Environment configuration:
WELSHARE_API_ENVIRONMENT - Pre-configured environments (production, staging, preview, development)resolveEnvironment() - Resolve environment configuration with optional overridesWelshareApiEnvironment - Type definition for environment configcreateJWTForStorageKeys(), verifyJWT()deriveKey() (with types Context, KeyId, UserSecret)deriveApplicationKeypair(), deriveStorageKeypair(), verifyAuthProof()Fhir):
validateQuestionnaire() - Validate FHIR Questionnaire resourcesvalidateQuestionnaireResponse() - Validate FHIR QuestionnaireResponse resourceswrapFhirResourceForNillion() - Wrap FHIR resources for Nillion insertionfhirDateRegex, fhirDateTimeRegex, fhirBase64BinaryRegex - Validation regex patternsFhirCoding, FhirCodeableConcept, FhirQuantity, FhirSimpleQuantity, FhirRange, FhirRatio, FhirPeriod, FhirAttachment, FhirSampledDataStandardCodes namespace) - FHIR standard code definitions. This is VERY helpful to refer to commonly used health conditions and observations and they frequently are found in user facing questionnaires.Nillion):
Keypair - Keypair class from @nillion/nuc (this will be replaced with a signer interface soon)makeUserClient() - Create a SecretVault UserClientsummarizeUploadResults() - Processes an array of redundant "upload" result that's returned by cluster nodes to one single resultCollectionType, NillionClusterConfig, NillionByNodeName, NillionDid, NillionUuid, BlindfoldFactoryConfigbase64urlEncode(), base64urlDecode(), base64urlDecodeUtf()bytesToHex(), hexToBytes() (re-exported from viem)makeUid()Browser-compatible AES-256-GCM encryption utilities for file handling:
generateRandomAESKey() - Generate a random 256-bit AES-GCM keyencryptFile(file, key) - Encrypt a file with an AES key, returns encrypted data and IVencodeEncryptionKey(key, iv) - Export key and IV to a storable EncryptionKey formatdecrypt(encryptedData, encryptionKey) - Decrypt data using an encoded encryption keydecodeEncryptionKey(encryptionKey) - Decode an EncryptionKey back to raw bytesEncryptionKey, Algorithm, ALGORITHMNote: Encryption utilities can be imported from the main package or from the lightweight /encryption entry point that has no Nillion dependencies:
// Option 1: Import from main package (includes all SDK features)
import { encryptFile, decrypt, type EncryptionKey } from "@welshare/sdk";
// Option 2: Import from /encryption (lightweight, no Nillion deps)
import {
encryptFile,
decrypt,
type EncryptionKey,
} from "@welshare/sdk/encryption";
All schemas are exported as Zod schemas for validation:
SessionKeyAuthMessage, AuthorizedSessionProof, SessionKeyDataInsertableSubmission, InterpolateSocials, ValidatableSchemaThe submitData() function is the primary method for submitting questionnaire response data to Welshare. It handles fetching application configuration, requesting delegations, validating data, and storing it on Nillion.
This assumes that your app helps your users to manage their keypairs, connects to wallets and signs messages. If you're looking for a more high level integration option, check out our dedicated React integration library: @welshare/react.
Before users can submit data, they need to derive a dedicated storage keypair that's used to authenticate against the Nillion nodes without disclosing the controlling key pair. Welshare uses deterministic key derivation based on signed messages from the user's wallet.
import { deriveStorageKeypair, Nillion } from "@welshare/sdk";
import { createWalletClient, custom } from "viem";
import { mainnet } from "viem/chains";
// Connect to user's wallet (e.g., MetaMask)
const walletClient = createWalletClient({
chain: mainnet,
transport: custom(window.ethereum),
});
const [userAddress] = await walletClient.getAddresses();
// Define a signer function that requests signatures from the wallet
const signMessageAsync = async (message: string): Promise<string> => {
const signature = await walletClient.signMessage({
account: userAddress,
message,
});
return signature;
};
// Derive the storage keypair deterministically
const keypair: Nillion.Keypair = await deriveStorageKeypair(
signMessageAsync,
userAddress
);
// The keypair can now be used for all Welshare operations
console.log("User DID:", keypair.toDidString());
Important Notes:
For this example to work, you must register an Application and a Questionnaire resource first. Use their ids in this example code.
import { WelshareApi, resolveEnvironment } from "@welshare/sdk";
// Using the keypair and applicationId from the previous example
const environment = resolveEnvironment("production");
// Prepare your FHIR QuestionnaireResponse
const questionnaireResponse = {
resourceType: "QuestionnaireResponse",
status: "completed",
questionnaire: "questionnaire-id",
authored: new Date().toISOString(),
item: [
{
linkId: "1",
text: "What is your age?",
answer: [
{
valueInteger: 35,
},
],
},
{
linkId: "2",
text: "Do you have any allergies?",
answer: [
{
valueBoolean: true,
},
],
},
],
};
// Submit the data
const result = await WelshareApi.submitData(
keypair,
{
collectionType: "QuestionnaireResponse",
data: questionnaireResponse,
},
environment,
applicationId
);
console.log("Submitted successfully:", result.insertedUid);
submitData() orchestrates the entire submission process:
fetchWelshareApp()fetchDelegation()The function automatically handles client-side field interpolation and ensures data is correctly formatted for storage.
The SDK provides functions to upload encrypted binary files (images, documents) to an S3 compatible storage bucket that's hosted by Welshare. Files are AES-256-GCM encrypted client-side before they're leaving the user's environment. Meta file information (mime types, sizes, file names) are stored on the BinaryFile collection on Nillion.
The symmetric encryption keys are threshold encrypted an distributed across Nillion cluster nodes; a single node cannot recover a key. When anchoring a user owned binary file document on Nillion, we're also adding an ACL entry that allows us to decipher the data on behalf of the submitting application, but technically users could withhold that allowance or withdraw it at any time.
This assumes that your app helps your users to manage their keypairs, connects to wallets and signs messages. If you're looking for a more high level integration option, check out our dedicated React integration library: @welshare/react.
The simplest way to upload a file is using uploadAndEncryptFile(), which handles the entire flow (generates a new encryption key, encrypts, uploads, and stores metadata):
import { WelshareApi } from "@welshare/sdk";
// Upload a file with one function call
const result = await WelshareApi.uploadAndEncryptFile(
keypair,
file, // File object from input or drag-drop
{
reference: `questionnaire/${questionnaireId}/photo`,
applicationId: "my-app-id",
},
"production"
);
console.log("File uploaded:", result.insertedUid);
console.log("File URL:", result.url);
// Use in a FHIR QuestionnaireResponse attachment
const attachment = {
id: result.insertedUid,
contentType: file.type,
size: file.size,
title: file.name,
url: result.url,
};
If you need more control over the upload process, you can use the lower-level functions:
import { WelshareApi, resolveEnvironment } from "@welshare/sdk";
import {
generateRandomAESKey,
encryptFile,
encodeEncryptionKey,
} from "@welshare/sdk/encryption";
const environment = resolveEnvironment("production");
// 1. Get presigned URL for S3 upload
const { presignedUrl, uploadKey } = await WelshareApi.fetchS3WriteDelegation(
keypair,
{
reference: "questionnaire/abc/photo",
fileName: file.name,
fileType: file.type,
},
environment
);
// 2. Encrypt and upload file to S3
const cryptoKey = await generateRandomAESKey();
const { encryptedData, iv } = await encryptFile(file, cryptoKey);
const encryptionKey = await encodeEncryptionKey(cryptoKey, iv);
await fetch(presignedUrl, {
method: "PUT",
body: encryptedData,
headers: { "Content-Type": file.type },
});
// 3. Store metadata on Nillion
// This call doesn't disclose the key to a single server - it's threshold-encrypted
// and distributed among cluster nodes.
const { insertedUid } = await WelshareApi.submitBinaryData(
keypair,
{
encryption_key: JSON.stringify(encryptionKey),
reference: "questionnaire/abc/photo",
file_name: file.name,
file_size: file.size,
file_type: file.type,
controller_did: keypair.toDidString(),
url: `welshare://${uploadKey}`,
},
environment,
applicationId
);
uploadAndEncryptFile(): Combines all steps below into one convenient function.fetchS3WriteDelegation() / fetchS3ReadDelegation(): Makes HTTP requests to the Welshare API (/auth/delegate/storage). Requires a valid self-signed JWT. Returns short-lived presigned URLs (15 sec expiry).submitBinaryData(): Calls fetchDelegation() internally, then writes to Nillion storage. Grants builder read/execute access to the uploaded file metadata.If you're building a React application that manages keypairs directly, you can create a custom hook using the SDK functions. Here's an example implementation:
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
Nillion,
WelshareApi,
resolveEnvironment,
type WelshareApiEnvironment,
type WelshareEnvironmentName,
} from "@welshare/sdk";
import { decrypt, type EncryptionKey } from "@welshare/sdk/encryption";
export interface UseBinaryUploadsOptions {
keypair: Nillion.Keypair | null | undefined;
environment: WelshareApiEnvironment | WelshareEnvironmentName;
}
export const useBinaryUploads = (options: UseBinaryUploadsOptions) => {
const [isRunning, setIsRunning] = useState(false);
const [error, setError] = useState<string | null>(null);
const mountedRef = useRef(true);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
}, []);
const resolvedEnvironment = useMemo(
() => resolveEnvironment(options.environment),
[options.environment]
);
const { keypair } = options;
const createUploadCredentials = useCallback(
async (payload: {
reference: string;
fileName: string;
fileType: string;
}) => {
if (!keypair) throw new Error("No keypair available");
return WelshareApi.fetchS3WriteDelegation(
keypair,
payload,
resolvedEnvironment
);
},
[keypair, resolvedEnvironment]
);
const downloadAndDecryptFile = useCallback(
async (documentId: string): Promise<File | undefined> => {
if (!keypair) throw new Error("No keypair available");
try {
setIsRunning(true);
setError(null);
const { binaryFile, data } = await WelshareApi.fetchBinaryData(
keypair,
resolvedEnvironment,
documentId
);
const encryptionKey: EncryptionKey = JSON.parse(
binaryFile.encryption_key
);
const decryptedData = await decrypt(await data, encryptionKey);
if (!decryptedData) throw new Error("Failed to decrypt file");
return new File([decryptedData], binaryFile.file_name, {
type: binaryFile.file_type,
});
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : "Failed to download/decrypt";
if (mountedRef.current) setError(errorMessage);
return undefined;
} finally {
if (mountedRef.current) setIsRunning(false);
}
},
[keypair, resolvedEnvironment]
);
return { createUploadCredentials, downloadAndDecryptFile, isRunning, error };
};
Usage:
const { createUploadCredentials, downloadAndDecryptFile } = useBinaryUploads({
keypair: storageKeyPair,
environment: "production",
});
// Get presigned URL for upload
const { presignedUrl, uploadKey } = await createUploadCredentials({
reference: "questionnaire/abc/photo",
fileName: file.name,
fileType: file.type,
});
// Download and decrypt a file
const decryptedFile = await downloadAndDecryptFile(documentId);
as an example, we're storing a subset of Fhir HL7 R5 QuestionnaireResponses. See questionnaire-response.schema.json.