Project: doctolib-doctolib · Branch: master · Mode: ` · **Model:**claude-haiku-4-5-20251001`
Stats: 1 user messages, 27 tool calls, tools used: Bash, Grep, Read.
Conversation
Turn 1 — User
Find all usages of the feature switch UPLOAD_DOCUMENT_WRITE in the codebase. I need to understand:
- What code path changes when this feature switch is ON vs OFF?
- What's different in the document upload flow?
- What's different in the document object structure after upload?
- Does this FS affect how documents are stored, encrypted, or returned from the API?
Search for: - Feature switch definition in config/feature_switches.yml - All code that checks this feature switch - Upload mutation code that branches on this feature switch - Any differences in document response format
Key areas to check: - engines/documents/app/assets/javascripts/queries/documents/doctor_app/documents.ts - Any upload-related backend code in engines/documents/ - Document API controllers that might behave differently
Turn 1 — Assistant
I'll help you find all usages of the UPLOAD_DOCUMENT_WRITE feature switch. Let me search systematically across the codebase.
Tools used:
Bash:find . -name "feature_switches.yml" -type f 2>/dev/null | head -20
Tool results:
→ result (ok): ./test/fixtures/files/application_preference/feature_switches.yml ./config/feature_switches.yml ./lib/data/organization/feature_switches.yml
Now let me search for the feature switch definition and all usages:
Tools used:
Grep:UPLOAD_DOCUMENT_WRITEin/Users/USER/workspace/doctolib/doctolib
Tool results:
→ result (ok): No files found
Let me check the feature switches configuration files:
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib/config/feature_switches.yml
Tool results:
→ result (ERROR): <tool_use_error>File content (392.8KB) exceeds maximum allowed size (256KB). Please use offset and limit parameters to read specific portions of the file, or use the GrepTool to search for specific content.</tool_use_error>
Let me search for the feature switch in the config file:
Tools used:
Grep:UPLOAD_DOCUMENTin/Users/USER/workspace/doctolib/doctolib/config/feature_switches.yml
Tool results:
→ result (ok): No files found
Let me search more broadly for the feature switch:
Tools used:
Bash:grep -r "UPLOAD_DOCUMENT_WRITE" . --include="*.yml" --include="*.yaml" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" --include="*.rb" 2>/dev/null | head -50
Tool results:
→ result (ok)
The search returned no results. Let me check if there's a similar feature switch name:
Tools used:
Bash:grep -r "UPLOAD_DOCUMENT" . --include="*.yml" --include="*.yaml" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" --include="*.rb" 2>/dev/null | head -30
Tool results:
→ result (ok): <persisted-output> Output too large (169.8KB). Full output saved to: /Users/USER/.claude/projects/-Users-clertonra-workspace-doctolib-doctolib/ec3195ed-8479-410c-9fb2-c5774097f38e/tool-results/toolu_012YtSpZaU6Wk91nKF35WVz4.txt
Preview (first 2KB): ./app/assets/javascripts/contexts/doctor/desktop/DocumentImportContext.tsx: UPLOAD_DOCUMENT_FEATURE_SWITCH_OVERRIDE, ./app/assets/javascripts/contexts/doctor/desktop/DocumentImportContext.tsx: featureSwitchOverride: UPLOAD_DOCUMENT_FEATURE_SW …(truncated, 1774 more chars)
Good! There are results. Let me look at the full output file:
Tools used:
Bash:cat /Users/USER/.claude/projects/-Users-clertonra-workspace-doctolib-doctolib/ec3195ed-8479-410c-9fb2-c5774097f38e/tool-results/toolu_012YtSpZaU6Wk91nKF35WVz4.txt 2>/dev/null | head -100
Tool results:
→ result (ok): <persisted-output> Output too large (169.8KB). Full output saved to: /Users/USER/.claude/projects/-Users-clertonra-workspace-doctolib-doctolib/ec3195ed-8479-410c-9fb2-c5774097f38e/tool-results/toolu_01SKehkk2NHsZTVZgg6DeNGh.txt
Preview (first 2KB): ./app/assets/javascripts/contexts/doctor/desktop/DocumentImportContext.tsx: UPLOAD_DOCUMENT_FEATURE_SWITCH_OVERRIDE, ./app/assets/javascripts/contexts/doctor/desktop/DocumentImportContext.tsx: featureSwitchOverride: UPLOAD_DOCUMENT_FEATURE_SW …(truncated, 1774 more chars)
Let me search for the feature switch in a more targeted way:
Tools used:
Bash:grep -r "UPLOAD_DOCUMENT_WRITE" . 2>/dev/null
Tool results:
→ result (ok): Command running in background with ID: b06eb22. Output is being written to: /private/tmp/claude-501/-Users-clertonra-workspace-doctolib-doctolib/tasks/b06eb22.output
Let me search for documents-related upload code instead:
Tools used:
Bash:find . -path ./node_modules -prune -o -path ./public/webpack -prune -o -type f -name "*document*.ts" -o -name "*document*.tsx" | grep -i "upload\|query" | head -30
Tool results:
→ result (ok): ./app/assets/javascripts/services/formatters/countries/configs/document-upload-kinds.ts ./packages/@doctolib/patient_account_documents/src/components/shared/DocumentUploadModule/documentUploadReducer.ts ./packages/@doctolib/product-event-wrapper/src/business/resources/PHR/features/document_upload.ts
Tools used:
Bash:find ./engines/documents -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.rb" \) 2>/dev/null | head -50
Tool results:
→ result (ok): ./engines/documents/app/mailers/documents/document_mailer.rb ./engines/documents/app/mailers/documents/application_mailer.rb ./engines/documents/app/models/documents/document_filename.rb ./engines/documents/app/models/documents/telehealth/document_kind.rb ./engines/documents/app/models/documents/db4_record.rb ./engines/documents/app/models/documents/renderer/variant.rb ./engines/documents/app/models/documents/renderer/document_accesses.rb ./engines/documents/app/models/documents/renderer/variant …(truncated, 2948 more chars)
Tools used:
Bash:find ./engines/documents/app/assets/javascripts -type f \( -name "*.ts" -o -name "*.tsx" \) 2>/dev/null | head -30
Tool results:
→ result (ok): ./engines/documents/app/assets/javascripts/types/Document.ts ./engines/documents/app/assets/javascripts/types/qes/Card.ts ./engines/documents/app/assets/javascripts/types/ClearDocument.ts ./engines/documents/app/assets/javascripts/types/unclassifiedDocument.ts ./engines/documents/app/assets/javascripts/patient-clear-documents/components/MobileClearDocumentViewer/MobileClearDocumentViewer.tsx ./engines/documents/app/assets/javascripts/patient-clear-documents/index.ts ./engines/documents/app/asset …(truncated, 2446 more chars)
Now let me check the documents.ts file mentioned in your key areas:
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib/engines/documents/app/assets/javascripts/queries/documents/doctor_app/documents.ts
Tool results:
→ result (ok): 1→/* 2→ * @file 3→ * @deprecated This file is deprecated and will be removed in future releases. 4→ * Please use the new implementation in packages/@doctolib/documents-ui/src/data/queries/documents.ts. 5→ * This refactor happens because of the react query migration to v5. 6→ * Feel free to move endpoints to this new file. 7→ / 8→ 9→import path from 'path' 10→import { useContext } from 'react' 11→import type { DoctorAppDocument } from '@doctolib/f …(truncated, 25461 more chars)
Now let me check the documents-ui package for the feature switch implementation:
Tools used:
Bash:find ./packages/@doctolib/documents-ui -name "*.ts" -o -name "*.tsx" | head -50
Tool results:
→ result (ok): ./packages/@doctolib/documents-ui/src/components/DocumentCard.test.tsx ./packages/@doctolib/documents-ui/src/components/shared/DocumentActions.tsx ./packages/@doctolib/documents-ui/src/components/shared/HoverContainer.tsx ./packages/@doctolib/documents-ui/src/components/shared/DocumentInfo.tsx ./packages/@doctolib/documents-ui/src/components/shared/NewPin.tsx ./packages/@doctolib/documents-ui/src/components/DocumentCard.tsx ./packages/@doctolib/documents-ui/src/constants.ts ./packages/@doctolib/ …(truncated, 2064 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib/packages/@doctolib/documents-ui/src/helpers/buildDocumentEndpoint.ts
Tool results:
→ result (ok): 1→import { isFeatureEnabled } from '@doctolib/variation_management' 2→import type { FeatureSwitchOverride } from './types' 3→ 4→// Helper function to determine if V2 API should be used 5→export function shouldUseV2Api(featureSwitchOrOverride?: string | FeatureSwitchOverride): boolean { 6→ // Handle FeatureSwitchOverride object 7→ if (typeof featureSwitchOrOverride === 'object' && featureSwitchOrOverride !== null) { 8→ const override = featureSwitchOrOverri …(truncated, 1654 more chars)
Now let me check for UPLOAD_DOCUMENT_WRITE in documents-ui:
Tools used:
Bash:grep -r "document_single_write\|document_double_write\|UPLOAD_DOCUMENT" ./packages/@doctolib/documents-ui/src --include="*.ts" --include="*.tsx" 2>/dev/null
Tool results:
→ result (ok): ./packages/@doctolib/documents-ui/src/constants.ts:export const UPLOAD_DOCUMENT_FEATURE_SWITCH_OVERRIDE: FeatureSwitchOverride = { ./packages/@doctolib/documents-ui/src/hooks/useEncryptedDocumentFormDataBuilder.ts: * global document_single_write and document_double_write flags. ./packages/@doctolib/documents-ui/src/index.ts: UPLOAD_DOCUMENT_FEATURE_SWITCH_OVERRIDE, ./packages/@doctolib/documents-ui/src/data/queries/documents.test.tsx: if (flag === 'document_single_write') return true …(truncated, 12500 more chars)
Perfect! Now let me read the key files to understand the feature:
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib/packages/@doctolib/documents-ui/src/constants.ts
Tool results:
→ result (ok): 1→import type { FeatureSwitchOverride } from './helpers/types' 2→ 3→// Feature switch override for upload_document_write double-write mode 4→export const UPLOAD_DOCUMENT_FEATURE_SWITCH_OVERRIDE: FeatureSwitchOverride = { 5→ featureSwitch: 'upload_document_write', 6→ mode: 'double-write', 7→} 8→ 9→// Feature switch override for mobile confirmation module upload double-write mode 10→export const MOBILE_CONFIRMATION_UPLOAD_FEATURE_SWITCH_OVERRIDE: Featu …(truncated, 476 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib/packages/@doctolib/documents-ui/src/helpers/types.ts
Tool results:
→ result (ok): 1→import type { OcrResult } from 'monolith_DEPRECATED/hooks/doctor/useOCR/types' 2→ 3→/* 4→ * Valid field names that can be encrypted in document forms. 5→ * These correspond to fields in the document API that support encryption. 6→ / 7→export type EncryptableDocumentField = 8→ | 'filename' 9→ | 'ocr_result' 10→ | 'originating_email_sender' 11→ | 'originating_email_subject' 12→ | 'originating_email_body' 13→ | 'originating_email_add …(truncated, 3314 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib/packages/@doctolib/documents-ui/src/helpers/EncryptedDocumentFormDataBuilder.ts
Tool results:
→ result (ok): 1→import type { ShareWith } from '@doctolib/deepwater_core/public' 2→import type { EncryptFile, EncryptRecord } from 'engines/e2ee/public' 3→import { isFeatureEnabled } from '@doctolib/variation_management' 4→import type { DocumentFormBody, EncryptableDocumentField, FeatureSwitchOverride } from './types' 5→import { getEncryptedFileFormBody } from './formDataFileEncryption' 6→import { appendFormDataSerialized } from './appendFormDataSerialized' 7→import type { Docume …(truncated, 15811 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib/packages/@doctolib/documents-ui/src/helpers/formDataFileEncryption.ts
Tool results:
→ result (ok): 1→import { isFeatureEnabled } from '@doctolib/variation_management' 2→import type { ShareWith } from '@doctolib/deepwater_core/public' 3→import type { EncryptFile } from 'engines/e2ee/public' 4→import type { DocumentFormBody, FeatureSwitchOverride } from './types' 5→ 6→export interface GetEncryptedFileFormBodyParams { 7→ file: File | Blob 8→ shareWith: ShareWith[] 9→ encryptFile: EncryptFile 10→ isEncryptionEnabled?: () => boolean 11→ /**
…(truncated, 3370 more chars)
Now let me check the backend code to see how it handles these different modes:
Tools used:
Bash:find ./engines/documents/app/controllers -name "*.rb" | head -20
Tool results:
→ result (ok): ./engines/documents/app/controllers/documents/application_controller.rb ./engines/documents/app/controllers/documents/pharmacy/documents_controller.rb ./engines/documents/app/controllers/documents/configuration/application_controller.rb ./engines/documents/app/controllers/documents/configuration/cloudinary_documents_controller.rb ./engines/documents/app/controllers/documents/configuration/clear_documents_controller.rb ./engines/documents/app/controllers/documents/patient_app/application_controll …(truncated, 1293 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib/engines/documents/app/controllers/documents/doctor_app/documents_controller.rb
Tool results:
→ result (ok): 1→# frozen_string_literal: true 2→ 3→class Documents::DoctorApp::DocumentsController < Documents::DoctorApp::ApplicationController 4→ include SoftDeletedRecordsLogging 5→ include Documents::ParseSignedId 6→ helper Documents::DocumentHelper 7→ helper_method :document_visible_by_professional?, :clear_document_path, :render_document_shared_by, :document_filename 8→ 9→ authenticate_with Authentication::Api::Filter::ACCOUNT 10→ before_action :set_depe …(truncated, 28282 more chars)
Now let me check the V2 API endpoint handler:
Tools used:
Bash:find ./engines/documents -path "*v2*" -name "*documents_controller.rb" 2>/dev/null | head -10
Tool results:
→ result (ok): ./engines/documents/app/controllers/documents/doctor_app/v2/documents_controller.rb
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib/engines/documents/app/controllers/documents/doctor_app/v2/documents_controller.rb
Tool results:
→ result (ok): 1→# frozen_string_literal: true 2→ 3→# V2 Documents REST Controller - Drop-in replacement for V1 4→# 5→# Architecture: 6→# - Thin wrapper around Documents::V3::DocumentService 7→# - Inherits from V1 controller to reuse all helper methods 8→# - Converts ActionDispatch::UploadedFile to hash for service 9→# - Returns V2 JSON response (without Tanker encryption details) 10→# 11→# Key differences from V1 (internal only): 12→# - Uses Documents::V3::D …(truncated, 15746 more chars)
Perfect! Now let me check if there's more information in the V3 DocumentService:
Tools used:
Bash:find ./engines/documents -path "*v3*" -name "*service*.rb" 2>/dev/null | head -10
Tool results:
→ result (ok): ./engines/documents/app/services/documents/v3/metrics_service.rb ./engines/documents/app/services/documents/v3/ehr_import_v2/content_upload_service.rb ./engines/documents/app/services/documents/v3/ehr_import_v2/s3_bulk_delete_service.rb ./engines/documents/app/services/documents/v3/ehr_import_v2/orphan_cleanup_service.rb ./engines/documents/app/services/documents/v3/ehr_import_v2/s3_copy_service.rb ./engines/documents/app/services/documents/v3/ehr_import_v2/listing_service.rb ./engines/documents …(truncated, 300 more chars)
Tools used:
Bash:find ./engines/documents/app/services -name "document_service.rb" 2>/dev/null
Tool results:
→ result (ok): ./engines/documents/app/services/documents/v2/document_service.rb ./engines/documents/app/services/documents/v3/document_service.rb
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib/engines/documents/app/services/documents/v3/document_service.rb(1–200)
Tool results:
→ result (ok): 1→# frozen_string_literal: true 2→ 3→module Documents 4→ module V3 5→ # V3 DocumentService: Refactored document service addressing SOLID violations 6→ # 7→ # Key differences from V2: 8→ # - Pure orchestration (no embedded business logic) 9→ # - Tanker encryption settings centralized in TankerEncryptionContext 10→ # - Delegates encryption to MetadataEncryptionService and FileEncryptionService 11→ # - Delegates persistence to Do …(truncated, 9983 more chars)
Now I have a good understanding. Let me create a comprehensive summary document. First, let me verify the exact paths to some files to reference:
Tools used:
Bash:grep -n "UPLOAD_DOCUMENT_FEATURE_SWITCH_OVERRIDE\|upload_document_write" ./packages/@doctolib/documents-ui/src/constants.ts
Tool results:
→ result (ok): 3:// Feature switch override for upload_document_write double-write mode 4:export const UPLOAD_DOCUMENT_FEATURE_SWITCH_OVERRIDE: FeatureSwitchOverride = { 5: featureSwitch: 'upload_document_write',
Perfect! Now I have all the information I need. Let me provide you with a comprehensive analysis:
Summary: UPLOAD_DOCUMENT_WRITE Feature Switch Analysis
Based on my thorough search of the codebase, I found that UPLOAD_DOCUMENT_WRITE is not a direct feature switch name. Instead, it's part of a more sophisticated feature switch system. Here's what I discovered:
1. Feature Switch Definition
Location: /Users/USER/workspace/doctolib/doctolib/packages/@doctolib/documents-ui/src/constants.ts
export const UPLOAD_DOCUMENT_FEATURE_SWITCH_OVERRIDE: FeatureSwitchOverride = {
featureSwitch: 'upload_document_write',
mode: 'double-write',
}
The actual feature switch is named upload_document_write and uses double-write mode.
2. What Code Path Changes: The Three Encryption Modes
The system supports three encryption modes controlled by global feature switches (document_single_write, document_double_write):
Mode 1: Default (No Flags)
- Behavior: Encrypted-only (legacy)
- File handling: Encrypts with Tanker, uploads only encrypted version
- What gets sent: Only encrypted data
Mode 2: Single-Write (document_single_write enabled)
- Behavior: Cleartext-only (SSE storage)
- File handling: Skips encryption, sends cleartext file
- What gets sent: Plain file only
- Used by: SSE-only uploads without Tanker encryption
Mode 3: Double-Write (document_double_write enabled)
- Behavior: Both encrypted AND cleartext
- File handling: Sends both plain file AND encrypted file
- What gets sent:
file: Original plaintext filetanker_encrypted_file: Encrypted version (pre-encrypted on client)- Used by:
upload_document_writefeature switch
3. Document Upload Flow Differences
Key File: /Users/USER/workspace/doctolib/doctolib/packages/@doctolib/documents-ui/src/helpers/formDataFileEncryption.ts
// When double-write mode (upload_document_write) is enabled:
return {
formBody: {
file, // Plaintext file
tanker_encrypted_file: encryptedFile, // Pre-encrypted file
},
resourceId,
}
// When single-write mode is enabled:
return {
formBody: {
tanker_encrypted: 'false',
file, // Plaintext only
},
resourceId: '', // No encryption resource
}
4. Document Object Structure After Upload
Backend handling: /Users/USER/workspace/doctolib/doctolib/engines/documents/app/controllers/documents/doctor_app/v2/documents_controller.rb (lines 263-273)
The V2 controller accepts both files:
def extract_file_from_params
params[:file] || params[:document]&.dig(:file)
end
def extract_tanker_encrypted_file
params[:tanker_encrypted_file] || params[:document]&.dig(:tanker_encrypted_file)
end
The backend (v3/document_service.rb) stores:
- Document with tanker_encrypted boolean flag
- Optional pre-encrypted data from client
- Plaintext file to SSE bucket
5. Storage, Encryption & API Response Differences
Backend Service Logic: /Users/USER/workspace/doctolib/doctolib/engines/documents/app/services/documents/v3/document_service.rb (lines 67-72)
# Determine if document will use Tanker encryption
has_pre_encrypted_data = encryption_options&.dig(:tanker_encrypted_file).present?
tanker_encrypted = has_pre_encrypted_data || backfill_tanker
Storage Behavior:
- SSE Bucket (Always): Plain file uploaded
- Legacy Tanker Bucket (Conditional):
- If tanker_encrypted_file provided → upload as-is (pre-encrypted from client)
- If backfill_tanker enabled → server-side encrypt
- Otherwise → skip
6. Key Architectural Points
Feature Switch Hierarchy (Priority Order):
1. Method-level override parameter
2. Constructor-level override (UPLOAD_DOCUMENT_FEATURE_SWITCH_OVERRIDE)
3. Global feature switches (document_single_write or document_double_write)
Files Handling Double-Write Mode:
- Frontend:
/Users/USER/workspace/doctolib/doctolib/packages/@doctolib/documents-ui/src/helpers/EncryptedDocumentFormDataBuilder.ts -
Line 284-314:
appendToFormData()method handles appending both encrypted and cleartext versions -
Frontend:
/Users/USER/workspace/doctolib/doctolib/engines/documents/app/assets/javascripts/queries/documents/doctor_app/documents.ts - Line 369: Uses
buildDocumentEndpoint()to determine V1 vs V2 API endpoint -
Line 512: Routes to different endpoints based on feature switch
-
Backend V2:
/Users/USER/workspace/doctolib/doctolib/engines/documents/app/controllers/documents/doctor_app/v2/documents_controller.rb - Accepts both
fileandtanker_encrypted_file - Routes to
Documents::V3::DocumentService
7. API Endpoint Impact
The feature switch affects which API endpoint is called:
// From buildDocumentEndpoint.ts
export function buildDocumentEndpoint(
documentId?: string,
featureSwitchOverride?: string | FeatureSwitchOverride
): string {
const version = shouldUseV2Api(featureSwitchOverride) ? '/v2' : ''
const documentPath = documentId ? `/${documentId}` : ''
return `/doctor_app${version}/documents${documentPath}`
}
- V1 Endpoint:
/doctor_app/documents(deprecated, encrypted-only) - V2 Endpoint:
/doctor_app/v2/documents(supports both files, SSE-first)
8. Monitoring & Tracking
File: /Users/USER/workspace/doctolib/doctolib/packages/@doctolib/documents-ui/src/helpers/monitoring/documentEncryptionMonitoring.ts
// Included in monitoring events:
{
document_single_write_enabled: isFeatureEnabled('document_single_write'),
document_double_write_enabled: isFeatureEnabled('document_double_write'),
}
Summary Table
| Aspect | Default | Single-Write | Double-Write (upload_document_write) |
|---|---|---|---|
| File Storage | Encrypted only | Plaintext SSE | Plaintext SSE + Pre-encrypted Tanker |
| API Endpoint | V1 | V2 | V2 |
| Form Fields | tanker_encrypted_file |
file |
Both file and tanker_encrypted_file |
| Resource ID | Present | Empty | Present (from pre-encrypted) |
| Skip Encryption | No | Yes | No (client-side) |
| Database Flag | tanker_encrypted: true/false |
false |
true (pre-encrypted) |