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:
Option A: React Applications (Recommended)
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 } = 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}`,
},
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.
DocumentReference Metadata
Binary file uploads are standalone entities, stripped off metadata and mostly meaningless without context. When submitting binary data attachments inlined in questionnaire response submissions, we automatically create a DocumentReference record that provides searchable FHIR-compliant metadata for the file. This enables:
- Discoverability: Find files by type, category, or description
- FHIR compliance: Standard resource linking to Binary content
- Provenance: Track who created/authored the document
How It Works
Binary Upload
│
▼
┌─────────────────────────────────────┐
│ 1. Binary record created │
│ - Encryption key (secret) │
│ - File metadata │
│ - welshare:// URL │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 2. DocumentReference created │
│ - Links via "binary/{id}" │
│ - Type, category, description │
│ - Subject & author references │
└─────────────────────────────────────┘
Author Attribution
The value of the author field depends on the upload context:
| Upload Source | Author |
|---|---|
| User profile page (direct uploads) | Patient (the uploader themselves) |
| External frame (questionnaire attachments) | Application (applicationId) |
Using DocumentReference in SDK
When using the SDK directly, you can create DocumentReferences with additional metadata:
import {
WelshareApi,
DocumentReferenceHelpers,
DOCUMENT_TYPE_CONCEPTS,
DOCUMENT_CATEGORY_CONCEPTS,
Keypair
} from "@welshare/sdk";
// After uploading a binary file
const uploadResult = await WelshareApi.uploadAndEncryptFile(
keypair,
file,
{ reference: "user-upload", applicationId },
environment
);
// Create DocumentReference with rich metadata
const docRef = DocumentReferenceHelpers.createDocumentReference({
binaryId: uploadResult.insertedUid,
fileName: file.name,
fileType: file.type,
fileSize: file.size,
subjectDid: Keypair.keypairKeyDid(keypair).didString;,
// author defaults to subject if not provided
description: "My medical document",
type: DOCUMENT_TYPE_CONCEPTS.CLINICAL_NOTE,
category: [DOCUMENT_CATEGORY_CONCEPTS.CLINICAL],
});
// Store the DocumentReference
const { insertedUid: docRefUid } = await WelshareApi.submitDocumentReference(
keypair,
docRef,
environment
);
Available Document Types
| Constant | LOINC Code | Use For |
|---|---|---|
DOCUMENT_TYPE_CONCEPTS.PHOTO_DOCUMENT | 72170-4 | Photos, images |
DOCUMENT_TYPE_CONCEPTS.ADDENDUM_DOCUMENT | 55107-7 | General files, PDFs |
DOCUMENT_TYPE_CONCEPTS.CONSENT_DOCUMENT | 59284-0 | Consent forms |
DOCUMENT_TYPE_CONCEPTS.CLINICAL_NOTE | 34109-9 | Clinical notes |
DOCUMENT_TYPE_CONCEPTS.LAB_REPORT | 11502-2 | Lab results |
DOCUMENT_TYPE_CONCEPTS.IMAGING_REPORT | 18748-4 | Imaging/radiology |
Backward Compatibility
DocumentReference creation is transparent and does not affect existing integrations:
- QuestionnaireResponse
valueAttachmentstill uses the Binary ID - Applications using
@welshare/reactdon't need changes - The
BINARY_DATA_SUBMITTEDresponse optionally includesdocumentReferenceUid
Reference
Binary Files Collection Schema UID: 9d696baf-483f-4cc0-b748-23a22c1705f5
DocumentReference Collection Schema UID: fe9903c4-199c-449b-acad-c0458d95b262
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 (transitively pulls in 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
| Endpoint | Purpose |
|---|---|
POST /auth/delegate/storage?requestType=write | Get upload presigned URL |
POST /auth/delegate/storage?requestType=read | Get download presigned URL |
POST /auth/delegate/storage?requestType=delete | Delete file |
Related
- Questionnaires - Creating questionnaires
- Authentication - Session keys and JWT
- SDK API Docs - Full SDK reference