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_PREFIX is set, it is prepended automatically (e.g., prod/v1/workspace/...)
  • UUID injection: Filenames receive a UUID prefix to prevent collisions
  • Surface isolation: The surface parameter (description or comment) 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:

  1. If both S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY are present → returns static credentials object
  2. If neither is present → returns undefined, allowing the AWS SDK to use instance metadata service (IMDSv2), ECS task roles, or other chain providers
  3. 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:

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_ENDPOINT and S3_BUCKET at minimum; add credentials or rely on IAM roles
  • Core module: All S3 logic lives in apps/api/src/storage/s3.ts with 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_PREFIX for multi-tenancy, S3_PUBLIC_BASE_URL for CDN integration, and cleanup-assets.ts for 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →