Skip to main content
JOIP uses Supabase Storage for all file uploads including manual session images, user media vaults, and community-shared content.

Overview

Supabase Storage provides:
  • Public buckets for user-uploaded media
  • Automatic CDN for fast global delivery
  • File management with folder organization
  • 100MB per file upload limit (configurable)
  • Unlimited storage (pay-as-you-go)
Supabase is required for manual sessions and media vault features. Standard (Reddit-based) sessions work without it.

Prerequisites

  • Supabase account at supabase.com
  • Active project with storage enabled
  • Project URL and API keys

Setup Steps

1

Create Supabase Project

  1. Sign up at supabase.com
  2. Click “New Project”
  3. Choose organization and name
  4. Select region (choose closest to users)
  5. Set database password (save this!)
  6. Click “Create new project”
Project creation takes 1-2 minutes. The dashboard will show “Setting up project…” during initialization.
2

Get API Credentials

Once your project is ready:
  1. Navigate to SettingsAPI
  2. Find and copy these values:

Project URL

Format: https://xxxxx.supabase.coFound under “Project URL”

Anon Key

Long string starting with eyJ...Found under “Project API keys” → “anon public”

Service Role Key

Long string starting with eyJ...Found under “Project API keys” → “service_role”
The service_role key has admin privileges. Never expose it to clients or commit to version control.
3

Configure Environment Variables

Add credentials to .env:
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
SUPABASE_SERVICE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
All three variables are required. The server will throw an error on startup if any are missing.
4

Initialize Storage Buckets

JOIP automatically creates required buckets on first startup:
  • user-media: Personal uploads and manual sessions
  • general: Community-shared content
Check server logs:
[supabase] Created user-media bucket successfully
[supabase] Created general bucket successfully
[supabase] Storage buckets initialized successfully
If initialization fails, JOIP continues to run but storage features will be unavailable. Check your Supabase project is active (not paused).
5

Verify Storage Configuration

Test storage connectivity:
curl http://localhost:5000/api/storage/status
The status endpoint performs a tiny upload/delete test to verify connectivity.

Implementation Details

Bucket Configuration

JOIP creates two public buckets with these settings:
Purpose: User-specific uploads and manual sessionsSettings:
  • Public access: Yes
  • Allowed MIME types: image/*, video/mp4, video/webm
  • File size limit: Unlimited (enforced in application)
  • Folder structure: users/{userId}/{subfolder}/
Usage:
  • Manual session images: users/{userId}/manual-sessions/{sessionId}/
  • Media vault: users/{userId}/media-vault/
  • Profile pictures: users/{userId}/profile/

File Upload Flow

import { uploadFile } from './supabase';

// Upload to user-media bucket
const { url, path, error } = await uploadFile(
  fileBuffer,                    // Buffer | Uint8Array | File
  'example.jpg',                 // Original filename
  'USER_MEDIA',                  // Bucket: 'USER_MEDIA' | 'GENERAL'
  userId,                        // User ID for folder organization
  'image/jpeg',                  // Content-Type
  'manual-sessions/session-123'  // Optional subfolder
);

if (error) {
  console.error('Upload failed:', error);
} else {
  console.log('File uploaded:', url);
  // url: https://your-project.supabase.co/storage/v1/object/public/user-media/users/{userId}/manual-sessions/session-123/timestamp-uniqueId-example.jpg
  // path: users/{userId}/manual-sessions/session-123/timestamp-uniqueId-example.jpg
}

File Naming Convention

JOIP generates unique filenames to prevent collisions:
{timestamp}-{uniqueId}-{sanitizedFilename}

Example:
1678901234567-a1b2c3d4e5f-my_image.jpg
├─ timestamp: 1678901234567 (milliseconds since epoch)
├─ uniqueId: a1b2c3d4e5f (random alphanumeric)
└─ filename: my_image.jpg (sanitized, special chars → underscores)
Filenames are sanitized to remove special characters and prevent path traversal attacks.

Folder Organization

user-media/
├── users/
│   ├── user-123/
│   │   ├── manual-sessions/
│   │   │   ├── session-456/
│   │   │   │   ├── 1678901234567-abc123-img1.jpg
│   │   │   │   └── 1678901234568-def456-img2.jpg
│   │   │   └── session-789/
│   │   │       └── ...
│   │   ├── media-vault/
│   │   │   ├── 1678901234569-ghi789-vacation.jpg
│   │   │   └── ...
│   │   └── profile/
│   │       └── avatar.jpg
│   └── user-456/
│       └── ...

general/
├── 1678901234570-jkl012-community.jpg
├── 1678901234571-mno345-shared.png
└── ...

API Usage Examples

Upload File

import { uploadFile, STORAGE_BUCKETS } from './supabase';
import { getUserId } from './authUtils';

// From Express route with multer
app.post('/api/upload', upload.single('file'), async (req, res) => {
  const userId = getUserId(req);
  const file = req.file;
  
  if (!file) {
    return res.status(400).json({ error: 'No file provided' });
  }
  
  // Upload to user-media bucket
  const result = await uploadFile(
    file.buffer,
    file.originalname,
    'USER_MEDIA',
    userId,
    file.mimetype,
    'media-vault'  // subfolder
  );
  
  if (result.error) {
    return res.status(500).json({ error: result.error });
  }
  
  res.json({
    url: result.url,
    path: result.path
  });
});

Delete File

import { deleteFile } from './supabase';

const { success, error } = await deleteFile(
  'users/user-123/manual-sessions/session-456/image.jpg',
  'USER_MEDIA'
);

if (success) {
  console.log('File deleted successfully');
} else {
  console.error('Delete failed:', error);
}

Delete Folder

import { deleteFolder } from './supabase';

// Delete all images in a manual session
const { success, error } = await deleteFolder(
  'users/user-123/manual-sessions/session-456',
  'USER_MEDIA'
);

// Deletes all files in the folder

List Files

import { listFiles } from './supabase';

const { files, error } = await listFiles(
  'USER_MEDIA',
  'user-123',
  'media-vault'  // optional subfolder
);

if (!error) {
  files.forEach(file => {
    console.log(file.name, file.created_at, file.metadata);
  });
}

Extract File Path from URL

import { extractFilePathFromUrl } from './supabase';

const url = 'https://abc.supabase.co/storage/v1/object/public/user-media/users/123/file.jpg';
const path = extractFilePathFromUrl(url);

console.log(path);
// Output: users/123/file.jpg

// Use this path for deletion
await deleteFile(path, 'USER_MEDIA');

Error Handling

Storage Error Codes

JOIP returns specific error codes for storage operations:
CodeDescriptionHTTP Status
STORAGE_CONFIG_ERRORMissing environment variables503
STORAGE_UNREACHABLECannot connect to Supabase503
STORAGE_PREFLIGHT_FAILEDBucket test upload failed503
UPLOAD_FAILEDFile upload error500
{
  "error": "Manual session creation requires storage",
  "code": "STORAGE_UNREACHABLE",
  "details": "Supabase project is paused or unreachable"
}

Common Issues

Cause: Supabase project is paused or network issueSolution:
  1. Check project status at app.supabase.com
  2. Resume paused project (free tier auto-pauses after 7 days inactivity)
  3. Verify SUPABASE_URL is correct
  4. Check firewall/network connectivity
  5. Test with /api/storage/status endpoint
Cause: File exceeds 100MB limitSolution:
  • JOIP enforces 100MB per file in application code
  • Compress images before upload
  • For videos, consider using external hosting
  • Increase limit in server/upload.ts if needed
Cause: Invalid API keysSolution:
  • Verify SUPABASE_ANON_KEY and SUPABASE_SERVICE_KEY
  • Check for spaces or quotes in .env
  • Regenerate keys in Supabase dashboard if compromised
  • Ensure project URL matches keys
Cause: Buckets not created during initializationSolution:
  • Restart server to trigger bucket creation
  • Manually create buckets via Supabase dashboard:
    • Storage → Create Bucket → “user-media” (public)
    • Storage → Create Bucket → “general” (public)
  • Check server logs for initialization errors

Storage Management

Monitor Usage

Check storage usage in Supabase dashboard:
  1. Navigate to StorageUsage
  2. View total storage consumed
  3. See breakdown by bucket
  4. Monitor bandwidth usage
Free tier includes 1GB storage and 2GB bandwidth per month. Upgrade to Pro for 100GB storage.

Automatic Cleanup

JOIP automatically cleans up orphaned files:
  • Manual sessions: Deletes entire folder when session deleted
  • Community content: Preserves files in general bucket
  • User media: Files remain until explicitly deleted by user
// In deleteSession function
if (session.isManualMode && !session.isImported) {
  const folderPath = `users/${session.userId}/manual-sessions/${sessionId}`;
  await deleteFolder(folderPath, 'USER_MEDIA');
  logger.info('Cleaned up manual session folder', { sessionId, folderPath });
}

Security & Access Control

Supabase buckets use Row Level Security (RLS):
Both buckets are configured as public:
  • Anyone with URL can view files
  • Server-side code controls uploads
  • Client validation prevents abuse
This is appropriate for an adult content app where all users are authenticated.

Performance Optimization

CDN & Caching

Supabase Storage includes automatic CDN:
  • Files served from nearest edge location
  • HTTP caching headers set automatically
  • No additional configuration needed

Image Optimization

For better performance, optimize images before upload:
import sharp from 'sharp';

async function compressImage(buffer: Buffer): Promise<Buffer> {
  return await sharp(buffer)
    .resize(2000, 2000, { // Max dimensions
      fit: 'inside',
      withoutEnlargement: true
    })
    .jpeg({ quality: 85 }) // Adjust quality
    .toBuffer();
}

// Use in upload route
const compressed = await compressImage(file.buffer);
await uploadFile(compressed, file.originalname, ...);

Batch Operations

For multiple files, upload in parallel:
const uploads = files.map(file => 
  uploadFile(
    file.buffer,
    file.originalname,
    'USER_MEDIA',
    userId,
    file.mimetype,
    subfolder
  )
);

const results = await Promise.all(uploads);

// Check for errors
const failed = results.filter(r => r.error);
if (failed.length > 0) {
  console.error('Some uploads failed:', failed);
}

Migration Guide

From Local Storage to Supabase

If migrating from local uploads/ directory:
1

Setup Supabase

Follow the setup steps above to create project and configure credentials.
2

Upload Existing Files

import fs from 'fs';
import path from 'path';
import { uploadFile } from './server/supabase';

async function migrateLocalFiles() {
  const uploadsDir = path.join(process.cwd(), 'uploads');
  const files = fs.readdirSync(uploadsDir);

  for (const file of files) {
    const filePath = path.join(uploadsDir, file);
    const buffer = fs.readFileSync(filePath);
    
    // Extract userId from path if structured
    const userId = extractUserIdFromPath(filePath);
    
    const result = await uploadFile(
      buffer,
      file,
      'USER_MEDIA',
      userId,
      'image/jpeg',
      'migrated'
    );
    
    if (result.error) {
      console.error(`Failed to migrate ${file}:`, result.error);
    } else {
      console.log(`Migrated ${file}${result.url}`);
    }
  }
}
3

Update Database URLs

Update database records to point to Supabase URLs:
UPDATE session_media
SET media_url = REPLACE(
  media_url,
  'http://localhost:5000/uploads/',
  'https://your-project.supabase.co/storage/v1/object/public/user-media/migrated/'
)
WHERE media_url LIKE 'http://localhost:5000/uploads/%';
4

Remove Local Storage Code

Once migration is verified:
  • Remove uploads/ directory
  • Delete local storage functions
  • Update .gitignore if needed

Troubleshooting

Debug Storage Issues

// In server/supabase.ts
function log(message: string, source = "supabase"): void {
  logger.debug(`[${source}] ${message}`);
}

// Logs will show:
// [supabase] File uploaded to Supabase: users/123/file.jpg
// [supabase] File deleted from Supabase: users/123/old.jpg

Test Storage Connectivity

# Test upload (creates tiny file)
curl -X GET http://localhost:5000/api/storage/status

# Response shows:
{
  "configured": true,
  "connectionTest": "success"
}

Check Supabase Dashboard

  1. Navigate to Storage in Supabase dashboard
  2. Select bucket (e.g., user-media)
  3. Browse uploaded files
  4. Check file sizes and paths
  5. Download files to verify integrity

Security Best Practices

API Key Protection

  • Store keys in .env only
  • Never commit service role key
  • Use anon key for client-side (if needed)
  • Rotate keys if compromised

Input Validation

  • Validate file types (MIME + extension)
  • Enforce size limits (100MB default)
  • Sanitize filenames (prevent path traversal)
  • Check image dimensions if needed

Access Control

  • Verify user owns files before operations
  • Use userId in paths for isolation
  • Implement rate limiting on uploads
  • Track upload quotas per user

Content Security

  • Scan uploads for malware (optional)
  • Validate image integrity
  • Block executable file types
  • Log all upload/delete operations

Build docs developers (and LLMs) love