How to Configure S3 for File Storage and Attachments in Kaneo
Kaneo uses a S3-compatible object store for file attachments, configured entirely through environment variables in apps/api/src/storage/s3.ts where the API generates presigned PUT URLs for uploads and handles private object retrieval.
All uploaded files in Kaneo—including task description images and comment attachments—flow through a unified S3 storage layer. This guide walks through every environment variable, credential strategy, and code pattern you need to enable reliable, scalable file storage in your Kaneo deployment.
Required Environment Variables for S3 Storage
The Kaneo API reads S3 configuration directly from process.env, loaded via dotenv-mono. At minimum, you must specify an endpoint and bucket name.
| Variable | Purpose | Required? |
|---|---|---|
S3_ENDPOINT |
HTTP(S) endpoint of your S3-compatible service (e.g., https://s3.us-east-1.amazonaws.com or https://s3.us-west-002.backblazeb2.com) |
Yes |
S3_BUCKET |
Target bucket for all file storage | Yes |
S3_REGION |
AWS region identifier | No (defaults to us-east-1) |
S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY |
Static IAM credentials | No* |
S3_FORCE_PATH_STYLE |
Use path-style URLs (bucket.endpoint/key) |
No (defaults to true) |
S3_KEY_PREFIX |
Prefix prepended to every object key for isolation | No |
S3_PUBLIC_BASE_URL |
CDN or public-facing base URL for direct access | No |
S3_MAX_IMAGE_UPLOAD_BYTES |
Upload size limit in bytes | No (default: 10485760 = 10 MiB) |
S3_PRESIGN_TTL_SECONDS |
Expiration for presigned URLs | No (default: 300) |
*Provide both access key variables or neither. When omitted, the AWS SDK falls back to its default credential provider chain (IAM instance roles, ECS task roles, etc.).
Example Production Configuration
S3_ENDPOINT=https://s3.us-east-1.amazonaws.com
S3_BUCKET=kaneo-production-attachments
S3_REGION=us-east-1
S3_ACCESS_KEY_ID=AKIA...
S3_SECRET_ACCESS_KEY=...
S3_KEY_PREFIX=prod/v1
S3_PUBLIC_BASE_URL=https://cdn.example.com/kaneo
S3_MAX_IMAGE_UPLOAD_BYTES=20971520
S3_PRESIGN_TTL_SECONDS=600
How Kaneo Generates Presigned Upload URLs
In apps/api/src/storage/s3.ts, the createTaskImageUploadUrl() function constructs secure, time-limited PUT URLs. This lets clients upload files directly to S3 without streaming through your API servers.
The function builds a namespaced object key from workspace, project, and task identifiers:
import { createTaskImageUploadUrl } from "@/storage/s3";
const uploadContext = {
workspaceId: "ws_abc123",
projectId: "proj_def456",
taskId: "task_ghi789",
surface: "description", // "description" | "comment"
filename: "architecture.png",
contentType: "image/png",
};
const { uploadUrl, key, headers } = await createTaskImageUploadUrl(uploadContext);
// Response:
// uploadUrl: "https://s3.us-east-1.amazonaws.com/kaneo-attachments/..."
// key: "prod/v1/workspace/ws_abc123/project/proj_def456/task/task_ghi789/descriptions/uuid-architecture.png"
// headers: { "Content-Type": "image/png", "x-amz-meta-..." }
Key behaviors to understand:
- Key prefixing: If
S3_KEY_PREFIXis set, it is prepended automatically (e.g.,prod/v1/workspace/...) - UUID injection: Filenames receive a UUID prefix to prevent collisions
- Surface isolation: The
surfaceparameter (descriptionorcomment) creates separate subdirectories - Validation gates:
validateTaskAssetUploadInput()rejects non-image MIME types and oversized files before URL generation - Context safety:
assertTaskImageKeyMatchesContext()validates that returned keys stay within the authorized workspace-project-task namespace
Retrieving and Deleting Stored Objects
Kaneo provides two access patterns: private proxied downloads through the API and public direct access when a CDN is configured.
Private Object Retrieval
Use getPrivateObject() in apps/api/src/storage/s3.ts when you need to stream file contents with full control over headers and caching:
import { getPrivateObject } from "@/storage/s3";
const asset = await getPrivateObject(
"prod/v1/workspace/ws_abc123/project/proj_def456/task/task_ghi789/descriptions/uuid-architecture.png"
);
// asset.body: Web ReadableStream
// asset.ContentType: "image/png"
// asset.ContentLength: 245760
// asset.ETag: "\"abc123...\""
// asset.LastModified: Date
This pattern is exposed through the Kaneo API in apps/api/src/index.ts for secure, authenticated downloads where presigned URLs would be inappropriate.
Object Deletion
Clean up files using deleteS3Object():
import { deleteS3Object } from "@/storage/s3";
await deleteS3Object(
"prod/v1/workspace/ws_abc123/project/proj_def456/task/task_ghi789/descriptions/old-architecture.png"
);
The cleanup utility at apps/api/src/storage/cleanup-assets.ts orchestrates deletions for orphaned assets using this same helper.
Credential Resolution Strategy
The resolveS3Credentials() function in apps/api/src/storage/s3.ts implements a strict all-or-nothing policy:
- If both
S3_ACCESS_KEY_IDandS3_SECRET_ACCESS_KEYare present → returns static credentials object - If neither is present → returns
undefined, allowing the AWS SDK to use instance metadata service (IMDSv2), ECS task roles, or other chain providers - If only one is present → throws an explicit configuration error
This design supports both local development (explicit keys) and production deployments on AWS infrastructure (IAM roles) without code changes.
Validation and Security Controls
Kaneo enforces multiple safety layers in the S3 layer:
| Control | Implementation | Location |
|---|---|---|
| MIME type whitelist | Only image/jpeg, image/png, image/gif, image/webp allowed |
validateTaskAssetUploadInput() |
| Size limits | Checked against S3_MAX_IMAGE_UPLOAD_BYTES |
Same |
| Path traversal prevention | Regex validation in assertTaskImageKeyMatchesContext() |
Same |
| Namespace isolation | Keys must match workspace/{id}/project/{id}/task/{id} pattern |
Same |
These checks prevent users from uploading executables, exceeding quotas, or accessing other tenants' data through manipulated keys.
Testing Your S3 Configuration
Verify your setup using the test suite at tests/api/storage/s3.test.ts. Key test categories include:
- Configuration parsing with valid and invalid environment combinations
- Presigned URL generation (signature validity, header correctness)
- Key prefix application and path-style URL forcing
- Validation rejection of oversized or disallowed file types
Run tests locally with valid credentials:
cd apps/api
S3_ENDPOINT=https://s3.us-east-1.amazonaws.com \
S3_BUCKET=test-bucket \
S3_ACCESS_KEY_ID=xxx \
S3_SECRET_ACCESS_KEY=xxx \
npm test -- tests/api/storage/s3.test.ts
Integration with Task and Comment Workflows
The S3 storage layer connects to Kaneo's domain logic in two primary locations:
apps/api/src/task/index.ts: Generates upload URLs when users add images to task descriptionsapps/api/src/index.ts: Proxies private downloads through authenticated API endpoints
When S3_PUBLIC_BASE_URL is configured, the API can return CDN-facing URLs for direct browser access, reducing origin load for frequently accessed attachments.
Summary
- Required configuration: Set
S3_ENDPOINTandS3_BUCKETat minimum; add credentials or rely on IAM roles - Core module: All S3 logic lives in
apps/api/src/storage/s3.tswith helpers for upload URLs, downloads, and deletion - Security model: Presigned PUT URLs for uploads, authenticated API proxy for downloads, strict key namespace validation
- Optional enhancements: Use
S3_KEY_PREFIXfor multi-tenancy,S3_PUBLIC_BASE_URLfor CDN integration, andcleanup-assets.tsfor garbage collection
Frequently Asked Questions
What S3-compatible services work with Kaneo?
Any S3-compatible object store works: AWS S3, Cloudflare R2, Backblaze B2, MinIO, Wasabi, or DigitalOcean Spaces. Configure the appropriate S3_ENDPOINT and set S3_FORCE_PATH_STYLE=true for providers that don't support virtual-hosted-style buckets.
How do I use IAM roles instead of static credentials?
Omit both S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY. The AWS SDK will automatically use the instance metadata service (IMDSv2) on EC2, ECS task roles, or Lambda execution roles. This is the recommended approach for production AWS deployments.
Why are my uploaded files not appearing in the correct location?
Verify that S3_KEY_PREFIX matches your expectations and that the surface parameter (description vs comment) is set correctly in your API calls. Use assertTaskImageKeyMatchesContext() in your custom code to validate key construction.
How can I increase the maximum upload size?
Set S3_MAX_IMAGE_UPLOAD_BYTES to your desired limit in bytes. You must also ensure your reverse proxy (nginx, Traefik, etc.) and the Kaneo API server's body parser are configured to accept larger payloads.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →