S3 Uploads
Presigned URL upload flow for document files.
HyperSaaS uses presigned S3 URLs so file uploads go directly from the client to S3, bypassing the Django server entirely.
Upload Flow
┌──────────┐ ┌──────────┐ ┌─────┐
│ Client │ │ Django │ │ S3 │
└────┬─────┘ └────┬─────┘ └──┬──┘
│ │ │
│ 1. POST /upload/ │ │
│ {filename, size} │ │
│────────────────────►│ │
│ │ │
│ 2. Presigned PUT │ │
│ URL + Document ID │ │
│◄────────────────────│ │
│ │ │
│ 3. PUT file ───────┼──────────────────►│
│ (direct to S3) │ │
│ ◄─────────────────────────── 200 OK ───│
│ │ │
│ 4. POST /confirm/ │ │
│────────────────────►│ │
│ │── Celery task ───►│
│ 5. {task_id} │ (ingestion) │
│◄────────────────────│ │Step 1: Request Upload URL
POST /api/workspaces/{workspace_id}/documents/upload/{
"filename": "product-guide.pdf",
"content_type": "application/pdf",
"file_size": 1234567,
"name": "Product Guide",
"description": "Internal product documentation",
"knowledge_base_ids": ["uuid1"]
}Validations:
- Filename: required, max 500 chars
- File size: min 1 byte, max 50MB (configurable via
DOCUMENT_MAX_UPLOAD_SIZE) - Extension: must be in
DOCUMENT_ALLOWED_EXTENSIONS
Step 2: Receive Presigned URL
{
"document_id": "uuid",
"upload_url": "https://bucket.s3.region.amazonaws.com/documents/ws-id/uuid/product-guide.pdf?X-Amz-...",
"s3_key": "documents/ws-id/uuid/product-guide.pdf"
}The server creates a Document record with processing_status="pending" and generates a presigned PUT URL valid for 1 hour.
Step 3: Upload to S3
The client uploads the file directly to S3 using the presigned URL:
await fetch(uploadUrl, {
method: "PUT",
body: file,
headers: { "Content-Type": contentType },
});Step 4: Confirm Upload
POST /api/workspaces/{workspace_id}/documents/{document_id}/confirm-upload/This triggers the Celery ingestion task. The document status changes to "processing".
Step 5: Poll Status
GET /api/workspaces/{workspace_id}/documents/{document_id}/processing-status/Poll until status is SUCCESS or FAILURE.
S3 Key Generation
Keys follow a predictable pattern ensuring uniqueness:
{prefix}/{workspace_id}/{uuid}/{sanitized_filename}Example: documents/ws-abc123/doc-def456/product-guide.pdf
The UUID ensures that duplicate filenames within the same workspace don't collide.
Download URLs
Generate a temporary download URL for the original file:
GET /api/workspaces/{workspace_id}/documents/{document_id}/download-url/{
"download_url": "https://bucket.s3.region.amazonaws.com/documents/...?X-Amz-..."
}Presigned GET URLs expire after 1 hour (configurable via DOCUMENT_PRESIGNED_URL_EXPIRY).
Deletion
When a document is deleted via the API:
- All
DocumentChunkrows are cascade-deleted - The S3 object is deleted (gracefully handles S3 errors)
- The
Documentrecord is removed
Configuration
| Setting | Default | Description |
|---|---|---|
AWS_ACCESS_KEY_ID | — | AWS credentials |
AWS_SECRET_ACCESS_KEY | — | AWS credentials |
AWS_STORAGE_BUCKET_NAME | — | S3 bucket name |
AWS_S3_REGION_NAME | — | AWS region |
DOCUMENT_S3_PREFIX | documents | Key prefix in bucket |
DOCUMENT_PRESIGNED_URL_EXPIRY | 3600 | URL TTL in seconds |
DOCUMENT_MAX_UPLOAD_SIZE | 52428800 | Max file size (50MB) |
DOCUMENT_ALLOWED_EXTENSIONS | pdf,docx,doc,txt,csv,md,pptx,xlsx | Allowed types |
If AWS credentials are not configured, the upload endpoint raises S3NotConfiguredError.