01-infrastructurewave: W1filled30 citations

MinIO — Object Storage Infrastructure

Audiences: developer, internal

MinIO — Object Storage Infrastructure

MinIO is the S3-compatible object store that holds every binary the platform produces — patient documents, attachments, audio recordings, thumbnails — partitioned into one bucket per clinic so a tenant's binary data sits in an isolated namespace that the application code cannot accidentally cross.

Business Purpose

Healthcare platforms accumulate large binaries fast: scanned referrals, lab PDFs, intake photos, ultrasound clips, voice-consultation recordings, signed consent forms. Putting these in PostgreSQL is unworkable (cost, backup time, replication lag); putting them in cloud S3 is unacceptable for German on-prem clinics. MinIO runs on the clinic's own hardware, speaks S3, and gives Dudoxx HMS one bucket per tenant so a customer's data is physically scoped to a known namespace. The "one bucket per clinic" rule is also a compliance lever — a clinic can be exported, deleted, or migrated by manipulating one bucket, not by trawling a shared store with row-level filters.

Audiences

  • Investor: S3-compatible on-prem storage, tenant-scoped buckets, dynamic provisioning during tenant onboarding — this is how Dudoxx HMS sells "your data, your hardware" without giving up the SDK ecosystem and DX of S3.
  • Clinical buyer: Every uploaded document, photo, or recording is stored only in your clinic's bucket. Tenant bucket isolation is a structural promise, not a configuration toggle.
  • Developer/partner: NestJS owns the only path to MinIO — never call MinIO directly from Next.js. StorageService (ddx-api/src/platform/storage/storage.service.ts) is the canonical seam; bucket naming is {clinic-slug}-{type} (e.g. ddx-hamburg-clinic-documents); type is resolved by getBucketTypeForAttachment() in storage/utils.ts:126.
  • Internal (ops/support): One MinIO container (:6000 API (MINIO_PORT), :6001 console (MINIO_CONSOLE_PORT)), one named volume. System buckets created at first boot; tenant buckets created on POST /super-admin/tenants. SSE-S3 encryption is enabled at the bucket level; versioning is enabled on system buckets.

Architecture

On the ddx-001-network:

ContainerSurfaceBinding
ddx-001-minio-1 (minio/minio:latest)API:6000 (host, MINIO_PORT) → container :9000
ddx-001-minio-1Console:6001 (host, MINIO_CONSOLE_PORT) → container :9001
ddx-001-minio-1Volumeddx-001-minio-data-1/data
ddx-001-minio-1-init (mc, one-shot)bootstrapcreates system buckets clinic-default | clinic-recordings | clinic-temp; service account ddx_api_user; policy all-clinics-rw

Tenant buckets are created DYNAMICALLY by the NestJS TenantService on POST /super-admin/tenants — never by ops, never by the seeder directly.

NestJS (ddx-api :6100) is the only caller, via S3Client (@aws-sdk/client-s3). The browser NEVER calls MinIO directly.

NestJS surfaceResponsibility
StorageServicegeneric CRUD + ACL + signed URLs
AudioStorageServicerecording-specific helpers
StorageControllerREST: upload, stream, ACL, FHIR sync

Architecture anchor: coding_context/ddx-hms-context.md for HMS topology. Bucket-naming and attachment-type → bucket-type rules are documented as a load-bearing reference in ddx-api/CLAUDE.md (§MinIO / File Storage).

Tech Stack & Choices

  • Engine: minio/minio:latest (production deployments pin to a digest). Image set at ddx-docker-minio/docker-compose.yml:24. MinIO chosen for S3 compatibility (we can switch to AWS S3 in cloud deployments without code changes) and on-prem first-class support.
  • Client: @aws-sdk/client-s3 ^3.985.0 (the actual one used). The package list also includes minio ^8.0.6; the live client construction is the AWS SDK — see ddx-api/CLAUDE.md §MinIO. Construction lives in StorageService constructor (storage.service.ts:92-131).
  • Compression: MinIO compresses .pdf, .txt, .csv, .xml, .json and text/*, application/json, application/xml mime types on the server side (docker-compose.yml:42-44) — cheap latency cost for visible disk savings.
  • Encryption: SSE-S3 enabled at the system-bucket level by the init container (mc encrypt set sse-s3 … at docker-compose.yml:102-104). Tenant buckets created at runtime inherit the same policy via the all-clinics-rw policy + service account.
  • Service account: ddx_api_user (created in the init step at docker-compose.yml:108) has the all-clinics-rw policy attached, granting full CRUD on any clinic-* bucket — so NestJS can create new tenant buckets without needing the root admin credentials.
  • Browser is never a client: The CLAUDE.md forbidden-patterns table makes this explicit. Frontend always goes Next.js → NestJS → MinIO, with signed URLs handed to the browser for direct read of large binaries.

Data Flow

Tenant bucket provisioning (one-time, on tenant creation):

  1. Operator (or seeder) calls POST /super-admin/tenants.
  2. NestJS TenantService.createTenant() runs; among its steps it invokes provisionMinioBucket(slug) which issues S3Client.send(new CreateBucketCommand({ Bucket: 'clinic-{slug}' })).
  3. The bucket is now visible in the MinIO console at :6001 and immediately usable by every per-tenant write.

Upload flow (binary write):

  1. Browser POSTs the file to Next.js (multipart).
  2. Next.js forwards to NestJS StorageController.uploadFile() at ddx-api/src/platform/storage/storage.controller.ts:86.
  3. Controller calls StorageService.uploadFile() (storage.service.ts:136); the service resolves the destination bucket via getBucketTypeForAttachment() (storage/utils.ts:126) to pick {slug}-documents, {slug}-attachments, {slug}-reports, {slug}-recordings, etc.
  4. S3Client.send(new PutObjectCommand(...)) writes to MinIO.
  5. A Prisma row is written in ddx_api_main referencing the bucket + key + attachment type.
  6. If the file is image-like, generateAndStoreThumbnail() runs (storage.service.ts:462) and stores the thumbnail under the same bucket.

Read flow (signed download URL):

  1. UI requests a download URL via StorageController.getDownloadUrl() (storage.controller.ts:228).
  2. StorageService.getDownloadUrl() (storage.service.ts:156) generates a time-bounded presigned URL using @aws-sdk/s3-request-presigner.
  3. Browser fetches the binary directly from MinIO using the signed URL (no NestJS proxying for large files — RAM and bandwidth saving).

ACL flow (per-attachment permissions):

  • StorageService.grantAcl() / revokeAcl() / listAcl() (storage.service.ts:266, :324, :282) maintain per-attachment, per-principal permission rows. ACL writes also append to the audit log (grantAclWithAudit at :416).

No SSE on this layer: storage operations do not directly fan out SSE events. Higher-level features (Document Explorer, Recordings) publish SSE after the storage write succeeds.

Implicated Code

≥3 file:line citations required. Counts: 11 below.

  • ddx-docker-minio/docker-compose.yml:23minio service (ddx-001-minio-1).
  • ddx-docker-minio/docker-compose.yml:30 — host port mapping: ${MINIO_PORT:-15000}:9000 API + ${MINIO_CONSOLE_PORT:-15001}:9001 console (live 6000/6001).
  • ddx-docker-minio/docker-compose.yml:68minio-init one-shot container that creates system buckets, the ddx_api_user service account, and the all-clinics-rw policy.
  • ddx-docker-minio/docker-compose.yml:90 — system bucket creation (clinic-default, clinic-recordings, clinic-temp).
  • ddx-api/src/platform/storage/storage.service.ts:85StorageService class (lines 85–616), the canonical seam to MinIO.
  • ddx-api/src/platform/storage/storage.service.ts:92 — constructor (lines 92–131) constructs s3Client, reads endpoint/region from ConfigService.
  • ddx-api/src/platform/storage/storage.service.ts:136uploadFile() write path.
  • ddx-api/src/platform/storage/storage.service.ts:156getDownloadUrl() signed-URL generator.
  • ddx-api/src/platform/storage/storage.service.ts:462generateAndStoreThumbnail() — image post-processing.
  • ddx-api/src/platform/storage/storage.controller.ts:86uploadFile() REST endpoint (lines 86–164).
  • ddx-api/src/platform/storage/storage.controller.ts:228getDownloadUrl() REST endpoint (lines 228–290).
  • ddx-api/src/platform/storage/storage.module.ts:27StorageModule provider declarations.
  • ddx-api/src/platform/storage/audio-storage.service.tsAudioStorageService (recording-specific bucket logic).

Business → Technical match. Business outcome: a doctor uploads an ECG PDF to a patient record; only authorized staff at that clinic can ever fetch it back, and the file never sits in a bucket that contains another clinic's data. Technical mechanism: storage.controller.ts:86 accepts the upload, storage.service.ts:136 writes it to clinic-{slug}-reports (resolved by getBucketTypeForAttachment() from attachmentType=ECG_REPORT), a Prisma row records the bucket + key, and download requests pass through storage.controller.ts:228 which generates a time-limited signed URL — the URL cannot point at another tenant's bucket because the slug is derived from the authenticated X-Clinic-ID.

Operational Notes

  • Tenant buckets are NOT pre-created. The init container deliberately only seeds system buckets. Tenant buckets are dynamic — produced by TenantService.provisionMinioBucket(slug) on tenant creation. Manually mc mb is a smell; the right move is POST /super-admin/tenants.
  • Browser never calls MinIO directly. The platform's Trusted Gateway pattern stops at NestJS; signed URLs are the only mechanism by which a browser ever talks to port 6000.
  • Attachment-type → bucket-type mapping: getBucketTypeForAttachment() at storage/utils.ts:126 is the single source of truth. DIAGNOSTIC_REPORT / ECG_REPORT → {slug}-reports; PRESCRIPTION / MEDICAL_RECORD / etc. → {slug}-documents; AVATAR / PROFILE_IMAGE → {slug}-attachments; audio → {slug}-recordings.
  • Bucket naming convention: {clinic-slug}-{type} (e.g. ddx-hamburg-clinic-attachments); slug → bucket prefix is the only mapping you need to memorise.
  • Encryption: SSE-S3 at the bucket level; the underlying disk is not encrypted by MinIO. Use filesystem-level encryption on the host for at-rest protection.
  • Console: http://localhost:6001 (root creds from MINIO_ROOT_USER / MINIO_ROOT_PASSWORD in .env.docker; compose defaults ddx_admin/DdxSecure2024! are overridden — the live dev stack uses dudoxx_admin per context/ENVIRONMENT.md. Change in production). The console is for ops only; never use it to manage tenant data.
  • Healthcheck: mc ready local every 30s (docker-compose.yml:50-55).
  • Validation commands:
    • mc alias set ddx http://localhost:6000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" (values from .env.docker)
    • mc ls ddx — list all buckets
    • curl http://localhost:6000/minio/health/live
  • PostgreSQL — Multi-Schema Database Infrastructure — every MinIO object has a Prisma row in ddx_api_main (attachments table); the row is the source of truth, the binary is the payload.
  • Qdrant — Vector Search Infrastructure — OCR'd document content stored in MinIO becomes embeddings stored in Qdrant; the join is the attachment ID.
  • Canonical bucket-naming + attachment-type rules (not duplicated here): ddx-api/CLAUDE.md §MinIO / File Storage.