# Secure Authentication for Electron SaaS Apps
## Synapse Implementation Guide

## Overview

This guide explains how to implement secure authentication in your Synapse Electron app, removing the need to embed API credentials in your distributed application.

## The Problem

Currently, your Synapse app embeds these credentials in the built application:
- `VITE_SUPABASE_URL` and `VITE_SUPABASE_ANON_KEY` 
- Claude API key
- Digital Ocean Spaces credentials
- Sentry DSN

**Security Risk**: Anyone who downloads your app can extract these credentials from the bundled JavaScript files.

## Recommended Architecture

Modern Electron SaaS applications use a **proxy/backend service** pattern with user-specific authentication:

```
┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│ Electron App│────►│ Your Backend │────►│  Services   │
│   (Client)  │◄────│  (Proxy API) │◄────│ (Supabase,  │
└─────────────┘     └──────────────┘     │  Claude,    │
                           ▲              │  DO Spaces) │
                           │              └─────────────┘
                           │
                    ┌──────┴───────┐
                    │ User Auth    │
                    │ (JWT/Session)│
                    └──────────────┘
```

## Implementation Options

### Option A: Supabase Edge Functions (Recommended)

Since you're already using Supabase, Edge Functions provide the perfect proxy layer.

#### 1. Create the Edge Function

```typescript
// supabase/functions/api-proxy/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

const CLAUDE_API_KEY = Deno.env.get('CLAUDE_API_KEY')
const DO_SPACES_ACCESS_KEY = Deno.env.get('DO_SPACES_ACCESS_KEY')
const DO_SPACES_SECRET_KEY = Deno.env.get('DO_SPACES_SECRET_KEY')

serve(async (req) => {
  // Verify JWT from Electron app
  const authHeader = req.headers.get('Authorization')
  if (!authHeader) {
    return new Response('Unauthorized', { status: 401 })
  }

  // Create Supabase client with user's JWT
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL') ?? '',
    Deno.env.get('SUPABASE_ANON_KEY') ?? '',
    { global: { headers: { Authorization: authHeader } } }
  )

  // Verify user is authenticated
  const { data: { user }, error } = await supabase.auth.getUser()
  if (error || !user) {
    return new Response('Unauthorized', { status: 401 })
  }

  // Route requests based on endpoint
  const url = new URL(req.url)
  const path = url.pathname

  if (path.startsWith('/claude')) {
    return proxyClaudeRequest(req, user)
  } else if (path.startsWith('/storage')) {
    return proxyStorageRequest(req, user)
  }
  
  return new Response('Not found', { status: 404 })
})

async function proxyClaudeRequest(req: Request, user: any) {
  const body = await req.json()
  
  // Add user context to track usage
  const enhancedBody = {
    ...body,
    metadata: {
      user_id: user.id,
      timestamp: new Date().toISOString()
    }
  }
  
  // Forward to Claude API
  const response = await fetch('https://api.anthropic.com/v1/messages', {
    method: 'POST',
    headers: {
      'x-api-key': CLAUDE_API_KEY!,
      'anthropic-version': '2023-06-01',
      'content-type': 'application/json',
    },
    body: JSON.stringify(body)
  })
  
  // Log usage for billing
  await logUsage(user.id, 'claude', response.headers)
  
  return response
}

async function proxyStorageRequest(req: Request, user: any) {
  // Generate presigned URL for DO Spaces
  const { filename, contentType } = await req.json()
  
  // Create S3 client and generate presigned URL
  // (Implementation depends on your S3 client library)
  const presignedUrl = await generatePresignedUrl(filename, contentType)
  
  return new Response(JSON.stringify({ uploadUrl: presignedUrl }), {
    headers: { 'Content-Type': 'application/json' }
  })
}
```

#### 2. Deploy the Function

```bash
# Create the function
supabase functions new api-proxy

# Deploy it
supabase functions deploy api-proxy --no-verify-jwt

# Set environment variables
supabase secrets set CLAUDE_API_KEY=sk-ant-xxx
supabase secrets set DO_SPACES_ACCESS_KEY=xxx
supabase secrets set DO_SPACES_SECRET_KEY=xxx
```

### Option B: Standalone Backend Service

Create a dedicated backend service using Express.js or Fastify:

```typescript
// backend/src/server.ts
import express from 'express'
import { createClient } from '@supabase/supabase-js'
import cors from 'cors'
import rateLimit from 'express-rate-limit'

const app = express()

// Configure CORS for Electron
app.use(cors({
  origin: ['file://', 'http://localhost:3000'],
  credentials: true
}))

// Rate limiting per user
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each user to 100 requests per windowMs
  keyGenerator: (req) => req.user?.id || req.ip
})

app.use('/api/', limiter)
app.use(express.json())

// Authentication middleware
const authMiddleware = async (req, res, next) => {
  const token = req.headers.authorization?.replace('Bearer ', '')
  
  if (!token) {
    return res.status(401).json({ error: 'No token provided' })
  }

  try {
    const supabase = createClient(
      process.env.SUPABASE_URL!,
      process.env.SUPABASE_ANON_KEY!
    )
    
    const { data: { user }, error } = await supabase.auth.getUser(token)
    
    if (error || !user) {
      return res.status(401).json({ error: 'Invalid token' })
    }
    
    req.user = user
    next()
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' })
  }
}

// Claude API proxy
app.post('/api/claude/messages', authMiddleware, async (req, res) => {
  try {
    const response = await fetch('https://api.anthropic.com/v1/messages', {
      method: 'POST',
      headers: {
        'x-api-key': process.env.CLAUDE_API_KEY!,
        'anthropic-version': '2023-06-01',
        'content-type': 'application/json',
      },
      body: JSON.stringify(req.body)
    })
    
    const data = await response.json()
    res.json(data)
  } catch (error) {
    res.status(500).json({ error: 'Failed to proxy request' })
  }
})

// Storage presigned URL generation
app.post('/api/storage/presigned-url', authMiddleware, async (req, res) => {
  const { filename, contentType } = req.body
  
  // Generate presigned URL (implementation varies by provider)
  const uploadUrl = await generatePresignedUrl(
    `users/${req.user.id}/${filename}`,
    contentType
  )
  
  res.json({ uploadUrl })
})

const PORT = process.env.PORT || 3001
app.listen(PORT, () => {
  console.log(`API proxy running on port ${PORT}`)
})
```

## Electron App Updates

### 1. Create a Secure API Client

```typescript
// src/renderer/services/api-client.ts
import { supabase } from './supabase'

export class SecureAPIClient {
  private baseURL: string
  
  constructor() {
    // Use Edge Functions in production, local backend in development
    this.baseURL = import.meta.env.PROD 
      ? 'https://your-project.supabase.co/functions/v1'
      : 'http://localhost:3001/api'
  }

  private async getAuthHeaders(): Promise<HeadersInit> {
    const { data: { session } } = await supabase.auth.getSession()
    if (!session) throw new Error('Not authenticated')
    
    return {
      'Authorization': `Bearer ${session.access_token}`,
      'Content-Type': 'application/json'
    }
  }

  async callClaude(messages: any[]): Promise<any> {
    const headers = await this.getAuthHeaders()
    
    const response = await fetch(`${this.baseURL}/claude/messages`, {
      method: 'POST',
      headers,
      body: JSON.stringify({ 
        messages,
        model: 'claude-3-opus-20240229',
        max_tokens: 4096
      })
    })
    
    if (!response.ok) {
      const error = await response.json()
      throw new Error(error.message || 'API call failed')
    }
    
    return response.json()
  }

  async getPresignedUploadUrl(filename: string, contentType: string): Promise<string> {
    const headers = await this.getAuthHeaders()
    
    const response = await fetch(`${this.baseURL}/storage/presigned-url`, {
      method: 'POST',
      headers,
      body: JSON.stringify({ filename, contentType })
    })
    
    if (!response.ok) {
      throw new Error('Failed to get upload URL')
    }
    
    const { uploadUrl } = await response.json()
    return uploadUrl
  }

  async uploadFile(file: File): Promise<void> {
    // Get presigned URL from backend
    const uploadUrl = await this.getPresignedUploadUrl(file.name, file.type)
    
    // Upload directly to storage
    const response = await fetch(uploadUrl, {
      method: 'PUT',
      body: file,
      headers: {
        'Content-Type': file.type
      }
    })
    
    if (!response.ok) {
      throw new Error('Upload failed')
    }
  }
}

export const apiClient = new SecureAPIClient()
```

### 2. Update Your Agent Service

```typescript
// src/main/services/agent-orchestrator-service.ts
import { apiClient } from '../renderer/services/api-client'

export class AgentOrchestratorService {
  async sendMessageToClaude(agentId: string, messages: any[]) {
    try {
      // Use the secure API client instead of direct API calls
      const response = await apiClient.callClaude(messages)
      
      // Process response as before
      return this.processClaudeResponse(response)
    } catch (error) {
      console.error('Failed to call Claude:', error)
      throw error
    }
  }
}
```

## Environment Variables

### Client-Side `.env` (Safe to expose)
```env
# Public Supabase configuration
VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=your_anon_key # This is safe - it's designed to be public

# API endpoints
VITE_API_URL=https://your-backend.com # Your proxy API URL

# Public Sentry DSN (safe to expose)
VITE_SENTRY_DSN=https://xxx@sentry.io/xxx
```

### Server-Side Environment Variables (Never exposed)
```env
# Private API Keys (Edge Functions or Backend)
CLAUDE_API_KEY=sk-ant-xxx
DO_SPACES_ACCESS_KEY_ID=xxx
DO_SPACES_SECRET_ACCESS_KEY=xxx
DO_SPACES_BUCKET=your-bucket
DO_SPACES_REGION=nyc3
DO_SPACES_ENDPOINT=https://nyc3.digitaloceanspaces.com

# Supabase Service Role (if needed)
SUPABASE_SERVICE_ROLE_KEY=xxx
```

## Security Best Practices

### 1. Rate Limiting
Implement per-user rate limits to prevent abuse:

```typescript
// Supabase Edge Function example
const rateLimits = new Map()

function checkRateLimit(userId: string): boolean {
  const key = `${userId}-${Math.floor(Date.now() / 60000)}` // Per minute
  const count = rateLimits.get(key) || 0
  
  if (count >= 10) { // 10 requests per minute
    return false
  }
  
  rateLimits.set(key, count + 1)
  return true
}
```

### 2. Usage Tracking
Track API usage for billing and analytics:

```typescript
async function logUsage(userId: string, service: string, metadata: any) {
  await supabase.from('api_usage').insert({
    user_id: userId,
    service,
    timestamp: new Date().toISOString(),
    metadata
  })
}
```

### 3. Token Management
Handle token refresh automatically in your Electron app:

```typescript
// src/renderer/hooks/useAuth.ts
import { useEffect } from 'react'
import { supabase } from '../lib/supabase'

export function useAuth() {
  useEffect(() => {
    // Listen for auth changes
    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      async (event, session) => {
        if (event === 'TOKEN_REFRESHED') {
          console.log('Token refreshed successfully')
        } else if (event === 'SIGNED_OUT') {
          // Clear any cached data
          await window.synapseAPI.clearCache()
        }
      }
    )

    return () => subscription.unsubscribe()
  }, [])
}
```

### 4. Secure Local Storage
Use Electron's `safeStorage` API for any sensitive data that must be stored locally:

```typescript
// src/main/services/secure-storage.ts
import { safeStorage } from 'electron'

export class SecureStorage {
  static encrypt(text: string): Buffer {
    if (!safeStorage.isEncryptionAvailable()) {
      throw new Error('Encryption not available')
    }
    return safeStorage.encryptString(text)
  }

  static decrypt(encrypted: Buffer): string {
    if (!safeStorage.isEncryptionAvailable()) {
      throw new Error('Encryption not available')
    }
    return safeStorage.decryptString(encrypted)
  }
}
```

## Implementation Steps

1. **Set up Supabase Edge Functions**
   - Create the proxy function
   - Deploy with proper environment variables
   - Test authentication flow

2. **Update Electron App**
   - Create the secure API client
   - Replace direct API calls with proxy calls
   - Update environment variables

3. **Test Security**
   - Verify no credentials in built app
   - Test with expired tokens
   - Check rate limiting works

4. **Add Monitoring**
   - Track API usage per user
   - Monitor for anomalies
   - Set up alerts for failures

5. **Deploy**
   - Update your build process
   - Remove sensitive env vars from CI/CD
   - Test production builds

## Examples from Popular Apps

This architecture is used by successful Electron SaaS applications:

- **Notion**: Proxies all API calls through their backend
- **Linear**: Uses GraphQL proxy with authentication
- **Discord**: Custom backend handles all operations
- **Figma**: WebSocket proxy for real-time features
- **Slack**: Token-based API gateway

## Conclusion

By implementing this proxy architecture, you'll:
- ✅ Remove all sensitive credentials from your Electron app
- ✅ Enable per-user authentication and authorization
- ✅ Add usage tracking for billing
- ✅ Implement rate limiting and abuse prevention
- ✅ Maintain a secure, scalable architecture

The key principle: **Only public keys and user-specific tokens should ever be in your Electron app**. All sensitive operations should go through your authenticated proxy layer.
