aethex-forge/docs/discord-deployment.md
MrPiglr 25d584fd46
feat: Complete database migration and developer platform
- Applied all 31 pending Supabase migrations successfully
- Fixed 100+ policy/trigger/index duplication errors for shared database
- Resolved foundation_contributions schema mismatch (added user_id, contribution_type, resource_id, points columns)
- Added DROP IF EXISTS statements for all policies, triggers, and indexes
- Wrapped storage.objects operations in permission-safe DO blocks

Developer Platform (10 Phases Complete):
- API key management dashboard with RLS and SHA-256 hashing
- Complete API documentation (8 endpoint categories)
- 9 template starters + 9 marketplace products + 12 code examples
- Quick start guide and SDK distribution
- Testing framework and QA checklist

Database Schema Now Includes:
- Ethos: Artist/guild tracking, verification, tracks, storage
- GameForge: Games, assets, monetization
- Foundation: Courses, mentorship, resources, contributions
- Nexus: Creator marketplace, portfolios, contracts, escrow
- Corp Hub: Invoices, contracts, team management, projects
- Developer: API keys, usage logs, profiles

Platform Status: Production Ready 
2026-01-10 02:05:15 +00:00

20 KiB

Discord Deployment Guide - Production Checklist

Complete guide for deploying Discord Activity to production


Pre-Deployment Checklist

Before deploying your Discord Activity, ensure:

  • Discord Application properly configured in Developer Portal
  • HTTPS domain with valid SSL certificate
  • Environment variables configured correctly
  • Database migrations applied
  • Bot commands registered (if using Discord bot)
  • Local testing completed successfully

Part 1: Discord Developer Portal Configuration

Step 1: Enable Activities Feature

  1. Go to Discord Developer Portal
  2. Select your application
  3. Navigate to General Information tab
  4. Scroll to Activity Settings
  5. Click Enable Activities (if not already enabled)

Step 2: Configure Activity URLs

In the Activity Settings section:

Activity URL:

https://yourdomain.com/discord

Interactions Endpoint URL:

https://yourdomain.com/api/discord/interactions

Important Notes:

  • Must use HTTPS (not HTTP)
  • No trailing slashes
  • Must match your actual domain exactly
  • Wait 1-2 minutes after saving for changes to propagate

Step 3: Set Up OAuth2

  1. Go to OAuth2General
  2. Note your Client ID and Client Secret
  3. Go to OAuth2URL Generator
  4. Under Redirects, add:
https://yourdomain.com/api/discord/oauth/callback
https://yourdomain.com/discord/callback
  1. Under OAuth2 Scopes, select:

    • identify
    • email
    • guilds
  2. Click Save Changes

Step 4: Get Your Public Key

  1. In General Information tab
  2. Copy Public Key (64-character hex string)
  3. Save for environment variables

Step 5: Verify Interactions Endpoint

Discord will automatically test your Interactions Endpoint:

  1. After setting the URL, Discord sends a PING request
  2. You should see a green checkmark appear
  3. If it fails:
    • Verify your API is deployed and accessible
    • Check that DISCORD_PUBLIC_KEY is set correctly
    • Ensure endpoint responds with { "type": 1 } for PING requests

Part 2: Environment Variables Configuration

Production Environment Variables

Create these environment variables in your hosting platform (Vercel, Netlify, Railway, etc.):

Frontend Variables

# Discord Application
VITE_DISCORD_CLIENT_ID=your_discord_client_id

# API Configuration
VITE_API_BASE=https://yourdomain.com

# Supabase
VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=your_anon_key

Backend Variables

# Discord Application (Server-side)
DISCORD_CLIENT_ID=your_discord_client_id
DISCORD_CLIENT_SECRET=your_discord_client_secret
DISCORD_PUBLIC_KEY=your_discord_public_key

# Database
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_ROLE=your_service_role_key

# Security
DISCORD_ADMIN_REGISTER_TOKEN=create_a_random_secure_token
SESSION_SECRET=create_a_random_secure_token

# Optional: Discord Bot (if using /verify command)
DISCORD_BOT_TOKEN=your_bot_token

Generating Secure Tokens

# Generate random tokens (Linux/Mac)
openssl rand -hex 32

# Or use Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Part 3: Database Setup

Run Migrations

Ensure these tables exist in your database:

-- Discord account links
CREATE TABLE IF NOT EXISTS discord_links (
  discord_id TEXT PRIMARY KEY,
  user_id UUID REFERENCES user_profiles(id) ON DELETE CASCADE,
  discord_username TEXT,
  discord_email TEXT,
  discord_avatar TEXT,
  access_token TEXT,
  refresh_token TEXT,
  token_expires_at TIMESTAMPTZ,
  linked_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_discord_links_user_id 
  ON discord_links(user_id);

-- Temporary linking sessions
CREATE TABLE IF NOT EXISTS discord_linking_sessions (
  session_token TEXT PRIMARY KEY,
  user_id UUID REFERENCES user_profiles(id) ON DELETE CASCADE,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  expires_at TIMESTAMPTZ NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_linking_sessions_expires 
  ON discord_linking_sessions(expires_at);

-- Verification codes (for /verify command)
CREATE TABLE IF NOT EXISTS verification_codes (
  code TEXT PRIMARY KEY,
  discord_id TEXT NOT NULL,
  discord_username TEXT NOT NULL,
  used BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  expires_at TIMESTAMPTZ NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_verification_codes_discord_id 
  ON verification_codes(discord_id);
CREATE INDEX IF NOT EXISTS idx_verification_codes_expires 
  ON verification_codes(expires_at);

Set Up Cleanup Job

Old sessions and codes should be cleaned up automatically:

-- Create cleanup function
CREATE OR REPLACE FUNCTION cleanup_expired_discord_data()
RETURNS void AS $$
BEGIN
  -- Delete expired linking sessions
  DELETE FROM discord_linking_sessions
  WHERE expires_at < NOW();
  
  -- Delete expired verification codes
  DELETE FROM verification_codes
  WHERE expires_at < NOW();
END;
$$ LANGUAGE plpgsql;

-- Schedule to run hourly (PostgreSQL + pg_cron)
SELECT cron.schedule('cleanup-discord-data', '0 * * * *', 
  'SELECT cleanup_expired_discord_data()');

Or use a scheduled serverless function (Vercel Cron, etc.):

// api/cron/cleanup-discord.ts
import { createClient } from '@supabase/supabase-js';

export default async function handler(req: Request) {
  // Verify cron secret
  if (req.headers.get('authorization') !== `Bearer ${process.env.CRON_SECRET}`) {
    return new Response('Unauthorized', { status: 401 });
  }
  
  const supabase = createClient(
    process.env.SUPABASE_URL!,
    process.env.SUPABASE_SERVICE_ROLE!
  );
  
  // Clean up expired sessions
  await supabase
    .from('discord_linking_sessions')
    .delete()
    .lt('expires_at', new Date().toISOString());
  
  // Clean up expired codes
  await supabase
    .from('verification_codes')
    .delete()
    .lt('expires_at', new Date().toISOString());
  
  return new Response('OK', { status: 200 });
}

Part 4: Deploy Your Application

Vercel Deployment

# Install Vercel CLI
npm i -g vercel

# Login
vercel login

# Deploy
vercel --prod

# Set environment variables
vercel env add DISCORD_CLIENT_SECRET
vercel env add SUPABASE_SERVICE_ROLE
# ... add all other secrets

Netlify Deployment

# Install Netlify CLI
npm i -g netlify-cli

# Login
netlify login

# Deploy
netlify deploy --prod

# Set environment variables via Netlify UI:
# Site Settings → Environment Variables

Railway Deployment

# Install Railway CLI
npm i -g @railway/cli

# Login
railway login

# Initialize project
railway init

# Deploy
railway up

# Set environment variables
railway variables set DISCORD_CLIENT_SECRET=your_secret
# ... add all other secrets

Docker Deployment

# Dockerfile
FROM node:18-alpine

WORKDIR /app

# Copy package files
COPY package*.json ./
RUN npm ci --only=production

# Copy app files
COPY . .

# Build
RUN npm run build

# Expose port
EXPOSE 8080

# Start
CMD ["npm", "start"]
# Build and run
docker build -t aethex-app .
docker run -p 8080:8080 \
  -e DISCORD_CLIENT_SECRET=your_secret \
  -e SUPABASE_SERVICE_ROLE=your_key \
  aethex-app

Part 5: Register Discord Bot Commands

If you're using a Discord bot for /verify command:

Option A: Via API Endpoint

curl -X POST https://yourdomain.com/api/discord/admin-register-commands \
  -H "Authorization: Bearer YOUR_DISCORD_ADMIN_REGISTER_TOKEN" \
  -H "Content-Type: application/json"

Option B: Via Script

npm run register-commands

Option C: Manual Registration

// scripts/register-commands.ts
import { REST, Routes } from 'discord.js';

const commands = [
  {
    name: 'verify',
    description: 'Link your Discord account to AeThex'
  }
];

const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_BOT_TOKEN!);

async function registerCommands() {
  try {
    console.log('Registering slash commands...');
    
    await rest.put(
      Routes.applicationCommands(process.env.DISCORD_CLIENT_ID!),
      { body: commands }
    );
    
    console.log('Successfully registered commands!');
  } catch (error) {
    console.error('Error registering commands:', error);
  }
}

registerCommands();

Part 6: Testing in Production

Test Checklist

1. Test Activity Launch

  • Add your bot to a test Discord server
  • Right-click bot → Apps → Your Activity
  • Activity opens in modal
  • No console errors

2. Test Authentication

  • Click "Login with Discord" in Activity
  • Discord authorization prompt appears
  • After authorizing, user is logged in
  • User profile data is correct

3. Test Account Linking

  • Log in to web app (not Activity)
  • Go to Dashboard → Connections
  • Click "Link Discord"
  • After authorizing, Discord appears in connections
  • No session loss

4. Test Bot /verify Command

  • Type /verify in Discord
  • Bot responds with link and code
  • Click link, redirects to verification page
  • Account links successfully
  • Discord appears in Dashboard connections

5. Test Error Handling

  • Try linking already-linked Discord to different account (should fail)
  • Try using expired verification code (should fail)
  • Try accessing Activity without authorization (should prompt)

Monitoring

Set up monitoring for:

// Monitor OAuth callback failures
app.post('/api/discord/oauth/callback', async (req, res) => {
  try {
    // ... callback logic
  } catch (error) {
    // Log to monitoring service (Sentry, LogRocket, etc.)
    console.error('Discord OAuth callback failed:', error);
    
    // Track metrics
    metrics.increment('discord.oauth.callback.error');
  }
});

// Monitor Activity authentication
app.post('/api/discord/activity-auth', async (req, res) => {
  try {
    // ... auth logic
    metrics.increment('discord.activity.auth.success');
  } catch (error) {
    console.error('Discord Activity auth failed:', error);
    metrics.increment('discord.activity.auth.error');
  }
});

Part 7: Troubleshooting

Issue: "Could not fetch application data"

Symptoms:

  • Activity doesn't load in Discord
  • Console error: 403 Forbidden on Discord API

Causes & Solutions:

  1. Activities not enabled:

    • Go to Discord Portal → General Information
    • Enable Activities feature
    • Set Activity URL
    • Wait 2 minutes
  2. Wrong Activity URL:

    • Verify URL matches your deployed domain exactly
    • Must use HTTPS
    • No trailing slash
  3. Domain not accessible:

    • Test: curl https://yourdomain.com/discord
    • Should return HTML, not error

Issue: "Session lost during OAuth"

Symptoms:

  • User redirected to login page after Discord authorization
  • Error: "session_lost"

Causes & Solutions:

  1. Redirect URI not registered:

    • Go to Discord Portal → OAuth2 → Redirects
    • Add: https://yourdomain.com/api/discord/oauth/callback
    • Wait 2 minutes
  2. Cookie domain mismatch:

    • Frontend domain: app.yourdomain.com
    • API domain: api.yourdomain.com
    • Cookies won't be sent cross-domain
    • Solution: Use same domain or set up CORS properly
  3. SameSite cookie issue:

    res.cookie('session', token, {
      httpOnly: true,
      secure: true,
      sameSite: 'lax',  // or 'none' if cross-domain
      domain: '.yourdomain.com'  // Share across subdomains
    });
    

Issue: "Interactions Endpoint verification failed"

Symptoms:

  • Red X next to Interactions Endpoint URL
  • Discord can't verify endpoint

Causes & Solutions:

  1. Endpoint not responding:

    # Test endpoint
    curl -X POST https://yourdomain.com/api/discord/interactions \
      -H "Content-Type: application/json" \
      -d '{"type": 1}'
    
    # Should return: {"type": 1}
    
  2. DISCORD_PUBLIC_KEY not set:

    • Verify environment variable is set
    • Must be 64-character hex string
    • Restart server after setting
  3. Signature verification failing:

    // Ensure you're using the correct public key
    const { verifyKey } = require('discord-interactions');
    
    const signature = req.headers['x-signature-ed25519'];
    const timestamp = req.headers['x-signature-timestamp'];
    const body = JSON.stringify(req.body);
    
    const isValid = verifyKey(body, signature, timestamp, PUBLIC_KEY);
    

Issue: "Rate limited"

Symptoms:

  • HTTP 429 responses
  • Header: X-RateLimit-Remaining: 0

Solution:

async function handleRateLimit(response) {
  if (response.status === 429) {
    const retryAfter = response.headers.get('Retry-After');
    console.log(`Rate limited. Retry after ${retryAfter}s`);
    
    await new Promise(resolve => 
      setTimeout(resolve, (parseInt(retryAfter) + 1) * 1000)
    );
    
    // Retry request
    return fetch(url, options);
  }
  return response;
}

Issue: "Discord account already linked"

Symptoms:

  • Error when trying to link Discord
  • Message: "This Discord account is already linked to another user"

Solution:

  1. User must unlink from previous account first
  2. Admin can manually unlink in database:
    DELETE FROM discord_links WHERE discord_id = 'discord_id_here';
    

Issue: "Verification code expired"

Symptoms:

  • /verify code doesn't work
  • Error: "Code expired or invalid"

Solution:

  1. Codes expire after 5 minutes by design
  2. User should generate new code: /verify again
  3. Check database cleanup job is running:
    SELECT * FROM verification_codes WHERE expires_at < NOW();
    

Part 8: Security Best Practices

Secrets Management

Never commit secrets to git:

# .gitignore
.env
.env.local
.env.production
**/secrets.json

Use environment variables:

  • Vercel: Site Settings → Environment Variables
  • Netlify: Site Settings → Build & Deploy → Environment
  • Railway: Project → Variables
  • GitHub Actions: Repository → Settings → Secrets

Token Storage

Frontend (Client-side):

// ❌ DON'T store sensitive tokens in localStorage
localStorage.setItem('discord_token', token);  // Bad!

// ✅ DO use HTTP-only cookies
// (Set by backend)
res.cookie('session', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'lax'
});

Backend (Server-side):

// ✅ Store access tokens encrypted
import { encrypt, decrypt } from '@/lib/encryption';

// Save to database
const encrypted = encrypt(accessToken, process.env.ENCRYPTION_KEY);
await db.discord_links.update({
  access_token: encrypted
});

// Retrieve from database
const decrypted = decrypt(encrypted, process.env.ENCRYPTION_KEY);

Rate Limiting

Implement rate limiting on sensitive endpoints:

import rateLimit from 'express-rate-limit';

// OAuth endpoints
const oauthLimiter = rateLimit({
  windowMs: 60 * 1000,  // 1 minute
  max: 10,  // 10 requests per minute
  message: 'Too many OAuth requests, please try again later'
});

app.use('/api/discord/oauth', oauthLimiter);

// Account linking
const linkLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 5,  // 5 requests per minute
  message: 'Too many linking attempts, please try again later'
});

app.use('/api/discord/link', linkLimiter);

Input Validation

Always validate user input:

import { z } from 'zod';

const linkDiscordSchema = z.object({
  discordId: z.string().regex(/^\d{17,19}$/),
  username: z.string().min(2).max(32),
  email: z.string().email(),
  avatar: z.string().nullable()
});

app.post('/api/discord/link', async (req, res) => {
  try {
    const data = linkDiscordSchema.parse(req.body);
    // Proceed with linking...
  } catch (error) {
    return res.status(400).json({ error: 'Invalid input' });
  }
});

Part 9: Performance Optimization

Caching Discord Data

Cache Discord user data to reduce API calls:

import NodeCache from 'node-cache';

const userCache = new NodeCache({ stdTTL: 3600 });  // 1 hour

async function getDiscordUser(accessToken: string) {
  const cacheKey = `discord_user_${accessToken}`;
  const cached = userCache.get(cacheKey);
  
  if (cached) {
    return cached;
  }
  
  const response = await fetch('https://discord.com/api/v10/users/@me', {
    headers: { 'Authorization': `Bearer ${accessToken}` }
  });
  
  const user = await response.json();
  userCache.set(cacheKey, user);
  
  return user;
}

Database Connection Pooling

Use connection pooling for better performance:

import { createClient } from '@supabase/supabase-js';

// Create client with connection pooling
const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE!,
  {
    db: {
      schema: 'public',
    },
    global: {
      headers: {
        'x-connection-pool-size': '20'
      }
    }
  }
);

CDN for Static Assets

Serve static assets from CDN:

// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        assetFileNames: 'assets/[name].[hash][extname]'
      }
    }
  },
  // Use CDN for production
  base: process.env.NODE_ENV === 'production'
    ? 'https://cdn.yourdomain.com/'
    : '/'
});

Part 10: Monitoring & Logging

Set Up Error Tracking

import * as Sentry from '@sentry/node';

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,
  tracesSampleRate: 0.1
});

// Track Discord OAuth errors
app.post('/api/discord/oauth/callback', async (req, res) => {
  try {
    // ... callback logic
  } catch (error) {
    Sentry.captureException(error, {
      tags: { flow: 'discord_oauth' },
      extra: { state: req.query.state }
    });
    throw error;
  }
});

Log Important Events

import winston from 'winston';

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

// Log Discord account linking
logger.info('Discord account linked', {
  userId: user.id,
  discordId: discord.id,
  timestamp: new Date().toISOString()
});

// Log OAuth callback
logger.info('Discord OAuth callback', {
  action: state.action,
  success: true,
  timestamp: new Date().toISOString()
});

Health Check Endpoint

app.get('/health', async (req, res) => {
  try {
    // Check database connection
    await supabase.from('user_profiles').select('count').limit(1);
    
    res.json({
      status: 'healthy',
      timestamp: new Date().toISOString(),
      uptime: process.uptime(),
      discord: {
        clientId: process.env.DISCORD_CLIENT_ID,
        configured: !!process.env.DISCORD_CLIENT_SECRET
      }
    });
  } catch (error) {
    res.status(500).json({
      status: 'unhealthy',
      error: error.message
    });
  }
});

Part 11: Rollback Plan

If something goes wrong in production:

Quick Rollback

# Vercel
vercel rollback

# Netlify
netlify rollback

# Railway
railway rollback

# Git-based rollback
git revert HEAD
git push origin main

Database Rollback

-- Remove Discord links created after deployment
DELETE FROM discord_links 
WHERE linked_at > '2026-01-07 12:00:00';

-- Remove linking sessions
DELETE FROM discord_linking_sessions 
WHERE created_at > '2026-01-07 12:00:00';

Disable Discord Integration

If you need to temporarily disable:

  1. Discord Portal:

    • Disable Activities feature
    • This stops Activity from loading
  2. Application:

    // Add feature flag
    if (!process.env.DISCORD_ENABLED) {
      return res.status(503).json({ 
        error: 'Discord integration temporarily unavailable' 
      });
    }
    

Summary

Deployment Steps:

  1. Configure Discord Developer Portal
  2. Set environment variables
  3. Run database migrations
  4. Deploy application
  5. Register bot commands (if applicable)
  6. Test all flows
  7. Set up monitoring
  8. Monitor for issues

Need Help?


Last Updated: January 7, 2026
Deployment Platform: Universal (Vercel, Netlify, Railway, Docker)
Status: Production Ready