Skip to main content

Overview

Featul uses S3-compatible storage for:
  • User avatars
  • Workspace assets (logos, images)
  • Post images (feedback attachments)
  • Comment images
The platform uses AWS SDK for S3 with support for:
  • Amazon S3
  • Cloudflare R2 (recommended)
  • Any S3-compatible storage provider

Cloudflare R2 Configuration

Cloudflare R2 is the recommended storage provider for Featul (S3-compatible, zero egress fees).

1. Create R2 Bucket

  1. Log in to Cloudflare Dashboard
  2. Navigate to R2 Object Storage
  3. Create a new bucket
  4. Note your Account ID from the R2 overview page

2. Create API Token

  1. Go to R2 > Manage R2 API Tokens
  2. Create a new API token with:
    • Permissions: Object Read & Write
    • Bucket scope: Apply to specific bucket (select your bucket)
  3. Save the Access Key ID and Secret Access Key

3. Configure Public Access

  1. Go to your bucket settings
  2. Enable Public Access or configure a custom domain
  3. Note the public URL (e.g., https://pub-xxxxx.r2.dev)

4. Environment Variables

Add these to your .env file:
.env
R2_ACCOUNT_ID=your_cloudflare_account_id
R2_ACCESS_KEY_ID=your_r2_access_key_id
R2_SECRET_ACCESS_KEY=your_r2_secret_access_key
R2_BUCKET=your_bucket_name
R2_PUBLIC_BASE_URL=https://pub-xxxxx.r2.dev
The R2_PUBLIC_BASE_URL should not include a trailing slash.
Configuration in code: packages/api/src/services/storage-signer.ts:17-32

Amazon S3 Configuration

To use Amazon S3 instead of Cloudflare R2:

1. Create S3 Bucket

  1. Go to AWS S3 Console
  2. Create a new bucket
  3. Configure bucket for public read access (if needed)
  4. Note the bucket name and region

2. Create IAM User

  1. Go to IAM > Users
  2. Create a new user with programmatic access
  3. Attach policy with S3 permissions:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::your-bucket-name/*"
    }
  ]
}

3. Environment Variables

Replace the R2 variables with S3 equivalents:
.env
R2_ACCOUNT_ID=your_aws_region  # e.g., us-east-1
R2_ACCESS_KEY_ID=your_aws_access_key
R2_SECRET_ACCESS_KEY=your_aws_secret_key
R2_BUCKET=your_s3_bucket_name
R2_PUBLIC_BASE_URL=https://your-bucket.s3.amazonaws.com
You’ll need to modify the storage signer code at packages/api/src/services/storage-signer.ts:24 to use the proper S3 endpoint instead of the R2 endpoint format.

Upload Policies

Featul enforces upload policies for different asset types:

Avatar Uploads

  • Max size: Defined in AVATAR_UPLOAD_POLICY
  • Allowed types: Images
  • Path: users/{userId}/avatar/{id}-{filename}
  • Rate limit: Per user
Code reference: packages/api/src/router/storage.ts:27-56

Workspace Assets

  • Max size: Defined in WORKSPACE_UPLOAD_POLICIES
  • Allowed types: Images, documents
  • Path: workspaces/{slug}/{folder}/{id}-{filename}
  • Rate limit: Per user
  • Access control: Workspace members only
Code reference: packages/api/src/router/storage.ts:58-103

Post Images

  • Max size: Defined in POST_IMAGE_UPLOAD_POLICY
  • Allowed types: Images
  • Path: workspaces/{slug}/posts/{id}-{filename}
  • Rate limit: Per user (authenticated) or per IP (anonymous)
  • Access control: Public boards allow uploads
Code reference: packages/api/src/router/storage.ts:105-154

Comment Images

  • Max size: Defined in COMMENT_IMAGE_UPLOAD_POLICY
  • Allowed types: Images
  • Path: workspaces/{slug}/comments/{id}-{filename}
  • Rate limit: Per user
  • Access control: Workspace members or public board participants
Code reference: packages/api/src/router/storage.ts:156-219

Signed Upload URLs

Featul uses pre-signed URLs for secure direct uploads:
  1. Client requests upload URL from API
  2. API generates pre-signed URL with:
    • Short expiration time (default: 300 seconds)
    • Content-Type validation
    • Content-Length validation
  3. Client uploads directly to S3/R2
  4. Client receives public URL for the uploaded file
Code reference: packages/api/src/services/storage-signer.ts:35-59
Pre-signed URLs expire after a short time for security. The TTL is defined in SIGNED_UPLOAD_URL_TTL_SECONDS.

Rate Limiting

Upload URL requests are rate-limited per endpoint:
  • Avatar uploads: Per user
  • Workspace uploads: Per user
  • Public post images: Per user or per IP (anonymous)
  • Comment images: Per user
Rate limits require Redis configuration. See the Authentication Configuration page for Redis setup. Code reference: packages/api/src/router/storage.ts:7-13

Storage Structure

bucket/
├── users/
│   └── {userId}/
│       └── avatar/
│           └── {id}-{filename}
├── workspaces/
│   └── {workspaceSlug}/
│       ├── {folder}/
│       │   └── {id}-{filename}
│       ├── posts/
│       │   └── {id}-{filename}
│       └── comments/
│           └── {id}-{filename}

Dependencies

The storage system uses AWS SDK packages:
"@aws-sdk/client-s3": "^3.721.0",
"@aws-sdk/s3-request-presigner": "^3.721.0"
Package file: packages/api/package.json:23-24

Build docs developers (and LLMs) love