Skip to main content

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:

PermissionDescription
storage:readView storage files, metadata, and list files
storage:writeUpload files and get presigned upload URLs
storage:editModify file metadata (rename, move, update metadata)
storage:downloadDownload files and get presigned download URLs
storage:deleteDelete files from storage

Upload Categories

Files are organized into categories with configurable limits:

CategoryDefault Max SizeDefault Allowed Types
images5 MBimage/jpeg, image/png, image/webp, image/gif
documents25 MBapplication/pdf, text/plain, application/json
videos500 MBvideo/mp4, video/webm, video/quicktime, video/x-msvideo
general100 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:

FieldTypeRequiredDescription
filefileYesThe file to upload
categorystringYesCategory: "images", "documents", "videos", or "general"
keystringNoCustom 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:

ParameterTypeDescription
keystringThe 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:

ParameterTypeDefaultDescription
expiry_secondsinteger3600URL 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:

ParameterTypeDefaultDescription
prefixstring-Filter by key prefix (e.g., "images/user123/")
max_keysinteger1000Maximum number of results
continuation_tokenstring-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:

ParameterTypeDescription
keystringThe storage key (URL-encoded if contains special characters)

Response (200 OK):

{
"message": "File deleted successfully"
}

Error Responses:

StatusDescription
401Unauthorized - invalid or missing token
403Forbidden - missing storage:delete permission
404File not found
500Storage 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:

EventDescription
file_createdFile uploaded to storage
file_downloadedFile downloaded from storage
file_editedFile metadata modified
file_deletedFile 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:

ParameterTypeDefaultDescription
pageinteger1Page number (1-indexed)
limitinteger20Items per page (max 100)
userIdstring-Filter by user ID
categorystring-Filter by category (images, documents, videos, general)
contentTypestring-Filter by MIME type
keyPrefixstring-Filter by key prefix
filenameSearchstring-Search by filename (partial match)
startDatestring-Filter by start date (ISO 8601)
endDatestring-Filter by end date (ISO 8601)
sortFieldstringcreated_atSort by: created_at, size_bytes, original_filename
sortDirectionstringdescSort 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

StatusCodeDescription
400VALIDATION_ERRORFile exceeds size limit or invalid MIME type
401UNAUTHORIZEDAuthentication required
403FORBIDDENInsufficient permissions
404NOT_FOUNDFile not found
500STORAGE_ERRORStorage operation failed