Skip to main content

Overview

Kuest Prediction Market stores user-generated assets like profile images in cloud storage. The application supports two storage providers:
  1. Supabase Storage: Integrated with Supabase database, automatic bucket setup
  2. S3-Compatible Storage: AWS S3, Cloudflare R2, MinIO, or any S3-compatible service
The application automatically detects which provider to use based on environment variables.

Storage Provider Selection

The storage provider is automatically selected in this order:
src/lib/storage.ts
// 1. If both SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are set
//    → Use Supabase Storage

// 2. If S3 credentials are set
//    → Use S3-compatible storage

// 3. If neither is configured
//    → Storage provider is 'none' (read-only mode)
If you partially configure a storage provider (e.g., set S3_BUCKET but not S3_ACCESS_KEY_ID), the application will throw an error listing missing variables.

Supabase Storage Setup

Prerequisites

  • Supabase project created
  • Database migrations applied (npm run db:push)

Step 1: Configure Environment Variables

.env
SUPABASE_URL="https://your-project.supabase.co"
SUPABASE_SERVICE_ROLE_KEY="your-service-role-key"

Step 2: Automatic Bucket Creation

When you run npm run db:push, the migration script automatically:
  1. Creates the kuest-assets bucket
  2. Sets up public read access policy
  3. Configures service role full access policy
  4. Sets bucket limits:
    • Max file size: 2 MB
    • Allowed types: image/jpeg, image/png, image/webp
migrations/2025_08_28_003_buckets.sql
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
VALUES (
  'kuest-assets',
  'kuest-assets',
  TRUE,
  2097152,  -- 2 MB
  ARRAY ['image/jpeg', 'image/png', 'image/webp']
)
ON CONFLICT (id) DO NOTHING;

Step 3: Verify Setup

  1. Go to your Supabase dashboard
  2. Navigate to StorageBuckets
  3. Verify kuest-assets bucket exists and is public

File Access URLs

Files are automatically accessible via:
https://your-project.supabase.co/storage/v1/object/public/kuest-assets/{path}
The getPublicAssetUrl() function automatically generates these URLs:
import { getPublicAssetUrl } from '@/lib/storage'

const imageUrl = getPublicAssetUrl('profiles/user-123.jpg')
// → https://your-project.supabase.co/storage/v1/object/public/kuest-assets/profiles/user-123.jpg

S3-Compatible Storage Setup

Supported Providers

AWS S3

Industry standard object storage

Cloudflare R2

Zero egress fees, S3-compatible API

MinIO

Self-hosted S3-compatible storage

DigitalOcean Spaces

Simple S3-compatible storage

AWS S3 Configuration

Step 1: Create an S3 Bucket

  1. Go to AWS S3 Console
  2. Click Create bucket
  3. Configure:
    • Bucket name: kuest-assets (or your choice)
    • Region: Choose closest to your users
    • Block Public Access: Disable (for public assets)
    • Bucket Versioning: Optional

Step 2: Create IAM User

  1. Go to IAM Console
  2. Create a new user with Programmatic access
  3. Attach this policy:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:DeleteObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::kuest-assets/*",
        "arn:aws:s3:::kuest-assets"
      ]
    }
  ]
}

Step 3: Configure Environment Variables

.env
S3_BUCKET="kuest-assets"
S3_REGION="us-east-1"
S3_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
S3_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
For AWS S3, do not set S3_ENDPOINT and S3_PUBLIC_URL. The application automatically generates the correct AWS URLs.

Cloudflare R2 Configuration

Step 1: Create an R2 Bucket

  1. Go to Cloudflare dashboard → R2
  2. Click Create bucket
  3. Choose a name: kuest-assets
  4. Select location hint for performance

Step 2: Generate API Token

  1. Go to R2Manage R2 API Tokens
  2. Create a new API token with:
    • Permissions: Object Read & Write
    • Bucket: kuest-assets
  3. Copy the Access Key ID and Secret Access Key

Step 3: Configure Environment Variables

.env
S3_BUCKET="kuest-assets"
S3_ENDPOINT="https://your-account-id.r2.cloudflarestorage.com"
S3_REGION="auto"
S3_ACCESS_KEY_ID="your-access-key-id"
S3_SECRET_ACCESS_KEY="your-secret-access-key"
S3_PUBLIC_URL="https://assets.yourdomain.com"  # Optional: Custom domain
S3_FORCE_PATH_STYLE=true
Cloudflare R2 has zero egress fees, making it cost-effective for serving user-generated content.

MinIO Configuration (Self-Hosted)

Step 1: Deploy MinIO

Using Docker:
docker run -p 9000:9000 -p 9001:9001 \
  -e MINIO_ROOT_USER=minioadmin \
  -e MINIO_ROOT_PASSWORD=minioadmin \
  minio/minio server /data --console-address ":9001"

Step 2: Create Bucket

  1. Access MinIO console at http://localhost:9001
  2. Create a bucket named kuest-assets
  3. Set bucket policy to public read

Step 3: Create Access Keys

  1. Go to IdentityUsers
  2. Create a new user or use root credentials
  3. Generate access keys

Step 4: Configure Environment Variables

.env
S3_BUCKET="kuest-assets"
S3_ENDPOINT="http://localhost:9000"
S3_REGION="us-east-1"
S3_ACCESS_KEY_ID="minioadmin"
S3_SECRET_ACCESS_KEY="minioadmin"
S3_PUBLIC_URL="https://cdn.yourdomain.com"  # Optional: Custom domain
S3_FORCE_PATH_STYLE=true

DigitalOcean Spaces Configuration

Step 1: Create a Space

  1. Go to DigitalOcean Spaces
  2. Click Create Space
  3. Configure:
    • Name: kuest-assets
    • Region: Choose closest to users
    • File Listing: Enable public

Step 2: Generate API Keys

  1. Go to APISpaces access keys
  2. Click Generate New Key
  3. Copy the key and secret

Step 3: Configure Environment Variables

.env
S3_BUCKET="kuest-assets"
S3_ENDPOINT="https://nyc3.digitaloceanspaces.com"
S3_REGION="nyc3"
S3_ACCESS_KEY_ID="your-spaces-key"
S3_SECRET_ACCESS_KEY="your-spaces-secret"
S3_PUBLIC_URL="https://kuest-assets.nyc3.cdn.digitaloceanspaces.com"
S3_FORCE_PATH_STYLE=false

Configuration Options

S3_FORCE_PATH_STYLE

Controls URL format for accessing files:
https://s3.amazonaws.com/bucket-name/object-key
https://minio.example.com/bucket-name/object-key
Use when:
  • Using MinIO or self-hosted S3
  • Using S3-compatible services with custom domains
  • Default when S3_ENDPOINT is set
https://bucket-name.s3.amazonaws.com/object-key
https://bucket-name.nyc3.digitaloceanspaces.com/object-key
Use when:
  • Using AWS S3 directly
  • Using DigitalOcean Spaces
  • Provider requires subdomain format

S3_PUBLIC_URL

Override the automatically generated public URL:
.env
# Without S3_PUBLIC_URL
# Generated: https://minio.example.com/kuest-assets/profiles/user.jpg

# With S3_PUBLIC_URL
S3_PUBLIC_URL="https://cdn.yourdomain.com"
# Result: https://cdn.yourdomain.com/profiles/user.jpg
Use cases:
  • CDN in front of storage (CloudFront, Cloudflare CDN)
  • Custom domain for branding
  • Load balancer or proxy

Upload Configuration

The application handles file uploads with these settings:
src/lib/storage.ts
export interface UploadPublicAssetOptions {
  contentType: string      // MIME type (image/jpeg, image/png, etc.)
  cacheControl?: string    // HTTP cache header
  upsert?: boolean         // Overwrite existing files
}

Default Upload Limits

  • Max file size: 2 MB (enforced by Supabase bucket, configure on S3)
  • Allowed formats: JPEG, PNG, WebP
  • Cache control: Defaults to 1 year for profile images

Example Upload

import { uploadPublicAsset } from '@/lib/storage'

const { error } = await uploadPublicAsset(
  'profiles/user-123.jpg',
  buffer,
  {
    contentType: 'image/jpeg',
    cacheControl: 'public, max-age=31536000',
    upsert: true, // Overwrite if exists
  }
)

if (error) {
  console.error('Upload failed:', error)
}

Troubleshooting

No storage configuration is set. Choose one:
  1. Supabase: Set SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY
  2. S3: Set S3_BUCKET, S3_ACCESS_KEY_ID, and S3_SECRET_ACCESS_KEY
See Environment Variables for details.
You’ve set some S3 variables but not all required ones. Minimum required:
  • S3_BUCKET
  • S3_ACCESS_KEY_ID
  • S3_SECRET_ACCESS_KEY
Optional: S3_ENDPOINT, S3_REGION, S3_PUBLIC_URL, S3_FORCE_PATH_STYLE
Check your credentials and permissions:Supabase:
  1. Verify SUPABASE_SERVICE_ROLE_KEY is correct
  2. Check bucket policies in Supabase dashboard
  3. Ensure bucket is public or has correct RLS policies
S3:
  1. Verify IAM user has s3:PutObject permission
  2. Check bucket policy allows public read
  3. Test credentials with AWS CLI: aws s3 ls s3://bucket-name/
Check the public URL configuration:Supabase:
  • Bucket must be public
  • URL format: https://{project}.supabase.co/storage/v1/object/public/{bucket}/{path}
S3:
  • Bucket must have public read policy
  • Check S3_PUBLIC_URL if using CDN
  • Verify S3_FORCE_PATH_STYLE matches your provider requirements
Configure CORS on your storage provider:AWS S3:
[
  {
    "AllowedOrigins": ["https://yourdomain.com"],
    "AllowedMethods": ["GET", "PUT", "POST", "DELETE"],
    "AllowedHeaders": ["*"],
    "MaxAgeSeconds": 3000
  }
]
Supabase: CORS is automatically configured for storage APIMinIO: Set CORS via mc admin config set myminio api cors_allowed_origins="https://yourdomain.com"
Optimize costs:
  1. Use Cloudflare R2 for zero egress fees
  2. Implement image optimization and compression
  3. Set up lifecycle policies to delete old files
  4. Use CDN caching to reduce storage requests
  5. Restrict upload sizes and formats
Monitor usage:
  • AWS S3: CloudWatch metrics
  • Supabase: Dashboard storage tab
  • R2: Cloudflare analytics

Best Practices

Security

  1. Never expose service keys: Keep SUPABASE_SERVICE_ROLE_KEY and S3_SECRET_ACCESS_KEY server-side only
  2. Use presigned URLs: For direct uploads from client to S3 (future feature)
  3. Validate file types: Check MIME types and file signatures
  4. Scan uploads: Consider virus scanning for user uploads
  5. Rate limit uploads: Prevent abuse

Performance

  1. Use CDN: Put CloudFront, Cloudflare, or Fastly in front of storage
  2. Optimize images: Compress before upload, use WebP format
  3. Cache aggressively: Set long cache headers for immutable assets
  4. Regional buckets: Store close to users for lower latency
  5. Lazy load images: Don’t load all assets on page load

Cost Optimization

  1. Choose right provider: R2 for high bandwidth, S3 for features
  2. Lifecycle policies: Auto-delete temporary files
  3. Compression: Use WebP instead of PNG/JPEG
  4. Image resizing: Store only necessary sizes
  5. Monitor usage: Set up billing alerts

Next Steps

Authentication

Configure Better Auth and wallet connections

Environment Variables

Complete variable reference

Database Setup

PostgreSQL configuration guide

Deploy to Vercel

Deploy your application

Build docs developers (and LLMs) love