Skip to main content

Binary File Uploads

Upload encrypted binary files (images, documents) to Welshare. Files are AES-256-GCM encrypted client-side before being uploaded to the storage backend (S3 compatible). Metadata is stored on Nillion.

Installation

Choose your integration approach:

This approach manages user accounts and upload credentials inside a browser frame running on an official welshare domain. Your application does not have to care about any cryptography at all. You just provide data upload interfaces. Your application and the frame communicate with postMessage commands.

npm install @welshare/react
# or
pnpm add @welshare/react

Requires React 19+ as a dependency

Option B: SDK Only (Node.js / Frontends managing wallets / signers on their own)

If you're managing wallet connections and user keys inside your own app, the SDK is right for you. You identify users and manage their wallet connections, data is encrypted on your app and you call the welshare delegation and storage endpoints directly.

npm install @welshare/sdk
# or
pnpm add @welshare/sdk

Configuration

Register your application at wallet.welshare.app/application to get an applicationId.

import { resolveEnvironment } from "@welshare/sdk";

// Use a named environment
const welshareEnvironment = resolveEnvironment("production"); // or "staging", complete custom nillion configs *are* possible.

Submission Flows

Option A: using the external wallet popup dialog (useWelshare)

import { useWelshare } from "@welshare/react";

function PhotoUpload({ questionnaireId }) {
const { uploadFile, isConnected, openWallet } = useWelshare({
applicationId: "your-app-id",
environment: welshareEnvironment,
callbacks: {
onFileUploaded: (uid, url) => console.log("Uploaded:", uid),
onError: (error) => console.error(error),
},
});

const handleUpload = async (file: File) => {
if (!isConnected) {
openWallet();
return;
}

const { url, binaryFileUid } = await uploadFile(
file,
`questionnaire/${questionnaireId}/photo`
);

// Use in QuestionnaireResponse
return {
valueAttachment: {
id: binaryFileUid,
url,
contentType: file.type,
size: file.size,
title: file.name,
},
};
};

return (
<input type="file" onChange={(e) => handleUpload(e.target.files[0])} />
);
}

Option B: Direct Keypair Access (useBinaryUploads + SDK)

import { useBinaryUploads, encryptAndUploadFile } from "@welshare/react";
import { WelshareApi, resolveEnvironment } from "@welshare/sdk";
// Or use lightweight encryption-only import:
// import { ... } from "@welshare/sdk/encryption";

/// @param applicationId: you can register applications on {staging}.wallet.welshare.app/application
function useDirectUpload(keypair: Keypair, applicationId: string) {
const { createUploadCredentials, downloadAndDecryptFile, isRunning, error } =
useBinaryUploads({
keypair,
environment: welshareEnvironment,
});

/// reference can point to another document on the welshare space that this binary file is "attached" to or related with.
/// References usually contain a document type or a context in which they are used, eg `questionnaire/{uuid}/facial-photo`
const uploadFile = async (file: File, reference: string) => {
// 1. Get presigned S3 URL
const { presignedUrl, uploadKey } = await createUploadCredentials({
applicationId,
reference,
fileName: file.name,
fileType: file.type,
});

// 2. Encrypt and upload to storage backend / S3
const { encryptionKey } = await encryptAndUploadFile(file, presignedUrl);

// 3. Store metadata and keys on Nillion (keys are encrypted across nodes)
const { insertedUid, errors } = await WelshareApi.submitBinaryData(
keypair,
{
encryption_key: JSON.stringify(encryptionKey),
reference,
file_name: file.name,
file_size: file.size,
file_type: file.type,
controller_did: keypair.toDidString(),
// when prefixed with "welshare://" this resolves to this gets resolved to urls of welshare's storage backend
url: `welshare://${uploadKey}`,
},
environment: welshareEnvironment,
applicationId
);

return { insertedUid, url: `welshare://${uploadKey}` };
};

return { uploadFile, downloadAndDecryptFile, isRunning, error };
}

Downloading Files

Option A

As you don't control your users' keys in this option, you can't use storage keypairs that are required to authenticate your users' requests.

Option B

import { WelshareApi } from "@welshare/sdk";
import { decrypt } from "@welshare/sdk/encryption";

// 1. Read binary file entry from Nillion (contains the decrypted key) & fetch data as a Promise<ArrayBuffer>
const { binaryFile, data } = await WelshareApi.fetchBinaryData(
keypair,
environment,
binaryFileUid
);

// 2. Download and decrypt
const encryptedData = await data; // this downloads the data
const decryptedData = await decrypt(
encryptedData,
JSON.parse(binaryFile.encryption_key)
);

Downloading Files as Application

tbd . Right now this requires signing off an application control key jwt to authenticatte with a welshare API that grants access to user records after verifying access control. This flow will be published very soon.

Integration with Questionnaires

Uploaded binary files can be part of FHIR QuestionnaireResponses's valueAttachment items:

const questionnaireResponse = {
resourceType: "QuestionnaireResponse",
questionnaire: questionnaireId,
status: "completed",
item: [
{
linkId: "photo-upload",
answer: [
{
valueAttachment: {
id: <binaryFileUid>, // From upload
url: <uploadedUrl>, // welshare://... URL
contentType: "image/jpeg",
size: 102400,
title: "profile-photo.jpg",
},
},
],
},
],
};

Use QuestionnaireResponseSchema.findValueAttachments(response.item) to extract all attachments from a response.

Reference

Binary Files Collection Schema UID: 9d696baf-483f-4cc0-b748-23a22c1705f5

Encryption

  • Algorithm: AES-256-GCM
  • Key size: 256 bits
  • IV size: 96 bits (12 bytes)

Keys are stored with Nillion's %allot modifier for secret sharing.

Import Options:

Encryption utilities can be imported from the main SDK package or from a lightweight entry point:

// Full SDK (includes Nillion dependencies)
import { encryptFile, decrypt, type EncryptionKey } from "@welshare/sdk";

// Lightweight encryption-only (no Nillion dependencies)
import { encryptFile, decrypt, type EncryptionKey } from "@welshare/sdk/encryption";

Use the /encryption import when you only need file encryption/decryption without other SDK features to reduce bundle size.

Presigned URLs

  • Expiry: 15 seconds
  • Format: {network}/user-uploads/{did}/{timestamp}-{fileName}

API Endpoints

EndpointPurpose
POST /auth/delegate/storage?requestType=writeGet upload presigned URL
POST /auth/delegate/storage?requestType=readGet download presigned URL
POST /auth/delegate/storage?requestType=deleteDelete file