Document Management
SeedTrust handles legally significant documents: escrow agreements, DR receipts, identity verification, lost wages documentation, and general uploads. All files are stored in AWS S3 and referenced in the database. Files are never served directly — they are always accessed via time-limited signed URLs.
How Files Are Stored
Files live in AWS S3. The database stores only metadata and the S3 path — never the file content itself.
Flow for any file upload:
- File received by the API
- MD5 checksum computed for integrity verification
- For PDFs: title metadata is stripped and replaced with a UUID (prevents metadata leakage)
- File uploaded to S3 with retry logic (up to 3 attempts, exponential backoff)
TransactionFilesrecord created in the database with the S3 path- A thumbnail is generated and uploaded for PDFs where possible
Code:
seedtrustapi/src/seedtrust/core/aws.py
File Size and Type Limits
- Maximum file size: 100 MB per file
- Accepted types: All standard document, image, media, and archive formats (JPEG, PNG, PDF, DOCX, XLSX, MP4, ZIP, and 600+ more)
Code:
seedtrust_flask/seedtrust/mimetypes.py
Accessing Files — Signed URLs
Files are never publicly accessible. Every file access requires a signed URL generated at request time.
- Method: AWS S3 presigned URL (S3v4 signature)
- Expiry: 1 hour (3600 seconds)
- Implication: Never store or hard-code a file URL. Always fetch a fresh URL from the API before displaying or downloading a file. Cached URLs will stop working after one hour.
For pages that display multiple files, the API generates all signed URLs in a single batch call (batch_generate_presigned_urls) to avoid round-trip overhead.
The TransactionFiles Model
Every document in the system is tracked by a TransactionFiles record:
| Field | Purpose |
|---|---|
transaction_file_id | Unique identifier |
case_id | The case this document belongs to |
title | Display name of the document |
path | S3 key (path within the bucket) |
uploaded_by_id + uploaded_by_type | Who uploaded it |
document_group | Category (e.g., DR attachment, contract, identity doc) |
is_contract | Whether this is a contract document |
disbursement_request_id | Link to a DR if this is a DR attachment |
viewable_list | Access control (see below) |
notes | Optional notes |
Code:
seedtrust_flask/seedtrust/models/files.py
Access Control
Who can see a document is controlled by the viewable_list field — a comma-separated string of user_id:user_type pairs.
- When a user is added to a case, the
add_viewable()method grants them access to existing case documents - New documents inherit access from the case’s current parties
- Special rule: If a case is redacted and the viewer is an IP or IP Rep, documents linked to a
dr_batch_idare hidden
To check if a user can view a file, search for their user_id:user_type pair in viewable_list.
Escrow Agreement Signing
The escrow agreement is a special document with its own signing flow.
How It Works
Admin assigns contract template to case →IP and Surrogate each receive a signing request →
Party opens escrow agreement: GET /api/cases/{case_id}/escrow-agreement → Contract text returned with party-specific variable substitution ({{agency_name}}, {{surrogate_name}}, {{ip_name}}) → Each party sees only their version
Party signs: PUT /api/cases/{case_id}/escrow-agreement/sign → escrow_signed = timestamp → escrow_ip = client IP address (from x-forwarded-for header) → escrow_signature = base64-encoded signature → Full contract text saved as signed → ContractSignatureHistory record created (permanent audit trail)Re-signature
If the contract template is updated after parties have already signed, needs_resignature = True is set on their record. They must sign again before payment activity can proceed.
A party’s signature is considered outdated when:
signed_contract_id != case.escrow_agreement_id, ORsigned_contract_version < case.contract.version
Access by Role
| Role | What They See |
|---|---|
| Admin / CM / Agency Owner | Both IP and Surrogate versions + all signature dates |
| Intended Parent | Only their version, other IP signatures hidden |
| Surrogate | Only their version |
Code:
seedtrustapi/src/seedtrust/modules/case/escrow_agreement/
File Deletion
Deleting a file requires two steps:
- Delete the file from S3 via
s3_delete_file() - Delete the
TransactionFilesrecord from the database
Thumbnails are stored as a separate S3 object ({filename}_thumb.png) and must be deleted separately.
There is no soft-delete for files — deletion is permanent.
Gotchas for Developers
Signed URLs expire after 1 hour. If your feature displays a document URL that was fetched more than an hour ago, it will return a 403. Always regenerate URLs on page load or user action — never cache them across sessions.
PDF metadata is stripped on upload. The PDF title field is replaced with a UUID. Do not rely on the PDF’s internal metadata for display purposes — use the TransactionFiles.title field from the database.
The viewable_list field is a string, not a relation. Access control is not enforced by the database. If you add a new party to a case, you must also call add_viewable() on existing documents — it is not automatic.
File existence in S3 can be checked in batch. batch_check_files_exist() verifies multiple files at once. Use this before displaying a list of documents to catch any files that were deleted from S3 but still have database records.
Open Questions
1. Document categories
The document_group field categorizes documents, but the allowed values are not enumerated in code. What are the valid document_group values and when is each used?
2. Signed URL length 1 hour is the current expiry. Are there cases (e.g., long form completion flows) where 1 hour is insufficient, and is there a plan to extend or refresh URLs mid-session?