Datasets on S3
A dataset on S3 uses the same calls as a dataset on any other storage kind. In the examples, Example Health’s imaging dataset holds files on S3.
Where S3 appears
S3 appears in one place: the dataset’s storageBinding, which an Owner or Admin registers once. Only the Seald Healthcare Cloud holds the binding and its credentials: an AWS role scoped to the bucket and prefix in the binding. A device never holds them. After registration, the application saves, opens, shares and deletes as for any other dataset. It never sees S3 again.
For a save or an open, the SDK:
- asks the Seald Healthcare Cloud for the object’s encryption details
- asks for temporary access to that one object in the bucket, valid for five minutes
- uploads or downloads the object itself, with that access
A share, an unshare, a hold or a retire changes only the Seald Healthcare Cloud. None of them touches an object in the bucket. A bucket that does not answer causes the error storage-unreachable, which is retryable.
Operations
| Operation | The application | The SDK | The bucket |
|---|---|---|---|
| Register | datasets.register with the bucket, prefix, region and role | Sends the registration. When this device’s person is the first domain owner, creates the domain key and makes it recoverable by break-glass recovery | Nothing yet |
| List | folders.list(root) | Makes one decision, then uses temporary read access to fetch and decrypt the folder index | Serves the folder index |
| Save | objects.create, or objects.save for the next version | Makes one decision, generates fresh keys, uploads the encrypted object using temporary write access, then updates the folder index and finishes the save | A new object under seald/<locator>/<version>, beside the earlier versions. A new folder index |
| Open | objects.open({ locator, version? }) | Makes one decision, uses temporary read access, verifies the whole object, and decrypts it in memory. close() discards the decrypted content | Serves the stored object. An earlier version is its own object |
| Search | folders.search | Opens every folder index in scope, one decision each, and searches them on the device | Serves the folder indexes. The bucket runs no search |
| Share, unshare | domains.share, domains.unshare | Verifies each device certificate (card). Then gives every device of the person access to the domain key, or removes their access and starts a new epoch | Nothing. No object changes, whatever the key domain’s size |
| One record | domains.shareRecord | Gives each of the person’s devices access to every version of the record | Nothing |
| Sub-domain | domains.create(folder) on an empty folder | Creates a domain key for the folder subtree and makes it recoverable by break-glass recovery | A folder index of its own under the folder’s prefix |
| Retire, restore | objects.delete, domains.restore | One decision. The SDK marks or unmarks the entry in the folder index | Nothing. The object stays. The Seald Healthcare Cloud refuses every fetch of it |
| Shred | domains.shred | One decision. The Seald Healthcare Cloud destroys the object’s encryption details, then deletes the object from the bucket | The object is deleted. The bucket’s own copies open nothing |
| Hold | holds.place, holds.lift | One decision. A retire or shred under it is a deny | Nothing |
| Offline lease | Nothing. Opens work as usual | When the offline lease is granted, fetches every object it covers and locks them to the device | Serves them when the offline lease is granted. Nothing during an outage |
| Export | evidence.export({ datasets }) | Sends the manifest over the pack | Nothing. A pack holds locators, which are the bucket’s keys, never a name |
| A save that fails after the upload | Nothing | Nothing. No version exists | An object nobody can open. The Seald Healthcare Cloud deletes it 24 hours after the temporary access expires |
Register, save, open
The samples use the allowed helper from Access decisions.
const sixYears = 6 * 365 * 24 * 60 * 60;await allowed(await client.datasets.register({ name: 'imaging', classification: 'phi', storage: { kind: 'object-storage' }, storageBinding: { s3: { bucket: 'example-health-imaging', prefix: 'sealdhealthcare/', region: 'us-east-1', role: 'arn:aws:iam::123456789012:role/sealdhealthcare-cloud' } }, indexFields: ['modality', 'study_date'], segmentFields: [], retentionFloor: sixYears, firstOwner: alice,}));
// from here on, the dataset's root is a folder like any otherconst imaging = (await client.datasets.list()).find((d) => d.name === 'imaging')!;const listing = await allowed(await client.folders.list(imaging.root));if (listing) render(listing.entries);
const saved = await client.objects.create( imaging.root, { kind: 'file', name: 'study-4471.dcm', type: 'application/dicom', bytes: fileStream, size }, { subject: medicalRecordNumber },);if ('outcome' in saved) render(await allowed(saved)); // the SDK uploaded the encrypted file with temporary access
const opened = await allowed(await client.objects.open({ locator }));if (opened?.kind === 'file') renderFile(opened.name, opened.stream()); // the SDK downloaded, verified and decrypted the file in memoryopened?.close();let sixYears: TimeInterval = 6 * 365 * 24 * 60 * 60_ = try await allowed(client.datasets.register(DatasetRegistration( name: "imaging", classification: .phi, storage: Dataset.Storage(kind: .objectStorage), indexFields: ["modality", "study_date"], segmentFields: [], retentionFloor: sixYears, storageBinding: .s3(S3Binding(bucket: "example-health-imaging", prefix: "sealdhealthcare/", region: "us-east-1", role: "arn:aws:iam::123456789012:role/sealdhealthcare-cloud")), firstOwner: alice)))
let imaging = try await client.datasets.list().first { $0.name == "imaging" }!let listing = try await allowed(client.folders.list(imaging.root))if let listing { render(listing.entries) }
let saved = try await client.objects.create(imaging.root, content: .file(FileContent(name: "study-4471.dcm", type: "application/dicom", bytes: fileStream, size: size)), subject: medicalRecordNumber)if case .decided(let decided) = saved { render(try await allowed(decided)) }
let opened = try await allowed(client.objects.open(ObjectRef(locator: locator)))if case .file(let file) = opened { renderFile(file.name, file.stream()) }opened?.close()val sixYears = (6 * 365).daysallowed(client.datasets.register(DatasetRegistration( name = "imaging", classification = Classification.PHI, storage = Dataset.Storage(kind = Dataset.Kind.OBJECT_STORAGE), indexFields = listOf("modality", "study_date"), segmentFields = emptyList(), retentionFloor = sixYears, storageBinding = StorageBinding.S3(S3Binding(bucket = "example-health-imaging", prefix = "seald/", region = "us-east-1", role = "arn:aws:iam::123456789012:role/sealdhealthcare-cloud")), firstOwner = alice)))
val imaging = client.datasets.list().first { it.name == "imaging" }val listing = allowed(client.folders.list(imaging.root))listing?.let { render(it.entries) }
val saved = client.objects.create(imaging.root, content = FileContent(name = "study-4471.dcm", type = "application/dicom", bytes = fileStream, size = size), subject = medicalRecordNumber)if (saved is SaveResult.Decided) render(allowed(saved.decided))
val opened = allowed(client.objects.open(ObjectRef(locator)))if (opened is Opened.File) renderFile(opened.name, opened.file.stream())opened?.close()Share and unshare
A share changes only the Seald Healthcare Cloud’s record of who can access the domain key. No object in the bucket changes. A share costs the same for ten studies or ten million.
await allowed(await client.domains.share(imaging.root, { person: alice })); // gives every device of alice access, for every epochawait allowed(await client.domains.share(imaging.root, { group: 'radiologists' })); // a standing share. Member devices give later joiners access.await allowed(await client.domains.unshare(imaging.root, { person: alice })); // removes access at once and starts a new epoch. The person keeps earlier studies.await allowed(await client.domains.shareRecord(locator, bob)); // gives access to one study, version by version, with no listingawait allowed(await client.domains.create(folder, { members: [{ group: 'oncology' }] })); // an empty folder becomes a key domain of its own_ = try await allowed(client.domains.share(imaging.root, to: .person(alice)))_ = try await allowed(client.domains.share(imaging.root, to: .group("radiologists")))_ = try await allowed(client.domains.unshare(imaging.root, from: .person(alice)))_ = try await allowed(client.domains.shareRecord(locator, to: bob))_ = try await allowed(client.domains.create(folder, members: [.group("oncology")]))allowed(client.domains.share(imaging.root, ShareTarget.ToPerson(alice)))allowed(client.domains.share(imaging.root, ShareTarget.ToGroup("radiologists")))allowed(client.domains.unshare(imaging.root, ShareTarget.ToPerson(alice)))allowed(client.domains.shareRecord(locator, bob))allowed(client.domains.create(folder, members = listOf(ShareTarget.ToGroup("oncology"))))Versions and search
Every save is a new object beside the old one. Seald Healthcare keeps the versions itself. The bucket’s own versioning is separate from Seald Healthcare’s. An older stored object put back in the bucket fails the folder index’s freshness check.
const next = await client.objects.save( locator, { kind: 'file', name: 'study-4471.dcm', type: 'application/dicom', bytes: fileStream, size }, { baseVersion: 1 },);const earlier = await allowed(await client.objects.open({ locator, version: 1 })); // its own object, open to whoever could open itconst found = await client.folders.search({ indexFields: { modality: 'MR' }, savedFrom: from }, { datasets: ['imaging'] });render(found.entries); // locators only. The bucket runs no search.let next = try await client.objects.save(locator, content: .file(FileContent(name: "study-4471.dcm", type: "application/dicom", bytes: fileStream, size: size)), baseVersion: 1)let earlier = try await allowed(client.objects.open(ObjectRef(locator: locator, version: 1)))let found = try await client.folders.search(SearchQuery(indexFields: ["modality": "MR"], savedFrom: from), datasets: ["imaging"])render(found.entries)val next = client.objects.save(locator, content = FileContent(name = "study-4471.dcm", type = "application/dicom", bytes = fileStream, size = size), baseVersion = 1)val earlier = allowed(client.objects.open(ObjectRef(locator, version = 1)))val found = client.folders.search(SearchQuery(indexFields = mapOf("modality" to "MR"), savedFrom = from), datasets = listOf("imaging"))render(found.entries)Delete, restore, shred, hold
await allowed(await client.objects.delete({ locator, version: 1 })); // retires version 1. The Seald Healthcare Cloud refuses later fetches. The object stays in the bucket.const bin = await client.domains.recycleBin(imaging.root);await allowed(await client.domains.restore(bin[0]));await allowed(await client.domains.shred(bin.filter((item) => !item.held))); // destroys the encryption details, then deletes the object from the bucket
await allowed(await client.holds.place({ dataset: 'imaging' }, 'Audit', 'AUD-2026-03')); // refuses every delete in the dataset until two Owners lift the hold_ = try await allowed(client.objects.delete(ObjectRef(locator: locator, version: 1)))let bin = try await client.domains.recycleBin(imaging.root)_ = try await allowed(client.domains.restore(bin[0]))_ = try await allowed(client.domains.shred(bin.filter { !$0.held }))
_ = try await allowed(client.holds.place(.dataset("imaging"), reason: "Audit", reference: "AUD-2026-03"))allowed(client.objects.delete(ObjectRef(locator, version = 1)))val bin = client.domains.recycleBin(imaging.root)allowed(client.domains.restore(bin[0]))allowed(client.domains.shred(bin.filter { !it.held }))
allowed(client.holds.place(HoldTarget.Dataset("imaging"), "Audit", "AUD-2026-03"))Offline and evidence
client.on('reachable', ({ reachable }) => banner(reachable ? undefined : 'Offline: the leased worklist opens, nothing else'), // the SDK fetched the covered objects when the offline lease started);
const pack = await allowed(await client.evidence.export({ datasets: ['imaging'] }, { from, to })); // every access event on the dataset, by locatorfor await event in client.on(.reachable) { banner(event.reachable ? nil : "Offline: the leased worklist opens, nothing else")}
let pack = try await allowed(client.evidence.export(.datasets(["imaging"]), from: from, to: to))client.on<ClientEvent.Reachable>().collect { event -> banner(if (event.reachable) null else "Offline: the leased worklist opens, nothing else")}
val pack = allowed(client.evidence.export(ExportScope.Datasets(listOf("imaging")), from = from, to = to))What the bucket holds
This is what anyone who browses the bucket sees, including the storage provider.
| Key | Holds | Never |
|---|---|---|
seald/<locator>/<version> | The encrypted object. A small unencrypted part identifies which key it needs. The name, type and times are inside the encrypted part, not visible in the bucket | A name, a key, a hash of the content, the customer’s own id |
| The same, for a folder index | The key domain’s listing, a stored object like any other. The SDK rewrites it on every save | A plaintext listing |
| The bucket’s own versions and copies | Stored objects with no key anywhere. Once a shred destroys the encryption details, they stay unreadable for good | A way back after a shred |
The customer’s bucket settings
| Setting | With Seald Healthcare |
|---|---|
| Replication, the customer’s own backups | Fine. Every copy is ciphertext. A copy without its encryption details opens nothing |
| Object versioning | Separate from Seald Healthcare’s versioning. Seald Healthcare keeps every version as its own object. A rollback fails the freshness check |
| A lifecycle rule that deletes objects | A loss Seald Healthcare cannot undo, never a disclosure |
| Access | The role the Seald Healthcare Cloud assumes, for the prefix alone. Nothing else in Seald Healthcare reaches the bucket. A person who browses it sees opaque keys |