PMS Partner Upload Portal
Technical Solution Design
Open-source architecture for vendor submissions, antivirus scanning, protected storage and controlled Public Tender opening.
Odoo 14FastAPIMinIOClamAVReact
1. Business Need
Secure submission without exposing tender files
The system must let vendors submit quotations while preventing internal preview before the official Public Tender opening.
External requirement
- Unique access: one submission link per shortlisted vendor.
- Controlled deadline: upload and resubmit only before lock.
- Version history: every submission remains traceable.
- Safe upload: all files pass antivirus scanning.
Internal requirement
- No preview: Public Tender files remain hidden before opening.
- Key-controlled opening: only the CRV Key Custodian can unlock.
- Server-side enforcement: direct URLs must also be blocked.
- Auditability: all actions and downloads are logged.
2. Solution Overview
Existing ERP integrated with a dedicated portal and storage tier
Existing Odoo 14 ServerTender workflow, vendor invitation, deadline and internal roles; retained as the current ERP platform
↓
FastAPIUpload orchestration, opening key and download authorization
↓
ClamAVMalware scanning before approval
↓
Dedicated MinIO Storage ServerSecure private object storage accessed by FastAPI through the internal network
↓
PostgreSQLPortal metadata, security state and audit history
Key principle
The existing Odoo server remains unchanged. The portal application runs on a dedicated server, while MinIO runs on a separate private storage server.
Odoo controls business context. FastAPI controls file access.
All software components are self-hosted and open source.
3. Technology Stack
Three isolated infrastructure tiers
Odoo remains on the existing ERP server. Portal services run on a dedicated application server, while MinIO is isolated on a private storage server.
Existing Odoo 14 Server
ERP
Odoo 14Tender workflow, vendor invitations, deadlines, permissions and internal actions.
PX
Existing NginxHTTPS and reverse proxy for the current Odoo environment.
Portal Application Server
PX
NginxHTTPS, React delivery, FastAPI reverse proxy, upload limits and rate limiting.
UI
ReactVendor upload pages, progress display and submission status.
API
FastAPIPortal sessions, uploads, scan orchestration, opening key and controlled downloads.
DB
PostgreSQLPortal metadata, submission versions, opening state and audit records.
AV
ClamAVMalware scanning before files are approved.
Private Storage Server
OBJ
MinIOSecure private object storage for tender documents throughout the upload and antivirus scanning lifecycle.
FastAPI accesses the MinIO API through its internal IP. MinIO is not exposed to the public internet.
4. Security Model
Two credentials for two different risks
Vendor Submission Token
Allows one external vendor to access only its own upload page.
- Generated by Odoo
- Bound to one invitation
- Expires and can be revoked
- Does not open internal files
Bid Opening Key
Allows the CRV Key Custodian to unlock internal access after submissions are locked.
- Generated by FastAPI
- Stored as a secure hash
- Versioned and regeneratable
- Does not give vendor portal access
| Credential | User | Owner | Protects | When used |
| Vendor Submission Token | Vendor | Odoo | One upload page | Before deadline |
| Bid Opening Key | CRV Key Custodian | FastAPI | Internal Public Tender downloads | After submission lock |
5. Vendor Submission Token
Odoo owns the vendor access token
PPIC generates link
Odoo
→
Vendor opens link
Opaque token
→
FastAPI validates
with Odoo
→
Portal session
short-lived
Token scope
- One Tender Request
- One vendor invitation line
- One upload flow
- No general Odoo API access
Validation rules
- Token hash matches latest version
- Token is not revoked
- Deadline has not passed
- Submission status is Open
https://upload.crv.vn/s/<opaque-token>
6. Bid Opening Key
FastAPI owns the tender opening key
Submissions locked
→
FastAPI generates key
→
CRV receives key
→
Valid key entered
→
Tender unlocked
Stored in FastAPI PostgreSQL
- Opening key hash
- Key version and status
- Generation and send timestamps
- Failed attempts and lockout
Security behavior
- Old key invalid after regeneration
- Only latest key can unlock
- Unlock actor and time are audited
- FastAPI remains final download gate
The key changes application state from LOCKED to UNLOCKED. It does not expose MinIO directly.
7. Vendor Upload Flow
Every file passes through quarantine
Vendor upload
→
FastAPI stream
→
MinIO quarantine
→
ClamAV scan
→
Approved
UPLOADINGQUARANTINEDSCANNINGCLEANAPPROVEDREJECTED
File validation
- Maximum file size
- Allowed extension
- MIME type and file signature
- Safe filename and object key
Submission result
- Create immutable submission version
- Latest version becomes Final
- Older versions remain available in history
- Update status back to Odoo
8. Public Tender Opening
Locked until the official opening event
Vendor submits
→
Deadline reached
→
Submissions locked
→
Opening key sent
→
Key validated
→
Files available
Before unlock
- Only metadata is visible
- Final Files button hidden
- Version History hidden
- Direct download denied by FastAPI
After unlock
- Authorized internal groups see actions
- FastAPI validates every download
- Files stream from MinIO
- Every access is audited
9. Controlled Download
FastAPI is the final enforcement point
Authorization checks
- Signed request from Odoo
- Authorized Odoo user and tender
- File belongs to requested submission
- Public Tender status is UNLOCKED
File checks
- Scan status is CLEAN
- Availability is APPROVED
- Request timestamp is valid
- Nonce has not been replayed
Odoo user
→
Signed FastAPI request
→
MinIO GetObject
→
Stream to browser
No permanent public MinIO URL. Prefer direct streaming through FastAPI.
10. MinIO Storage
Three private buckets, one service account
pms-quarantine
New uploads, incomplete scans and retryable scan failures.
pms-approved
Clean files that may be downloaded only through FastAPI.
pms-rejected
Malware detections and blocked files retained per policy.
production/{tender_uuid}/{invitation_uuid}/{submission_version}/{file_uuid}
MinIO runs on a dedicated private storage server. Only the FastAPI service account can access it through the internal MinIO API.
FastAPI ── Private Network ──► http://<MINIO_INTERNAL_IP>:9000
11. ClamAV
Fail closed when scanning is uncertain
clamd
- Persistent scanning daemon
- FastAPI uses INSTREAM
- Private application network
- Health and timeout monitoring
freshclam
- Updates malware signatures
- Track last successful update
- Alert when signatures are stale
- Update without exposing service publicly
Quarantine object
→
Stream to clamd
→
Clean
or
Blocked
Scanner error, timeout or unknown result must remain blocked.
12. PostgreSQL
Portal state and audit model
Business records
- upload_package — tender, invitation, vendor, deadline and state
- submission_version — version number and Final pointer
- upload_file — object key, hash, size and scan result
Security records
- bid_opening_key — key hash, version and status
- download_access_log — user, file, result and time
- audit_event — append-only workflow history
The clear vendor token and clear Bid Opening Key are never stored permanently.
13. API Contract
Signed integration between Odoo and FastAPI
Core endpoints
- Validate vendor token
- Create portal session
- Upload and submit files
- Generate, regenerate and validate opening key
- Download approved file
Request protection
- HMAC signature
- Timestamp validation
- Nonce replay protection
- Request ID for traceability
- Private network or allowlisted source
X-Odoo-Timestamp
X-Odoo-Nonce
X-Odoo-Signature
X-Request-ID
14. Odoo Module
Business controls remain inside Odoo
Invitation
Generate link, send, resend, regenerate and revoke.
Deadline
Maintain deadline and automatically lock submissions.
Status
Display submission, scan, lock, key and unlock states.
Opening Key
Generate, resend, regenerate and enter the key.
Files
Final Files, Version History and controlled download.
Security
Groups, record rules and direct URL protection.
Vendor quotation files must not be copied into normal Odoo attachments or chatter.
15. React Portal
Simple vendor-facing experience
Access
Validate token and show tender reference and deadline.
Upload
Drag and drop, validation, progress, remove and submit.
Success
Version number, submission time and file status.
Expired
Deadline reached or access token expired.
Revoked
Previous token invalidated after regeneration.
Error
Safe messages without infrastructure details.
16. Infrastructure Setup
Three separate server instances
Odoo remains on the existing server. The portal application runs on a dedicated server, and MinIO runs on an isolated private storage server.
Server 1 — Existing Odoo 14
- Existing Nginx: HTTPS and reverse proxy for Odoo.
- Odoo 14: tender workflow, invitations, deadlines and internal permissions.
- Current Odoo database: existing ERP business data remains unchanged.
- Integration: signed HTTPS requests to FastAPI.
Server 2 — Portal Application
- Nginx: public HTTPS gateway for the vendor portal and API.
- React: vendor-facing upload interface.
- FastAPI: upload, security, opening key and download services.
- PostgreSQL: portal metadata and audit database.
- ClamAV: antivirus scanning service.
Server 3 — Private MinIO Storage
- MinIO API: available only on the internal network.
- Port 9000: allowlisted only for the Portal Server internal IP.
- Port 9001: restricted to administrators through VPN or a management network.
- Buckets: quarantine, approved and rejected.
- Access: dedicated least-privilege FastAPI service account.
Internet
│
├────────► Existing Odoo Domain ──► Odoo Nginx ──► Odoo 14
│
└────────► Portal Domain ─────────► Portal Nginx
├── React
└── FastAPI
├── PostgreSQL
├── ClamAV
└── Private Network ──► MinIO Internal IP:9000
Odoo 14 ◄──────────── Signed HTTPS API ────────────► FastAPI
FastAPI ───────── Private MinIO API / Internal IP ─────────► MinIO
MinIO has no public route. Vendor and Odoo users can access files only through FastAPI authorization and streaming.
17. Production Security
Mandatory controls
- HTTPS only
- Opaque vendor access token
- Argon2id or bcrypt opening key hash
- Short-lived secure portal session
- Rate limiting and failed-key lockout
- Filename, MIME and signature validation
- Private Application-to-Storage network
- Least-privilege service accounts
- HMAC-signed Odoo requests
- Nonce replay protection
- Append-only audit events
- Backup, restore and monitoring tests
18. Implementation Roadmap
Deliver security foundations first
1
Infrastructure foundation
Dedicated Portal Application Server plus a private MinIO Storage Server, with internal network routing, firewall rules, systemd and CI.
Running platform
2
Vendor access
Odoo token generation, validation and React portal session.
Secure portal entry
3
Upload and scanning
Streaming, quarantine, ClamAV and approved/rejected lifecycle.
Safe file intake
4
Versioning and Odoo status
Submission history, Final pointer and synchronization.
Business visibility
5
Bid opening and download
Opening key, unlock state, FastAPI download gate and audit.
Public Tender security
Summary
Four design decisions
Odoo owns vendor accessThe vendor token is scoped to one invitation and validated against the live business state.
FastAPI owns file accessUploads, scans, opening key validation and downloads are enforced outside Odoo attachments.
MinIO remains privateOnly FastAPI can access objects; users never receive permanent storage credentials.
Two keys solve two risksThe vendor token protects upload entry. The Bid Opening Key protects Public Tender opening.
Secure upload before deadline. Controlled access after official opening.
PMS Partner Upload Portal
Questions?
Next step: confirm the security ownership, integration contract and production infrastructure.
ScopeSecurityIntegrationDelivery