Storage API
Heimdall provides S3-compatible storage for file uploads, downloads, and management. It supports any S3-compatible provider including AWS S3, MinIO, Cloudflare R2, and DigitalOcean Spaces.
Overview
The Storage API allows users to upload, download, and manage files with configurable validation rules per category. It supports presigned URLs for direct browser uploads/downloads and includes full audit logging.
Key Features
- S3-compatible - Works with any S3-compatible storage provider
- Presigned URLs - Generate upload/download URLs for direct browser access
- Category-based limits - Configurable file size and MIME type restrictions per category
- Permission-based access - RBAC permissions for fine-grained control
- Audit logging - All file operations are logged with user and request context
Required Permissions
Storage operations require specific RBAC permissions:
| Permission | Description |
|---|---|
storage:read | View storage files, metadata, and list files |
storage:write | Upload files and get presigned upload URLs |
storage:edit | Modify file metadata (rename, move, update metadata) |
storage:download | Download files and get presigned download URLs |
storage:delete | Delete files from storage |
Upload Categories
Files are organized into categories with configurable limits:
| Category | Default Max Size | Default Allowed Types |
|---|---|---|
images | 5 MB | image/jpeg, image/png, image/webp, image/gif |
documents | 25 MB | application/pdf, text/plain, application/json |
videos | 500 MB | video/mp4, video/webm, video/quicktime, video/x-msvideo |
general | 100 MB | / (all types) |
These limits are configurable in the API configuration.
REST API Endpoints
Get Storage Limits
Get the upload limits for all categories.
GET /v1/storage/limits
Authorization: Bearer YOUR_TOKEN
Response:
{
"images": {
"max_size_mb": 5,
"allowed_types": ["image/jpeg", "image/png", "image/webp", "image/gif"]
},
"documents": {
"max_size_mb": 25,
"allowed_types": ["application/pdf", "text/plain", "application/json"]
},
"videos": {
"max_size_mb": 500,
"allowed_types": ["video/mp4", "video/webm", "video/quicktime", "video/x-msvideo"]
},
"general": {
"max_size_mb": 100,
"allowed_types": ["*/*"]
}
}
Upload File
Upload a file directly to storage.
POST /v1/storage/upload
Authorization: Bearer YOUR_TOKEN
Content-Type: multipart/form-data
Form Fields:
| Field | Type | Required | Description |
|---|---|---|---|
file | file | Yes | The file to upload |
category | string | Yes | Category: "images", "documents", "videos", or "general" |
key | string | No | Custom storage key (auto-generated if not provided) |
Response:
{
"key": "images/user123/profile.jpg",
"url": "https://storage.example.com/images/user123/profile.jpg",
"content_type": "image/jpeg",
"size_bytes": 245632,
"etag": "\"d41d8cd98f00b204e9800998ecf8427e\""
}
Get Presigned Upload URL
Get a presigned URL for direct browser upload.
POST /v1/storage/upload/presigned
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json
Request Body:
{
"filename": "profile.jpg",
"content_type": "image/jpeg",
"category": "images"
}
Response:
{
"url": "https://storage.example.com/images/user123/abc123.jpg?X-Amz-Signature=...",
"key": "images/user123/abc123.jpg",
"expires_at": "2026-01-24T12:00:00Z"
}
Download File
Download a file from storage.
GET /v1/storage/download/{key}
Authorization: Bearer YOUR_TOKEN
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
key | string | The storage key (URL-encoded if contains special characters) |
Response: The file content with appropriate Content-Type header.
Get Presigned Download URL
Get a presigned URL for direct browser download.
GET /v1/storage/download/{key}/presigned
Authorization: Bearer YOUR_TOKEN
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
expiry_seconds | integer | 3600 | URL expiration time in seconds (max 86400) |
Response:
{
"url": "https://storage.example.com/images/user123/profile.jpg?X-Amz-Signature=...",
"key": "images/user123/profile.jpg",
"expires_at": "2026-01-24T13:00:00Z"
}
Get File Metadata
Get metadata for a stored file.
GET /v1/storage/{key}/metadata
Authorization: Bearer YOUR_TOKEN
Response:
{
"key": "images/user123/profile.jpg",
"size_bytes": 245632,
"content_type": "image/jpeg",
"last_modified": "2026-01-24T10:30:00Z",
"etag": "\"d41d8cd98f00b204e9800998ecf8427e\""
}
List Files
List files with optional prefix filtering.
GET /v1/storage/list
Authorization: Bearer YOUR_TOKEN
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
prefix | string | - | Filter by key prefix (e.g., "images/user123/") |
max_keys | integer | 1000 | Maximum number of results |
continuation_token | string | - | Token for pagination |
Response:
{
"objects": [
{
"key": "images/user123/profile.jpg",
"size_bytes": 245632,
"content_type": "image/jpeg",
"last_modified": "2026-01-24T10:30:00Z",
"etag": "\"d41d8cd98f00b204e9800998ecf8427e\""
}
],
"is_truncated": false,
"next_continuation_token": null
}
Delete File
Delete a file from storage. This endpoint:
- Verifies the file exists before deletion
- Deletes the file from S3 storage
- Removes the file metadata from the database
- Logs an audit event (
file_deleted)
Requires storage:delete permission.
DELETE /v1/storage/{key}
Authorization: Bearer YOUR_TOKEN
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
key | string | The storage key (URL-encoded if contains special characters) |
Response (200 OK):
{
"message": "File deleted successfully"
}
Error Responses:
| Status | Description |
|---|---|
| 401 | Unauthorized - invalid or missing token |
| 403 | Forbidden - missing storage:delete permission |
| 404 | File not found |
| 500 | Storage operation failed |
GraphQL API
Queries
storageLimits
Get upload limits for all categories.
query {
storageLimits {
images {
maxSizeMb
allowedTypes
}
documents {
maxSizeMb
allowedTypes
}
videos {
maxSizeMb
allowedTypes
}
general {
maxSizeMb
allowedTypes
}
}
}
presignedDownloadUrl
Get a presigned download URL.
query {
presignedDownloadUrl(key: "images/user123/profile.jpg", expirySeconds: 3600) {
url
key
expiresAt
}
}
fileMetadata
Get file metadata.
query {
fileMetadata(key: "images/user123/profile.jpg") {
key
sizeBytes
contentType
lastModified
etag
}
}
listFiles
List files with pagination.
query {
listFiles(input: { prefix: "images/user123/", maxKeys: 100 }) {
objects {
key
sizeBytes
contentType
lastModified
}
isTruncated
nextContinuationToken
}
}
Mutations
getPresignedUploadUrl
Get a presigned URL for uploading.
mutation {
getPresignedUploadUrl(input: {
filename: "profile.jpg"
contentType: "image/jpeg"
category: "images"
}) {
url
key
expiresAt
}
}
deleteFile
Delete a file from storage. This mutation:
- Verifies the file exists before deletion
- Deletes the file from S3 storage
- Removes the file metadata from the database
- Logs an audit event (
file_deleted)
Requires storage:delete permission.
mutation {
deleteFile(key: "images/user123/profile.jpg")
}
Response: Returns true on success.
Errors:
"Authentication required"- No valid auth token"File not found"- The specified key doesn't exist"Failed to delete file: ..."- S3 deletion failed
Audit Events
All storage operations are logged:
| Event | Description |
|---|---|
file_created | File uploaded to storage |
file_downloaded | File downloaded from storage |
file_edited | File metadata modified |
file_deleted | File deleted from storage |
Configuration
Storage is configured in the API configuration file:
[storage]
enabled = true
endpoint = "http://localhost:9000" # S3-compatible endpoint
region = "us-east-1"
access_key = "your-access-key"
secret_key = "your-secret-key"
bucket = "heimdall"
path_style = true # Required for MinIO
public_url = "" # Optional CDN URL
[storage.upload_limits.images]
max_size_mb = 5
allowed_types = ["image/jpeg", "image/png", "image/webp", "image/gif"]
[storage.upload_limits.documents]
max_size_mb = 25
allowed_types = ["application/pdf", "text/plain", "application/json"]
[storage.upload_limits.videos]
max_size_mb = 500
allowed_types = ["video/mp4", "video/webm", "video/quicktime", "video/x-msvideo"]
[storage.upload_limits.general]
max_size_mb = 100
allowed_types = ["*/*"]
Provider-Specific Configuration
MinIO (Local Development)
[storage]
endpoint = "http://localhost:9000"
region = "us-east-1"
path_style = true
Cloudflare R2
[storage]
endpoint = "https://<account_id>.r2.cloudflarestorage.com"
region = "auto"
path_style = false
public_url = "https://pub-xxx.r2.dev"
DigitalOcean Spaces
[storage]
endpoint = "https://<region>.digitaloceanspaces.com"
region = "<region>"
path_style = false
public_url = "https://<space>.<region>.cdn.digitaloceanspaces.com"
AWS S3
[storage]
endpoint = "https://s3.<region>.amazonaws.com"
region = "<region>"
path_style = false
Storage File Metadata
Track who uploaded what files and when with file metadata queries.
REST API
List Storage Files (Admin)
List all storage files with optional filtering. Requires storage:read permission.
GET /v1/storage/files
Authorization: Bearer YOUR_TOKEN
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number (1-indexed) |
limit | integer | 20 | Items per page (max 100) |
userId | string | - | Filter by user ID |
category | string | - | Filter by category (images, documents, videos, general) |
contentType | string | - | Filter by MIME type |
keyPrefix | string | - | Filter by key prefix |
filenameSearch | string | - | Search by filename (partial match) |
startDate | string | - | Filter by start date (ISO 8601) |
endDate | string | - | Filter by end date (ISO 8601) |
sortField | string | created_at | Sort by: created_at, size_bytes, original_filename |
sortDirection | string | desc | Sort direction: asc or desc |
Response:
{
"files": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"userId": "user123",
"key": "images/user123/profile.jpg",
"originalFilename": "profile.jpg",
"contentType": "image/jpeg",
"category": "images",
"sizeBytes": 245632,
"etag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
"bucket": "heimdall",
"createdAt": "2026-01-24T10:30:00Z"
}
],
"totalCount": 150,
"page": 1,
"limit": 20,
"totalPages": 8
}
Get File Info by Key
Get metadata for a specific file by its storage key.
GET /v1/storage/files/{key}/info
Authorization: Bearer YOUR_TOKEN
List My Files
List storage files uploaded by the current authenticated user.
GET /v1/storage/files/me
Authorization: Bearer YOUR_TOKEN
GraphQL API
Query: storageFiles
List all storage files with filters (admin, requires storage:read).
query {
storageFiles(input: {
category: "images"
filenameSearch: "profile"
limit: 20
sortField: "created_at"
sortDirection: "desc"
}) {
files {
id
key
originalFilename
contentType
sizeBytes
userId
createdAt
}
totalCount
totalPages
}
}
Query: storageFileByKey
Get file metadata by storage key.
query {
storageFileByKey(key: "images/user123/profile.jpg") {
id
userId
originalFilename
sizeBytes
createdAt
}
}
Query: myStorageFiles
List current user's files.
query {
myStorageFiles(
category: "images"
limit: 20
) {
files {
key
originalFilename
sizeBytes
createdAt
}
totalCount
}
}
Error Responses
| Status | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | File exceeds size limit or invalid MIME type |
| 401 | UNAUTHORIZED | Authentication required |
| 403 | FORBIDDEN | Insufficient permissions |
| 404 | NOT_FOUND | File not found |
| 500 | STORAGE_ERROR | Storage operation failed |