Skip to content

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:

  1. File received by the API
  2. MD5 checksum computed for integrity verification
  3. For PDFs: title metadata is stripped and replaced with a UUID (prevents metadata leakage)
  4. File uploaded to S3 with retry logic (up to 3 attempts, exponential backoff)
  5. TransactionFiles record created in the database with the S3 path
  6. 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:

FieldPurpose
transaction_file_idUnique identifier
case_idThe case this document belongs to
titleDisplay name of the document
pathS3 key (path within the bucket)
uploaded_by_id + uploaded_by_typeWho uploaded it
document_groupCategory (e.g., DR attachment, contract, identity doc)
is_contractWhether this is a contract document
disbursement_request_idLink to a DR if this is a DR attachment
viewable_listAccess control (see below)
notesOptional 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_id are 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, OR
  • signed_contract_version < case.contract.version

Access by Role

RoleWhat They See
Admin / CM / Agency OwnerBoth IP and Surrogate versions + all signature dates
Intended ParentOnly their version, other IP signatures hidden
SurrogateOnly their version

Code: seedtrustapi/src/seedtrust/modules/case/escrow_agreement/


File Deletion

Deleting a file requires two steps:

  1. Delete the file from S3 via s3_delete_file()
  2. Delete the TransactionFiles record 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?