Quorentra

Quorentra Adding CRM Documents and Attachments: Building from Zero — Part 26

Series: Building Quorentra CRM from Zero — A Modular, ChatGPT-Native AI CRM

Building secure document uploads, storage abstraction, metadata, CRM entity relationships, permissions, authorized retrieval, deletion, auditing, and ChatGPT document workflows.

Quorentra Adding CRM Documents and Attachments - Building from Zero — Part 26
Quorentra Adding CRM Documents and Attachments – Building from Zero — Part 26

1. Introduction

By Part 25, Quorentra has become a surprisingly capable CRM platform.

Our core structured domains now include:

Company
Contact
Opportunity
Activity
Task
Meeting

We also introduced a CRM Context Layer capable of assembling these domains into bounded, permission-aware evidence packages for ChatGPT.

The user can ask:

What’s happening with Adventure Works?

Quorentra can retrieve:

Adventure Works
├── Contacts
├── Open Opportunities
├── Recent Activities
├── Open Tasks
└── Upcoming Meetings

ChatGPT can reason over that evidence.

But an important part of customer knowledge is still missing.

Real customer relationships produce files.

For example:

Proposals
Contracts
Statements of Work
Requirements Documents
Pricing Sheets
Architecture Documents
Presentations
Meeting Notes
Project Plans
Security Questionnaires
RFPs
Purchase Orders
Technical Specifications

A CRM that cannot associate these documents with customer records is incomplete.

Therefore Part 26 introduces a dedicated:

Document and Attachment domain

But we are deliberately not building document AI yet.

We first need to answer a more fundamental question:

How does Quorentra securely own, store, relate, retrieve, and govern customer documents?

Only after that foundation exists should we introduce text extraction, chunking, embeddings, semantic retrieval, and RAG.


2. Our Target Interaction

Imagine the user has uploaded:

AdventureWorks-Azure-Migration-Proposal-v3.pdf

The user tells ChatGPT:

Attach this revised proposal to the Adventure Works Azure Migration opportunity.

ChatGPT identifies:

Intent:
Attach Document
Document:
Uploaded File
Company Reference:
Adventure Works
Opportunity Reference:
Azure Migration

Quorentra resolves:

Adventure Works
Company
Azure Migration
Opportunity

The user sees:

┌──────────────────────────────────────────────┐
│ QUORENTRA │
│ Attach Document │
│ │
│ AdventureWorks-Azure-Migration-Proposal-v3 │
│ PDF · 2.4 MB │
│ │
│ Company │
│ Adventure Works │
│ │
│ Opportunity │
│ Azure Migration │
│ │
│ Attach this document? │
│ │
│ [Cancel] [Attach Document] │
└──────────────────────────────────────────────┘

After confirmation, Quorentra stores the file and its authoritative metadata.

The Opportunity can then show:

Azure Migration
├── Activities
├── Tasks
├── Meetings
└── Documents
└── AdventureWorks-Azure-Migration-Proposal-v3.pdf

That is our first goal.


3. Documents Are CRM Entities

A common mistake is to treat uploaded files as little more than filesystem objects.

For example:

/uploads/customer/proposal.pdf

That is not sufficient.

A CRM needs to know:

Who owns the document?
Which tenant does it belong to?
Who uploaded it?
What CRM entity is it associated with?
What type of file is it?
How large is it?
Where is it stored?
Has it been deleted?
Can this user access it?
Has it been processed?

Therefore:

A Document is a first-class CRM domain entity.

The binary file is only one component of that entity.


4. Separate Metadata from Binary Storage

Our architecture should distinguish:

Document Metadata

from:

Document Binary

Conceptually:

PostgreSQL
└── Document
├── id
├── organization_id
├── filename
├── MIME type
├── size
├── checksum
├── storage key
├── relationships
└── audit metadata
Object/File Storage
└── Binary File

Do not store large uploaded files directly inside ordinary relational rows unless there is a compelling reason.


5. Why This Separation Matters

PostgreSQL is excellent for:

Metadata
Relationships
Transactions
Permissions
Search
Auditability

Object storage is better suited to:

PDF
DOCX
XLSX
PPTX
Images
Large binaries

Separating them also lets us change storage technology later without changing the Document domain.


6. Storage Abstraction

For local development, Quorentra may initially store files on disk.

For production, we may later use:

Amazon S3
Azure Blob Storage
Google Cloud Storage
S3-compatible storage

The application should not care which one is underneath.

Introduce:

DocumentStorage

as an abstraction.


7. Storage Interface

Conceptually:

class DocumentStorage(Protocol):
async def save(
self,
*,
storage_key: str,
content: BinaryIO,
) -> None:
...
async def open(
self,
*,
storage_key: str,
) -> BinaryIO:
...
async def delete(
self,
*,
storage_key: str,
) -> None:
...
async def exists(
self,
*,
storage_key: str,
) -> bool:
...

Domain services depend on this abstraction.


8. Local Development Storage

For the MVP:

LocalDocumentStorage

might store files under:

backend/storage/documents/

But do not use the original filename as the storage path.

Bad:

storage/documents/proposal.pdf

Better:

storage/documents/
└── org_x/
└── 2026/
└── 08/
└── <generated-storage-key>

The exact physical layout is an implementation detail.


9. Never Trust Uploaded Filenames

A filename such as:

proposal.pdf

is user-controlled input.

So is:

../../important-file

The original filename should be stored as metadata, not used directly to determine filesystem location.

Generate an opaque storage key.


10. Storage Key

For example:

organizations/
<organization_uuid>/
documents/
<document_uuid>/
original

or an equivalent opaque structure.

The important properties are:

Tenant-aware
Collision-resistant
Not user-controlled
Storage-provider independent

11. Starting Checkpoint

Before implementing Part 26, verify Part 25.

Run:

cd backend
python -m pytest

Then:

cd ..\chatgpt-ui
npm run build

Test:

What’s happening with Adventure Works?

Verify:

Search CRM
Resolve Company
CRM Context Builder
Permission Filtering
Bounded Context
Grounded ChatGPT Answer

Part 26 adds Documents without breaking that existing retrieval architecture.


12. Document Permissions

Introduce:

documents.read
documents.create
documents.delete

Later we may add:

documents.update
documents.download
documents.process
documents.manage

For the MVP, keep the permission model small.


13. Read Versus Download Permission

Initially:

documents.read

can authorize both:

metadata retrieval
authorized binary retrieval

Later, if necessary, we can separate:

documents.read
documents.download

This is a policy decision rather than a structural limitation.


14. Example Role Matrix

RoleReadCreateDelete
Viewer
Member
Manager
Admin
Owner

The exact matrix can evolve.

The important requirement is that permissions are checked server-side.


15. Define the Document Model

Conceptually:

Document
├── id
├── organization_id
├── original_filename
├── display_name
├── mime_type
├── file_extension
├── file_size_bytes
├── checksum_sha256
├── storage_provider
├── storage_key
├── status
├── uploaded_by_user_id
├── created_at
├── updated_at
├── deleted_at
├── deleted_by_user_id
└── version

Relationships will be handled separately.


16. Document Status

Start with:

class DocumentStatus(str, Enum):
AVAILABLE = "available"
DELETED = "deleted"

Later we can introduce processing states such as:

UPLOADING
PROCESSING
READY
FAILED
QUARANTINED

when document processing is added.

For Part 26, keep the lifecycle simple.


17. Why Not Add AI Processing States Yet?

Because Part 26 does not yet perform:

Text Extraction
OCR
Chunking
Embedding
Semantic Indexing

Adding states for nonexistent workflows would create unnecessary complexity.

Build them when the processing pipeline exists.


18. Original Filename

Store:

original_filename

exactly enough to identify the uploaded file to the user.

Example:

AdventureWorks-Azure-Migration-Proposal-v3.pdf

But sanitize it for display and never use it directly as a storage path.


19. Display Name

Optionally separate:

original_filename

from:

display_name

For example:

original_filename:
AdventureWorks-Azure-Migration-Proposal-v3.pdf
display_name:
Azure Migration Proposal v3

For the MVP, display name can default to the filename without its extension.


20. MIME Type

Store:

mime_type

Examples:

application/pdf
application/vnd.openxmlformats-officedocument.wordprocessingml.document
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
application/vnd.openxmlformats-officedocument.presentationml.presentation
text/plain
image/png
image/jpeg

But do not trust the browser-supplied MIME type blindly.


21. File Extension

Also record:

file_extension

such as:

pdf
docx
xlsx
pptx
txt
png
jpg

This helps validation and user display.


22. MIME and Extension Must Agree

A file named:

proposal.pdf

should not automatically be accepted as a PDF.

The upload pipeline should compare:

Declared MIME Type
File Extension
Detected File Type

where practical.

If they conflict, reject or quarantine the upload.


23. File Size

Store:

file_size_bytes

Do not rely on the client-reported value.

Calculate or verify it server-side.


24. Upload Size Limit

For the MVP, define a configurable limit.

For example:

MAX_DOCUMENT_SIZE_MB = 25

The exact number is less important than having an explicit limit.

Do not accept unlimited uploads.


25. Why File Limits Matter

Without limits:

Huge Upload
Memory Pressure
Storage Growth
Request Timeouts
Denial-of-Service Risk

The server must control resource consumption.


26. Stream Uploads

Avoid loading large files entirely into memory.

Prefer:

Incoming File
Stream
Validation
Checksum
Storage

rather than:

Incoming File
Read Entire File into RAM
Process

This becomes increasingly important in production.


27. SHA-256 Checksum

Calculate:

checksum_sha256

for every uploaded file.

Example:

7f83b1657ff1fc53...

This gives us:

Integrity checking
Duplicate detection foundation
Storage verification
Audit support

28. Do Not Deduplicate Automatically Yet

Two users may intentionally upload identical files to different CRM records.

Therefore Part 26 should calculate checksums but not necessarily collapse duplicate Documents.

Later we can implement intelligent duplicate handling.


29. Storage Provider

Store a logical value such as:

local

during development.

Later:

s3
azure_blob
gcs

This allows migration and multiple storage backends.


30. Document Relationships

A Document becomes useful when connected to CRM context.

It may relate to:

Company
Contact
Opportunity
Activity
Task
Meeting

We need a flexible relationship model.


31. Avoid Six Nullable Foreign Keys

One possible design is:

document.company_id
document.contact_id
document.opportunity_id
document.activity_id
document.task_id
document.meeting_id

This works initially, but becomes awkward.

A Document may also need multiple relationships.

For example:

Proposal
→ Adventure Works
→ Azure Migration
→ Proposal Review Meeting

Therefore introduce a dedicated relationship table.


32. DocumentLink

Conceptually:

DocumentLink
├── id
├── organization_id
├── document_id
├── entity_type
├── entity_id
├── created_by_user_id
└── created_at

This creates a flexible attachment model.


33. Supported Link Types

Define:

class DocumentLinkEntityType(str, Enum):
COMPANY = "company"
CONTACT = "contact"
OPPORTUNITY = "opportunity"
ACTIVITY = "activity"
TASK = "task"
MEETING = "meeting"

This aligns with our existing CRM entity types.


34. Why a Generic Link Table?

Because:

Document
├── Company
├── Opportunity
└── Meeting

can now be represented naturally.

We also avoid modifying the Document table every time a new attachable CRM domain is added.


35. Generic Relationships Require Validation

The flexibility of:

entity_type
entity_id

comes with responsibility.

The database cannot always enforce all cross-table foreign keys directly.

Therefore the application must strictly validate:

Entity exists
Entity belongs to active tenant
User can access entity
Entity type is supported

before creating a DocumentLink.


36. Tenant Isolation

Both:

Document

and:

DocumentLink

contain:

organization_id

This allows explicit tenant filtering.

Every document query must include the active tenant.


37. Cross-Tenant Linking Must Be Impossible

Suppose:

Document
→ Tenant A
Opportunity
→ Tenant B

Creating:

DocumentLink

between them must fail.

This is a hard security invariant.


38. Upload Workflow

The basic upload flow becomes:

Receive File
Authenticate User
Resolve Tenant
Check documents.create
Validate Filename
Validate Size
Validate Type
Generate Document ID
Generate Storage Key
Stream File
Calculate Checksum
Store Binary
Create Document Metadata
Create AuditEvent
Commit

But storage and database transactions require careful coordination.


39. The Storage Transaction Problem

PostgreSQL can roll back database changes.

Object storage generally cannot participate in the same ACID transaction.

Suppose:

Store File
Insert Document

Now we have an orphaned binary.

Or:

Insert Document
Store File

Now the database points to a missing file.

We need a deliberate strategy.


40. MVP Upload Strategy

For the first implementation:

Validate
Store Binary
Create Document Row
Create AuditEvent
Commit

If database creation fails after storage succeeds:

Best-effort delete stored binary

If cleanup fails:

Log orphan storage object

Later we can introduce a more sophisticated staging workflow.


41. Future Production Strategy

A more advanced design might use:

Upload Session
Temporary Storage
Database Metadata
Finalize Storage
AVAILABLE

or asynchronous reconciliation.

But that is beyond the Part 26 MVP.


42. Direct Upload Endpoint

Introduce:

POST /api/v1/documents

using:

multipart/form-data

This is different from our normal JSON APIs because the request contains binary content.


43. Example Upload Request

Conceptually:

file:
AdventureWorks-Azure-Migration-Proposal-v3.pdf
display_name:
Azure Migration Proposal v3

The server derives:

organization_id
uploaded_by_user_id
status
storage_key
checksum
file_size

44. Do Not Accept Storage Keys from Clients

Never allow:

storage_key =
"user-provided/path"

The server generates it.

Likewise, clients cannot provide:

organization_id
uploaded_by_user_id
status
checksum

as authoritative values.


45. Upload and Link as Separate Operations

A useful modular design is:

Upload Document

followed by:

Link Document

This separates:

File ownership

from:

CRM relationship

That makes the Document module reusable.


46. Why Separate Them?

Suppose the user uploads a proposal and later wants to attach it to:

Company
Opportunity
Meeting

The file should not need to be uploaded three times.

Instead:

Document
DocumentLink
├── Company
├── Opportunity
└── Meeting

47. Direct REST Linking

Introduce:

POST /api/v1/documents/{document_id}/links

Conceptual body:

{
"entity_type": "opportunity",
"entity_id": "..."
}

The backend validates the target.


48. ChatGPT Should Not Supply Arbitrary IDs

Conversationally, the user says:

Attach this proposal to the Adventure Works Azure Migration opportunity.

ChatGPT should provide:

Document reference
Company reference
Opportunity reference

Quorentra resolves authoritative IDs.

This follows the same architecture used throughout the CRM.


49. Document Draft

For ChatGPT-native attachment workflows, introduce:

DocumentLinkDraft

rather than allowing immediate arbitrary attachment.

Conceptually:

DocumentLinkDraft
├── id
├── organization_id
├── user_id
├── document_id
├── entity_reference
├── entity_type
├── resolved_entity_id
├── status
├── workflow_id
├── expires_at
├── created_at
└── updated_at

50. Why Use a Link Draft?

User:

Attach this to the Azure project.

There may be multiple matches:

Azure Migration
Azure Security Review
Azure Landing Zone

The draft can preserve the intent while Quorentra resolves ambiguity.


51. Create Document Link Draft Tool

Introduce:

create_document_link_draft

Conceptually:

class CreateDocumentLinkDraftInput(BaseModel):
document_id: UUID
entity_type: DocumentLinkEntityType | None = None
entity_reference: str

The document ID is acceptable here only when it originates from an authoritative upload result already available to the application workflow.


52. Resolution Flow

Document
Entity Reference
Unified CRM Search
Entity Resolution
Tenant Validation
Permission Validation
Relationship Validation
DocumentLinkDraft

Part 25’s unified search infrastructure is reused.


53. Example

User:

Attach this proposal to the Adventure Works Azure Migration opportunity.

Quorentra resolves:

Adventure Works
Company
Azure Migration
Opportunity

The Link Draft becomes:

document_id:
<uploaded document>
entity_type:
opportunity
resolved_entity_id:
Azure Migration UUID

54. Prepare Document Link

Introduce:

prepare_document_link

Input:

{
"draft_id": "..."
}

Flow:

Load Draft
Verify Tenant
Verify User
Verify documents.create
Verify Document AVAILABLE
Verify Target Entity
Verify Entity Permission
Verify Relationship
Check Link Does Not Already Exist
Create MutationRequest
Return Confirmation

No DocumentLink exists yet.


55. Mutation Type

Use:

document.link.create

The MutationRequest contains authoritative proposed values.


56. Confirmation Widget

Create:

chatgpt-ui/src/documents/
└── DocumentLinkConfirmation.tsx

Render:

┌──────────────────────────────────────────────┐
│ QUORENTRA │
│ Attach Document │
│ │
│ Azure Migration Proposal v3 │
│ PDF · 2.4 MB │
│ │
│ Company │
│ Adventure Works │
│ │
│ Opportunity │
│ Azure Migration │
│ │
│ [Cancel] [Attach Document] │
└──────────────────────────────────────────────┘

57. Execute Document Link

Introduce:

create_document_link

Input:

{
"mutation_request_id": "..."
}

Execution:

Load MutationRequest
Verify Tenant
Verify User
Verify Permission
Verify Pending
Verify Not Expired
Reload Document
Reload Target Entity
Verify Both Same Tenant
Verify Target Permission
Check Duplicate
Create DocumentLink
Create AuditEvent
Execute MutationRequest
Convert Draft
Commit

58. Link Audit Event

Use:

action =
document.link.created

Useful metadata:

document_id
entity_type
entity_id
actor_user_id
mutation_request_id
workflow_id

59. Document Upload Audit Event

When the file itself is uploaded:

action =
document.created

Record metadata such as:

document_id
mime_type
file_size_bytes
checksum
uploaded_by_user_id

Avoid logging document contents.


60. Reading Documents

Introduce:

GET /api/v1/documents/{document_id}

and ChatGPT tool:

get_document

This returns metadata, not necessarily the binary file itself.


61. Document Metadata Response

Conceptually:

{
"id": "...",
"display_name": "Azure Migration Proposal v3",
"original_filename": "AdventureWorks-Azure-Migration-Proposal-v3.pdf",
"mime_type": "application/pdf",
"file_size_bytes": 2516582,
"status": "available",
"uploaded_at": "...",
"uploaded_by": {
"id": "...",
"name": "..."
},
"links": [
{
"entity_type": "opportunity",
"entity_id": "...",
"display_name": "Azure Migration"
}
]
}

Do not expose:

raw filesystem path
cloud storage secret
bucket credentials
internal signed-storage credentials

62. Document Listing

Introduce:

search_documents

Useful filters:

query
entity_type
entity_id
mime_type
uploaded_from
uploaded_to
limit

63. Opportunity Documents

User:

Show me the documents for the Azure Migration opportunity.

Flow:

Resolve Opportunity
Find DocumentLinks
Load AVAILABLE Documents
Permission Filter
Return Document List

64. Company Documents

User:

What documents do we have for Adventure Works?

Return Documents linked directly to the Company.

Later we may optionally include Documents linked to child Opportunities.

But do not silently change relationship semantics.


65. Direct Versus Related Documents

We should distinguish:

Direct Documents

from:

Related Documents

Example:

Adventure Works
├── Direct Document
│ └── Master Services Agreement
└── Opportunity
└── Azure Migration
└── Proposal.pdf

A Company-level search may eventually support both.

For the MVP, make the behavior explicit.


66. Document List Widget

Create:

chatgpt-ui/src/documents/
├── DocumentList.tsx
├── DocumentCard.tsx
├── DocumentDetail.tsx
└── DocumentLinkConfirmation.tsx

Example card:

┌────────────────────────────────────────────┐
│ Azure Migration Proposal v3 │
│ │
│ PDF · 2.4 MB │
│ │
│ Azure Migration │
│ Adventure Works │
│ │
│ Uploaded 2 Aug 2026 │
│ │
│ [View] │
└────────────────────────────────────────────┘

67. Authorized Download

The binary file must never be exposed through a predictable public URL.

Bad:

/public/uploads/proposal.pdf

Anyone who guesses the URL might retrieve it.

Instead:

User
Quorentra
Authentication
Tenant Check
documents.read
Document Authorization
Binary Retrieval

68. Download Endpoint

Introduce something like:

GET /api/v1/documents/{document_id}/content

The backend authorizes the request before returning or redirecting to the content.


69. Local Storage Retrieval

For local development:

Authorized Request
Document Metadata
storage_key
LocalDocumentStorage.open()
Stream Response

Never accept a filesystem path from the client.


70. Future Object Storage Retrieval

With cloud storage:

Authorized Request
Quorentra
Generate Short-Lived Signed URL
Return / Redirect

The signed URL should be:

Short-lived
Document-specific
Generated only after authorization

71. Do Not Store Signed URLs

Signed URLs are temporary access mechanisms.

They should not become authoritative Document fields.

Store:

storage_key

not:

signed_download_url

72. Content-Disposition

When serving a file, return an appropriate:

Content-Disposition

using a sanitized filename.

This allows the browser to present a meaningful filename without exposing the storage key.


73. File Type Allowlist

For the first CRM MVP, support a conservative set.

For example:

PDF
DOCX
XLSX
PPTX
TXT
CSV
PNG
JPEG

Reject unsupported types.


74. Why Use an Allowlist?

An allowlist is safer than:

accept everything except known bad files

The latter is difficult to maintain.

Instead:

Known supported type
→ accept
Unknown type
→ reject

75. Executable Files

Files such as:

.exe
.bat
.cmd
.ps1
.js
.vbs
.scr

should not be accepted as normal CRM Documents in the MVP.

Quorentra is not a general-purpose file-sharing system.


76. Archive Files

Be cautious with:

ZIP
RAR
7z

They introduce:

Nested content
Compression bombs
Hidden executables
Malware scanning complexity

Exclude them from the first implementation unless required.


77. Malware Scanning

A production document platform should eventually scan uploaded files.

Possible future architecture:

Upload
Temporary Storage
Malware Scan
Clean?
┌────┴────┐
│ │
Yes No
│ │
▼ ▼
Available Quarantined

But Part 26 does not need to implement a full malware scanning service.


78. Prepare for Future Scanning

Even if scanning is not implemented yet, keep storage architecture compatible with:

Temporary
Quarantine
Available

states later.

Do not tightly couple Document availability to one physical folder.


79. Document Deletion

Users eventually need:

Delete the old proposal.

Deletion changes authoritative state.

Therefore it should use our governed mutation path.


80. Do Not Immediately Hard Delete

For the MVP, use logical deletion.

Set:

status =
deleted
deleted_at =
server timestamp
deleted_by_user_id =
authenticated user

Do not immediately erase the database row.


81. Why Soft Delete?

It preserves:

Audit history
Document identity
Relationship history
Who deleted it
When it was deleted

It also gives us room for future retention policies.


82. What About the Binary?

For the first implementation, there are two options:

A. Keep binary during soft-delete retention
B. Delete binary immediately but preserve metadata

For a CRM MVP, option A is usually operationally simpler.

Then a later retention process can permanently purge it.


83. Prepare Document Deletion

Introduce:

prepare_document_deletion

Input:

{
"document_id": "...",
"expected_version": 1
}

The server verifies:

Tenant
Permission
Document status
Version

84. Deletion Confirmation

┌──────────────────────────────────────────────┐
│ QUORENTRA │
│ Delete Document │
│ │
│ Azure Migration Proposal v2 │
│ PDF · 2.2 MB │
│ │
│ This document will no longer be available │
│ in normal CRM views. │
│ │
│ [Keep Document] [Delete Document] │
└──────────────────────────────────────────────┘

85. Execute Deletion

Introduce:

delete_document

Input:

{
"mutation_request_id": "..."
}

Execution:

Load MutationRequest
Verify Tenant
Verify User
Verify documents.delete
Verify Version
Verify AVAILABLE
Set DELETED
Set deleted_at
Set deleted_by_user_id
Increment Version
Create AuditEvent
Execute MutationRequest
Commit

86. Document Deletion Audit Event

Use:

action =
document.deleted

The document metadata remains available for audit purposes.


87. Deleted Documents in Search

Normal:

search_documents

must exclude:

status = deleted

unless the caller has an explicit administrative reason to include them.


88. Deleted Document Download

A deleted Document should not be downloadable through the normal content endpoint.

Return an appropriate error.


89. Document Links After Deletion

Do not necessarily delete DocumentLink rows.

They remain useful historical evidence:

This deleted document used to be attached to:
Azure Migration

Normal CRM views simply filter out deleted Documents.


90. Unlinking Documents

Eventually the user may say:

Remove this proposal from the meeting but keep it on the opportunity.

That requires:

document.link.delete

This is distinct from deleting the Document itself.


91. Link Removal

We can include basic unlinking in Part 26 or defer it slightly.

Conceptually:

prepare_document_unlink
delete_document_link

The important semantic distinction is:

Unlink
→ remove CRM relationship
Delete Document
→ remove Document from active use

Never confuse the two.


92. Document Versioning

Suppose we have:

Proposal v1
Proposal v2
Proposal v3

Should these be versions of one Document?

Eventually, perhaps.

But for Part 26:

Each uploaded file =
independent Document

This keeps the domain simple.


93. Why Defer Versioning?

Real document versioning requires decisions about:

Version chains
Current version
Replacement
Retention
Diffs
Rollback
Permissions
AI indexing

Do not build that complexity until the need is real.

The filename can still contain:

v1
v2
v3

without formal version semantics.


94. Document Type

We may optionally introduce a simple classification:

class DocumentType(str, Enum):
GENERAL = "general"
PROPOSAL = "proposal"
CONTRACT = "contract"
REQUIREMENTS = "requirements"
PRESENTATION = "presentation"
OTHER = "other"

But this should not be required for upload.


95. Avoid AI Classification Yet

Do not automatically infer:

This is definitely a contract.

based solely on filename or model interpretation.

For the MVP:

GENERAL

can be the default.

Later document processing can classify content with explicit provenance.


96. Document Metadata Editing

Users may eventually want to change:

display_name
document_type
description

But the core Part 26 requirement is upload, link, retrieve, and delete.

Metadata editing can be added later without redesigning storage.


97. Add Documents to CRM Context

Part 25 created:

CRM Context
├── Contacts
├── Opportunities
├── Activities
├── Tasks
└── Meetings

Part 26 allows us to extend it carefully:

CRM Context
├── Contacts
├── Opportunities
├── Activities
├── Tasks
├── Meetings
└── Documents

But only document metadata is included.


98. Do Not Put Binary Files in CRM Context

Context should contain:

Document ID
Display Name
Type
MIME Type
Relationship
Upload Date

not:

Binary content
Base64 file
Entire PDF
Entire DOCX

That would be inefficient and unnecessary.


99. Company Context Documents

For Adventure Works:

Documents
├── Master Services Agreement
├── Security Questionnaire
└── Account Plan

These may be directly linked to the Company.


100. Opportunity Context Documents

For Azure Migration:

Documents
├── Requirements.pdf
├── Architecture.pptx
├── Proposal-v3.pdf
└── Pricing.xlsx

This is extremely useful contextual metadata even before document AI exists.


101. Context Budget for Documents

Add a bounded default:

documents:
10

or another conservative value.

Again:

Context must remain bounded.


102. Document Ordering

Default:

created_at DESC

or perhaps:

updated_at DESC

For the MVP, use deterministic upload recency.


103. Document Provenance

A Context item should preserve:

entity_type =
document
entity_id =
Document UUID
source =
crm_document_metadata

Later extracted text can reference the same Document.


104. This Prepares Us for RAG

Future architecture:

Document
Text Extraction
Document Text
Chunks
Embeddings
Vector Index
Semantic Retrieval
ChatGPT

But every chunk will still trace back to:

Document ID

That is why building the Document domain correctly first matters.


105. Future Chunk Provenance

Later:

Chunk
├── document_id
├── page_number
├── section
├── text
└── embedding

can provide evidence such as:

Proposal.pdf
Page 14
Pricing Section

This becomes far stronger than untraceable AI context.


106. ChatGPT Document Tools

Part 26 can expose:

get_document
search_documents
create_document_link_draft
prepare_document_link
create_document_link
prepare_document_deletion
delete_document

Binary upload itself may happen through the App UI rather than a model tool.


107. Why Upload May Be UI-Driven

ChatGPT cannot safely invent a local file.

The user must provide the actual file.

Therefore:

User
Upload UI
Quorentra Document API
Document

Then ChatGPT can work with the resulting Document reference.


108. ChatGPT-Native Does Not Mean Model-Only

This is an important principle.

A ChatGPT-native CRM can use:

Conversation
Widgets
File Upload Controls
Confirmation UI
Structured Tools
Backend Services

The language model is the orchestration and reasoning interface.

It does not replace every application component.


109. Document Upload Widget

A Quorentra widget might show:

┌──────────────────────────────────────────────┐
│ QUORENTRA │
│ Upload Document │
│ │
│ Drop a file here │
│ or │
│ [Choose File] │
│ │
│ Supported: PDF, DOCX, XLSX, PPTX, TXT, CSV │
│ Maximum size: 25 MB │
└──────────────────────────────────────────────┘

After upload:

Upload complete
Azure Migration Proposal v3
PDF · 2.4 MB

110. Conversational Continuation

ChatGPT can then ask:

Where should I attach this document?

The user:

To the Azure Migration opportunity.

Quorentra resolves and prepares the link.

This is a natural combination of:

UI
+
Conversation
+
Structured CRM Tools

111. One-Shot Workflow

If the UI already knows the active Opportunity context:

Adventure Works
→ Azure Migration

the user may upload a file directly there.

The application can prepopulate the relationship.

Still, the backend must validate it.


112. Active Context Is Not Authority

Suppose the widget says:

Current Opportunity:
Azure Migration

The server must still verify:

Opportunity exists
belongs to active tenant
user can access it

Never trust UI state as security authority.


113. Document Search from ChatGPT

User:

Find the latest proposal for Adventure Works.

Possible flow:

Resolve Adventure Works
Search Documents
Filter linked Company/Opportunity Documents
Filter document type/name
Order by created_at DESC
Return candidates

No document content understanding is required yet.


114. Search by Filename

Part 26 search can support:

original_filename
display_name
document_type

This is enough for basic retrieval.


115. Do Not Pretend to Search Document Contents

Before text extraction exists, a query like:

Find the document that mentions a 15% discount.

cannot reliably be answered.

Quorentra should not imply that it searched inside files.

That capability belongs to later parts.


116. Grounding Boundary

At Part 26, ChatGPT knows:

A document exists.
Its name.
Its type.
Its CRM relationships.
Its upload metadata.

It does not yet know:

What the document says.

That boundary must remain explicit.


117. Example Correct Answer

User:

Do we have a proposal for Adventure Works?

Quorentra finds:

Azure Migration Proposal v3

Good:

Yes. Quorentra has a document named “Azure Migration Proposal v3” attached to the Azure Migration opportunity.


118. Example Incorrect Answer

Bad:

Yes. The proposal offers Azure migration for €90,000 and includes a six-month delivery schedule.

unless those facts come from structured CRM fields or previously extracted authoritative content.

The existence of a PDF does not mean ChatGPT knows its contents.


119. Upload Tests

Test:

valid PDF
valid DOCX
valid XLSX
valid PPTX
valid TXT
valid CSV
valid PNG
valid JPEG
unsupported executable
oversized file
empty file
invalid extension
MIME mismatch

120. Filename Security Tests

Test filenames such as:

../../secret.pdf
..\..\secret.pdf
proposal<script>.pdf
very-long-filename...
proposal?.pdf

Verify they cannot influence physical storage paths.


121. Storage Key Tests

Verify:

server-generated
tenant-aware
unique
not derived directly from filename
not client-controlled

122. Checksum Test

Upload known content.

Verify:

SHA-256

matches the expected digest.


123. File Size Test

Verify server-calculated:

file_size_bytes

matches actual stored content.


124. Tenant Isolation Upload Test

User in Tenant A uploads a Document.

Expected:

organization_id =
Tenant A

The client cannot override this to Tenant B.


125. Document Read Tenant Test

User in Tenant B requests Tenant A Document ID.

Expected:

not found / forbidden

according to security policy.

No metadata leakage.


126. Document Download Tenant Test

The same rule applies to binary retrieval.

Knowing a Document UUID must never be enough to download it.


127. Permission Tests

Viewer:

get_document
→ allowed
upload_document
→ denied
delete_document
→ denied

Member:

upload_document
→ allowed

according to configured roles.


128. Document Link Tests

Test linking to:

Company
Contact
Opportunity
Activity
Task
Meeting

Each target must:

exist
belong to tenant
be accessible

129. Cross-Tenant Link Test

Document:

Tenant A

Opportunity:

Tenant B

Expected:

DOCUMENT_LINK_TENANT_CONFLICT

No link created.


130. Duplicate Link Test

Link:

Document X
→ Opportunity Y

twice.

Expected:

1 logical DocumentLink

Use a uniqueness constraint where practical.


131. Link Draft Test

Verify:

create draft
ambiguous target
missing target
resolved target
expiry
cancellation
conversion

132. Link Confirmation Test

Before confirmation:

0 DocumentLink rows created

After execution:

1 DocumentLink

133. Link Permission Change Test

Prepare link.

Remove relevant permission.

Execute.

Expected:

denied

because permissions are rechecked.


134. Link Target Change Test

Prepare link.

Delete or invalidate target entity before execution.

Expected:

execution fails safely

No dangling link.


135. Search Document Tests

Verify:

search by display name
search by filename
filter by entity
filter by MIME type
limit results
exclude deleted
tenant isolation
permission filtering

136. Company Document Query Test

Documents directly linked to Adventure Works should appear.

Documents from another Company must not.


137. Opportunity Document Query Test

Documents linked to Azure Migration should appear in the Opportunity document list.


138. Context Integration Test

Company context now includes bounded Document metadata when permitted.

Opportunity context includes linked Documents.

Verify no binary content is inserted into context.


139. Context Permission Test

User has:

crm.context.read

but lacks:

documents.read

Expected:

Documents omitted

The Context Layer cannot bypass Document permissions.


140. Deletion Tests

Verify:

AVAILABLE → DELETED
DELETED → DELETED
handled safely

Deleted Documents disappear from normal search and context.


141. Deletion Concurrency Test

Prepare deletion at:

version = 1

Document changes to:

version = 2

Execute old mutation.

Expected:

VERSION_CONFLICT

142. Download Deleted Document Test

Attempt normal content retrieval after deletion.

Expected:

DOCUMENT_NOT_AVAILABLE

143. Binary Cleanup Failure Test

Simulate:

binary stored successfully
database creation fails
binary cleanup fails

Expected:

upload fails
orphan cleanup failure logged

Do not silently claim success.


144. Storage Failure Test

Simulate:

storage.save()
→ failure

Expected:

no AVAILABLE Document row
no successful upload response

145. Audit Tests

Verify:

document.created
document.link.created
document.deleted

produce appropriate AuditEvents.

Document contents must not be copied into audit logs.


146. Logging

Useful structured fields:

document_id
organization_id
actor_user_id
mime_type
file_extension
file_size_bytes
storage_provider
operation
result
duration_ms
workflow_id
mutation_request_id

Avoid logging:

document binary
full extracted content
signed URLs
storage credentials

147. Metrics

Useful metrics include:

document_uploads_total
document_upload_failures_total
document_upload_bytes_total
document_downloads_total
document_download_failures_total
document_links_created_total
documents_deleted_total
document_storage_failures_total

Later:

document_processing_total
document_extraction_failures_total

will be added.


148. Storage Metrics

Monitor:

total stored bytes
average document size
largest document size
uploads per tenant
storage failures

These become important for SaaS cost control.


149. Database Indexes

Useful indexes include:

organization_id, status, created_at
organization_id, display_name
document_id, entity_type, entity_id
organization_id, entity_type, entity_id

Also consider uniqueness for:

document_id
entity_type
entity_id

within a tenant.


150. Database Migration

Part 26 requires migrations for:

documents
document_links
document_link_drafts

and any new permission seeds.

Verify:

cd backend
alembic upgrade head

Then:

alembic current

The expected revision should be current.


151. Suggested Backend Structure

A modular layout might look like:

backend/app/
├── documents/
│ ├── models.py
│ ├── schemas.py
│ ├── repository.py
│ ├── service.py
│ ├── permissions.py
│ ├── storage.py
│ ├── validation.py
│ └── routes.py
├── document_links/
│ ├── models.py
│ ├── schemas.py
│ ├── repository.py
│ └── service.py
└── context/
└── service.py

Exact organization can follow the conventions already established in Quorentra.


152. Storage Implementations

For example:

documents/storage/
├── base.py
├── local.py
└── factory.py

Later:

s3.py
azure_blob.py
gcs.py

can be added without rewriting the Document service.


153. Configuration

Example environment configuration:

DOCUMENT_STORAGE_PROVIDER=local
DOCUMENT_LOCAL_STORAGE_PATH=./storage/documents
DOCUMENT_MAX_SIZE_MB=25

Later production:

DOCUMENT_STORAGE_PROVIDER=s3

The domain code should remain unchanged.


154. Do Not Put Secrets in Document Records

Cloud storage credentials belong in:

Environment
Secret Manager
Managed Identity
Workload Identity

not:

documents

Document rows contain storage references, never storage credentials.


155. Version Update

Part 26 introduces:

Document Domain
Document Storage
Secure Upload
Document Metadata
Document Links
Authorized Retrieval
Document Search
Document Deletion
Document Auditing
CRM Context Document Metadata
ChatGPT Document Workflows

Update:

app/core/constants.py

from:

APP_VERSION = "0.12.0"

to:

APP_VERSION = "0.13.0"

156. Quorentra 0.13.0

Our modular MVP now looks like:

Platform
├── FastAPI ✓
├── PostgreSQL ✓
├── SQLAlchemy ✓
├── Alembic ✓
└── Document Storage Abstraction ✓
Identity
├── Organizations ✓
├── Users ✓
├── Memberships ✓
├── Authentication ✓
└── JWT ✓
Security
├── TenantContext ✓
├── Tenant Isolation ✓
├── RBAC ✓
├── Domain Permissions ✓
├── Context-Aware Authorization ✓
└── Document Authorization ✓
CRM
├── Companies ✓
├── Contacts ✓
├── Opportunities ✓
├── Activities ✓
├── Tasks ✓
├── Meetings ✓
└── Documents ✓
Documents
├── Metadata ✓
├── Binary Storage ✓
├── Storage Abstraction ✓
├── Local Storage ✓
├── File Validation ✓
├── Size Limits ✓
├── MIME Validation ✓
├── Checksums ✓
├── CRM Links ✓
├── Authorized Retrieval ✓
├── Search ✓
├── Soft Deletion ✓
└── Audit Events ✓
Document Relationships
├── Company ✓
├── Contact ✓
├── Opportunity ✓
├── Activity ✓
├── Task ✓
└── Meeting ✓
CRM Retrieval
├── Unified CRM Search ✓
├── Tenant-Scoped Search ✓
├── Permission-Aware Search ✓
├── Document Search ✓
└── Bounded Results ✓
CRM Context Layer
├── Company Context ✓
├── Contact Context ✓
├── Opportunity Context ✓
├── Activities ✓
├── Tasks ✓
├── Meetings ✓
├── Document Metadata ✓
├── Context Budgets ✓
├── Context Provenance ✓
└── Grounded CRM Evidence ✓
Mutation Governance
├── Drafts ✓
├── Mutation Requests ✓
├── Confirmation ✓
├── Expiry ✓
├── Idempotency ✓
├── Transactions ✓
├── Optimistic Concurrency ✓
└── Audit Events ✓
ChatGPT CRM Operations
├── Search CRM ✓
├── Retrieve CRM Context ✓
├── Read CRM Records ✓
├── Create CRM Records ✓
├── Update CRM State ✓
├── Schedule Meetings ✓
├── Upload Document ✓
├── Find Documents ✓
├── Attach Documents ✓
└── Delete Documents ✓
ChatGPT UI
├── CRM Context Widgets ✓
├── Mutation Confirmation ✓
├── Task Widgets ✓
├── Meeting Widgets ✓
├── Document Upload ✓
├── Document List ✓
├── Document Detail ✓
└── Document Link Confirmation ✓
Grounding
├── Structured CRM Evidence ✓
├── Document Metadata Evidence ✓
├── Evidence Provenance ✓
├── Context Freshness ✓
└── Fact / Analysis Separation ✓
Document Intelligence
├── Text Extraction -
├── OCR -
├── Content Normalization -
├── Chunking -
├── Embeddings -
├── Vector Search -
└── RAG -
AI Intelligence -

157. Acceptance Criteria

Part 26 is complete when:

✓ Part 25 regression suite remains green
✓ documents.read exists
✓ documents.create exists
✓ documents.delete exists
✓ document permissions are server-enforced
✓ Document model exists
✓ DocumentLink model exists
✓ DocumentLinkDraft exists
✓ migrations apply successfully
✓ every Document is tenant-scoped
✓ every DocumentLink is tenant-scoped
✓ every DocumentLinkDraft is tenant-scoped
✓ original_filename exists
✓ display_name exists
✓ MIME type exists
✓ file extension exists
✓ file size exists
✓ SHA-256 checksum exists
✓ storage provider exists
✓ storage key exists
✓ uploaded_by_user_id exists
✓ version exists
✓ AVAILABLE status exists
✓ DELETED status exists
✓ new Documents become AVAILABLE only after successful upload
✓ DocumentStorage abstraction exists
✓ LocalDocumentStorage exists
✓ domain logic does not depend directly on local filesystem paths
✓ storage keys are server-generated
✓ storage keys are not user-controlled
✓ original filenames cannot control storage paths
✓ maximum upload size is configurable
✓ oversized uploads are rejected
✓ empty uploads are rejected
✓ supported file types use an allowlist
✓ unsupported executables are rejected
✓ archive files are excluded unless explicitly supported
✓ MIME validation exists
✓ extension validation exists
✓ server verifies file size
✓ SHA-256 is calculated server-side
✓ uploads are streamed where practical
✓ large uploads are not unnecessarily loaded fully into memory
✓ storage failure does not create a successful Document
✓ database failure after storage triggers best-effort binary cleanup
✓ orphan cleanup failures are logged
✓ clients cannot provide organization_id
✓ clients cannot provide uploaded_by_user_id
✓ clients cannot provide authoritative status
✓ clients cannot provide storage_key
✓ clients cannot provide authoritative checksum
✓ DocumentLink supports Company
✓ DocumentLink supports Contact
✓ DocumentLink supports Opportunity
✓ DocumentLink supports Activity
✓ DocumentLink supports Task
✓ DocumentLink supports Meeting
✓ target entity must exist
✓ target entity must belong to active tenant
✓ target entity permission is checked
✓ cross-tenant DocumentLinks are rejected
✓ duplicate links are prevented
✓ create_document_link_draft exists
✓ ambiguous targets require clarification
✓ missing targets do not create links
✓ Link Draft supports expiry
✓ Link Draft supports cancellation
✓ Link Draft supports conversion
✓ Link Draft supports workflow_id
✓ prepare_document_link exists
✓ preparation creates no DocumentLink
✓ document.link.create MutationRequest exists
✓ link confirmation widget exists
✓ create_document_link accepts mutation_request_id only
✓ execution reloads Document
✓ execution reloads target entity
✓ execution rechecks tenant
✓ execution rechecks permissions
✓ execution rechecks duplicate relationship
✓ successful execution creates DocumentLink
✓ successful execution creates AuditEvent
✓ successful execution executes MutationRequest
✓ successful execution converts Link Draft
✓ get_document exists
✓ Document metadata retrieval is tenant-scoped
✓ Document metadata retrieval is permission-aware
✓ raw filesystem paths are never exposed
✓ cloud credentials are never exposed
✓ search_documents exists
✓ search by display name works
✓ search by filename works
✓ entity filtering works
✓ MIME filtering works
✓ date filtering works
✓ search results are bounded
✓ deleted Documents are excluded by default
✓ Company Documents can be retrieved
✓ Contact Documents can be retrieved
✓ Opportunity Documents can be retrieved
✓ Activity Documents can be retrieved
✓ Task Documents can be retrieved
✓ Meeting Documents can be retrieved
✓ authorized content endpoint exists
✓ content endpoint requires authentication
✓ content endpoint verifies tenant
✓ content endpoint verifies permission
✓ storage path cannot be supplied by caller
✓ binary response is streamed
✓ Content-Disposition uses a safe filename
✓ deleted Documents cannot be downloaded normally
✓ predictable public document URLs do not exist
✓ prepare_document_deletion exists
✓ deletion requires confirmation
✓ deletion uses optimistic concurrency
✓ deletion sets DELETED
✓ deleted_at is recorded
✓ deleted_by_user_id is recorded
✓ version increments
✓ document.deleted AuditEvent exists
✓ soft deletion preserves Document metadata
✓ normal search excludes deleted Documents
✓ normal CRM context excludes deleted Documents
✓ unlinking is semantically distinct from deleting
✓ architecture can support document.link.delete
✓ each uploaded file is an independent Document
✓ formal document versioning is not required yet
✓ Document metadata can appear in CRM Context
✓ binary content never appears in CRM Context
✓ Document Context results are bounded
✓ Document context respects documents.read
✓ Document provenance retains Document ID
✓ ChatGPT can find Documents by metadata
✓ ChatGPT can attach an uploaded Document to a resolved CRM entity
✓ ChatGPT cannot pretend to search inside document contents
✓ ChatGPT cannot claim document facts that have not been extracted or otherwise provided
✓ document.created AuditEvent exists
✓ document.link.created AuditEvent exists
✓ document.deleted AuditEvent exists
✓ audit logs do not contain document binary contents
✓ document operational metrics exist
✓ storage metrics exist
✓ upload failures are observable
✓ download failures are observable
✓ Quorentra reports version 0.13.0

Most importantly:

Quorentra can now securely accept customer files, store them independently of the relational database, associate them with authoritative CRM entities, expose them only through tenant- and permission-aware retrieval, and let ChatGPT work with those document relationships without pretending to understand document contents that have not yet been processed.


158. What We Have Achieved

The user can now take:

AdventureWorks-Azure-Migration-Proposal-v3.pdf

and say:

Attach this proposal to the Adventure Works Azure Migration opportunity.

The workflow becomes:

User Upload
File Validation
Secure Storage
Document Metadata
Document Created
ChatGPT
Resolve Adventure Works
Resolve Azure Migration
Document Link Draft
Relationship Validation
Preparation
Confirmation
DocumentLink
Audit

This is another important step toward the complete MVP.


159. The Opportunity Workspace Is Becoming Richer

Our Azure Migration Opportunity can now look like:

Azure Migration
Adventure Works
€90,000
Proposal
60%
├── Contacts
│ ├── Sarah Johnson
│ └── Lisa Chen
├── Activities
│ ├── Discovery Call
│ ├── Requirements Meeting
│ └── Pricing Discussion
├── Tasks
│ ├── Send revised pricing
│ └── Update architecture diagram
├── Meetings
│ └── Proposal Review
└── Documents
├── Requirements.pdf
├── Azure-Architecture.pptx
├── Pricing-v3.xlsx
└── Proposal-v3.pdf

That is starting to resemble a genuine customer workspace rather than a collection of isolated CRM records.


160. But ChatGPT Still Cannot Read the Documents

This boundary is intentional.

At the end of Part 26, ChatGPT can know:

Proposal-v3.pdf exists.
It belongs to:
Adventure Works
and
Azure Migration.
It was uploaded:
2 August 2026.
Its size:
2.4 MB.
Its type:
PDF.

But ChatGPT cannot yet reliably answer:

What does the proposal say about pricing?

because we have not extracted the document’s content.


161. The Next Problem

Documents are binary containers.

Before we can use them as AI knowledge, we need:

PDF
DOCX
PPTX
TXT
Text

That sounds simple.

It is not.

Different document formats require different extraction strategies.

PDFs may contain:

Digital text
Scanned pages
Tables
Headers
Footers
Images
Multi-column layouts

DOCX files contain structured XML.

PPTX files contain slide-based text.

Some files may fail extraction entirely.

Therefore document extraction deserves its own module.


162. Keep Extraction Separate from Upload

Do not make:

Upload

synchronously perform:

Extract
Chunk
Embed
Index

Otherwise a simple upload becomes coupled to the entire AI pipeline.

Instead:

Upload
Document AVAILABLE

and later:

Document
Processing Pipeline

This preserves our modular architecture.


163. The Future Document Pipeline

We are heading toward:

Document Upload
Secure Storage
Document Metadata
Text Extraction
Normalization
Chunking
Embeddings
Vector Storage
Hybrid Retrieval
CRM Context
ChatGPT

But each layer should be independently testable.


164. Why Extraction Comes Next

Before embeddings, we need trustworthy text.

Before chunking, we need normalized text.

Before RAG, we need traceable chunks.

Therefore the correct next step is not:

Vector Database

It is:

Document Text Extraction

165. Next Article

In Part 27, we will build:

Building the Document Text Extraction Pipeline — PDF, DOCX, PPTX, TXT, Extraction Jobs, Normalization, Provenance, and Failure Handling

We will introduce:

documents.process
DocumentProcessingJob
Processing Status
PENDING
PROCESSING
COMPLETED
FAILED
DocumentExtractor
PDFExtractor
DOCXExtractor
PPTXExtractor
TXTExtractor
CSV Extraction Strategy
Extraction Router
ExtractedDocument
ExtractedPage
ExtractedSection
Raw Extracted Text
Normalized Text
Page Numbers
Slide Numbers
Paragraph Boundaries
Source Provenance
Character Counts
Extraction Metadata
Extractor Version
Processing Errors
Retry Policy
Idempotent Processing
Asynchronous Processing Boundary
Worker Architecture
Document Processing Service
ChatGPT Processing Status
Processing Metrics
Extraction Tests

The target flow will become:

Proposal-v3.pdf
DocumentProcessingJob
PDFExtractor
Extracted Pages
Normalized Text
Stored Extraction
Document
└── Searchable Text Foundation

Then a future user question:

What does the Adventure Works proposal say about pricing?

will eventually flow through:

Question
CRM Context
+
Document Retrieval
Relevant Extracted Content
Grounded ChatGPT Answer

But we will not jump there yet.

Part 27 first gives Quorentra a reliable way to transform an authoritative CRM Document into traceable, normalized text.

That is the next layer in Quorentra — A Modular, ChatGPT-Native AI CRM.

Quorentra

Contact

Menu

Designed with WordPress

Discover more from Quorentra

Subscribe now to keep reading and get access to the full archive.

Continue reading