# Discord-Stripe OAuth Linking - Complete Implementation Guide ## ๐ŸŽฏ Mission: Automate Discord User โ†’ Stripe Subscription Linking **Current Problem:** Subscriptions created via Stripe checkout have NO discord_id, making them invisible in admin panel and preventing Discord role assignment. **Gemini's Solution:** "Stateless OAuth Bridge" - Use Discord OAuth before Stripe checkout, pass Discord ID through the entire flow via Stripe's `client_reference_id` field. --- ## ๐Ÿ“‹ Prerequisites **Before implementing, verify these exist:** 1. **Discord Application Settings:** - Client ID: `1487080166969577502` - Client Secret: `xOK9ZYgionyqd-huGJRE2Rym98zy0W-m` - Current redirect URI: `https://discord-bot.firefrostgaming.com/auth/discord/callback` (for admin panel) - **NEW redirect URI needed:** `https://discord-bot.firefrostgaming.com/stripe/callback` (for checkout flow) 2. **Environment Variables in `/opt/arbiter-3.0/.env`:** ```bash DISCORD_CLIENT_ID=1487080166969577502 DISCORD_CLIENT_SECRET=xOK9ZYgionyqd-huGJRE2Rym98zy0W-m DISCORD_CHECKOUT_REDIRECT_URI=https://discord-bot.firefrostgaming.com/stripe/callback ``` 3. **Database:** - `stripe_products` table with `tier_level` and `stripe_price_id` columns - `subscriptions` table with `discord_id` column 4. **Stripe Integration:** - Webhook endpoint at `/webhooks/stripe` - Handles `checkout.session.completed` event --- ## ๐Ÿ”ง Implementation Steps ### Step 1: Add Discord Redirect URI **Action:** Go to Discord Developer Portal โ†’ Applications โ†’ OAuth2 โ†’ Redirects **Add:** ``` https://discord-bot.firefrostgaming.com/stripe/callback ``` **Why:** Discord will redirect users back to this URL after login. --- ### Step 2: Add Environment Variable **File:** `/opt/arbiter-3.0/.env` **Add this line:** ```bash DISCORD_CHECKOUT_REDIRECT_URI=https://discord-bot.firefrostgaming.com/stripe/callback ``` **Verify it loaded:** ```bash systemctl restart arbiter-3 journalctl -u arbiter-3 -n 20 | grep -i "listening" ``` --- ### Step 3: Install axios (if not already installed) **Command:** ```bash cd /opt/arbiter-3.0 && npm install axios ``` **Why:** We need to make HTTP requests to Discord's OAuth API. --- ### Step 4: Add OAuth Routes to stripe.js **File:** `/opt/arbiter-3.0/src/routes/stripe.js` **Add these TWO routes BEFORE the checkout route:** #### Route 1: Auth Entry Point ```javascript const axios = require('axios'); // Step 1: Entry point - User clicks subscribe button on website router.get('/auth', (req, res) => { const tierLevel = req.query.tier; if (!tierLevel) { return res.status(400).send('โŒ Tier level is required'); } // Redirect to Discord OAuth with tier in state parameter const discordAuthUrl = `https://discord.com/api/oauth2/authorize?client_id=${process.env.DISCORD_CLIENT_ID}&redirect_uri=${encodeURIComponent(process.env.DISCORD_CHECKOUT_REDIRECT_URI)}&response_type=code&scope=identify&state=${tierLevel}`; res.redirect(discordAuthUrl); }); ``` #### Route 2: OAuth Callback โ†’ Create Stripe Session ```javascript // Step 2: Discord redirects back here with user ID and tier router.get('/callback', async (req, res) => { const { code, state: tierLevel } = req.query; if (!code || !tierLevel) { return res.redirect('https://firefrostgaming.com/subscribe?error=missing_params'); } try { // Exchange OAuth code for Discord access token const tokenResponse = await axios.post( 'https://discord.com/api/oauth2/token', new URLSearchParams({ client_id: process.env.DISCORD_CLIENT_ID, client_secret: process.env.DISCORD_CLIENT_SECRET, grant_type: 'authorization_code', code: code, redirect_uri: process.env.DISCORD_CHECKOUT_REDIRECT_URI }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } ); // Fetch Discord user identity const userResponse = await axios.get('https://discord.com/api/users/@me', { headers: { Authorization: `Bearer ${tokenResponse.data.access_token}` } }); const discordId = userResponse.data.id; const discordUsername = userResponse.data.username; console.log(`โœ… Discord OAuth success: ${discordUsername} (${discordId}) purchasing tier ${tierLevel}`); // Get Stripe price ID from database const productResult = await pool.query( 'SELECT stripe_price_id, tier_name, billing_type FROM stripe_products WHERE tier_level = $1', [tierLevel] ); if (productResult.rows.length === 0) { return res.redirect('https://firefrostgaming.com/subscribe?error=invalid_tier'); } const { stripe_price_id, tier_name, billing_type } = productResult.rows[0]; // Create Stripe Checkout Session with Discord ID attached const session = await stripe.checkout.sessions.create({ mode: billing_type === 'recurring' ? 'subscription' : 'payment', line_items: [{ price: stripe_price_id, quantity: 1 }], success_url: 'https://firefrostgaming.com/success?session_id={CHECKOUT_SESSION_ID}', cancel_url: 'https://firefrostgaming.com/subscribe', client_reference_id: discordId, // ๐Ÿ”ฅ THIS IS THE MAGIC - Discord ID travels to webhook! metadata: { tier_level: tierLevel, tier_name: tier_name, discord_username: discordUsername } }); console.log(`โœ… Stripe session created: ${session.id} for ${discordUsername}`); // Redirect user to Stripe payment page res.redirect(session.url); } catch (error) { console.error('โŒ OAuth/Checkout Error:', error.response?.data || error.message); res.redirect('https://firefrostgaming.com/subscribe?error=auth_failed'); } }); ``` --- ### Step 5: Update Webhook Handler **File:** `/opt/arbiter-3.0/src/routes/stripe.js` **Find the `checkout.session.completed` handler and UPDATE it:** ```javascript case 'checkout.session.completed': { const session = event.data.object; // ๐Ÿ”ฅ CRITICAL: Extract Discord ID from client_reference_id const discordId = session.client_reference_id; const customerEmail = session.customer_details.email; const tierLevel = parseInt(session.metadata.tier_level); const tierName = session.metadata.tier_name; if (!discordId) { console.error('โŒ No Discord ID in checkout session:', session.id); return res.status(400).send('Missing Discord ID'); } console.log(`โœ… Payment complete: ${session.metadata.discord_username} (${discordId}) - ${tierName}`); // Determine subscription ID based on billing type let stripeSubscriptionId = null; let status = 'lifetime'; if (session.mode === 'subscription') { stripeSubscriptionId = session.subscription; status = 'active'; } // Insert/update subscription with Discord ID await pool.query(` INSERT INTO subscriptions ( stripe_subscription_id, stripe_customer_id, discord_id, tier_level, status, created_at ) VALUES ($1, $2, $3, $4, $5, NOW()) ON CONFLICT (discord_id) DO UPDATE SET tier_level = EXCLUDED.tier_level, status = EXCLUDED.status, stripe_subscription_id = EXCLUDED.stripe_subscription_id, stripe_customer_id = EXCLUDED.stripe_customer_id, updated_at = NOW() `, [stripeSubscriptionId, session.customer, discordId, tierLevel, status]); console.log(`โœ… Database updated: Discord ${discordId} โ†’ Tier ${tierLevel} (${status})`); // TODO: Trigger Discord role assignment via Arbiter // This will be implemented in future task break; } ``` --- ### Step 6: Update Website Buttons **File:** Ghost CMS website (firefrostgaming.com) **BEFORE (old direct Stripe buttons):** ```html Subscribe to Awakened ``` **AFTER (new OAuth flow buttons):** ```html Subscribe to Awakened ($1) Subscribe to Elemental (Fire) ($5/mo) Subscribe to Elemental (Frost) ($5/mo) Subscribe to Knight (Fire) ($10/mo) Subscribe to Knight (Frost) ($10/mo) Subscribe to Master (Fire) ($15/mo) Subscribe to Master (Frost) ($15/mo) Subscribe to Legend (Fire) ($20/mo) Subscribe to Legend (Frost) ($20/mo) Subscribe to Sovereign ($499) ``` --- ## ๐Ÿงช Testing the Flow ### Test 1: Complete Checkout Flow 1. Click subscribe button on website 2. Should redirect to Discord login 3. Log in with Discord 4. Should redirect to Stripe checkout 5. Complete test payment 6. Check database: ```sql SELECT id, discord_id, tier_level, status FROM subscriptions ORDER BY created_at DESC LIMIT 5; ``` 7. **EXPECTED:** New row with your Discord ID and correct tier_level ### Test 2: Verify in Admin Panel 1. Go to `https://discord-bot.firefrostgaming.com/admin/players` 2. **EXPECTED:** See your Discord username, avatar, and tier badge 3. **NOT:** See "N/A" or "Unlinked" ### Test 3: Multiple Tiers 1. Test purchasing different tiers (1, 5, 10) 2. Verify each creates correct database entry 3. Verify admin panel shows correct tier name --- ## ๐Ÿšจ Troubleshooting ### Issue: "Tier level is required" error **Cause:** Button missing `?tier=X` parameter **Fix:** Update website button URL to include tier number --- ### Issue: Discord OAuth fails with "Invalid redirect_uri" **Cause:** Redirect URI not added to Discord Developer Portal **Fix:** 1. Go to Discord Developer Portal 2. Add `https://discord-bot.firefrostgaming.com/stripe/callback` 3. Save changes --- ### Issue: Webhook receives no Discord ID **Cause:** `client_reference_id` not being set in session creation **Fix:** Verify callback route includes: ```javascript client_reference_id: discordId ``` --- ### Issue: Database shows NULL discord_id **Cause:** Webhook using old code that doesn't extract `client_reference_id` **Fix:** Update webhook handler per Step 5 --- ## โœ… Success Criteria **You'll know it works when:** 1. โœ… User clicks subscribe โ†’ redirects to Discord login 2. โœ… After Discord login โ†’ redirects to Stripe checkout 3. โœ… After payment โ†’ subscription in database has discord_id filled in 4. โœ… Admin panel Players page shows Discord username + avatar 5. โœ… No "N/A" or "Unlinked" entries for new subscriptions --- ## ๐Ÿ“Š Database Schema Reference **subscriptions table:** ```sql CREATE TABLE subscriptions ( id SERIAL PRIMARY KEY, stripe_subscription_id VARCHAR(255), stripe_customer_id VARCHAR(255), discord_id VARCHAR(255), -- This gets filled by OAuth flow! tier_level INTEGER, status VARCHAR(50), created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); ``` **stripe_products table:** ```sql CREATE TABLE stripe_products ( tier_level INTEGER PRIMARY KEY, tier_name VARCHAR(255), stripe_price_id VARCHAR(255), billing_type VARCHAR(50) -- 'recurring' or 'one-time' ); ``` --- ## ๐ŸŽฏ Next Steps After Implementation 1. **Test with real Discord accounts** (use test mode Stripe cards) 2. **Verify admin panel shows linked subscriptions** 3. **Update Ghost CMS website buttons** to use new OAuth flow 4. **Remove old direct Stripe checkout links** 5. **Implement Discord role assignment** (future task - Arbiter will auto-assign roles based on tier_level) --- ## ๐Ÿ”ฅ Why This Architecture Is Perfect **Stateless:** No cookies, no sessions, no CSRF tokens needed **Secure:** Discord OAuth validates user identity **Automatic:** Zero manual linking required **Scalable:** Works for 1 user or 10,000 users **Simple:** Only 2 new routes, minimal code changes **RV-Ready:** Runs completely automated while Michael travels --- ## ๐Ÿ“ Files Modified 1. `/opt/arbiter-3.0/.env` - Add DISCORD_CHECKOUT_REDIRECT_URI 2. `/opt/arbiter-3.0/src/routes/stripe.js` - Add /auth and /callback routes, update webhook 3. Ghost CMS website - Update subscribe button URLs 4. Discord Developer Portal - Add new redirect URI --- ## ๐ŸŽ‰ Final Note **This is the LAST piece of the payment puzzle!** Once this is implemented and tested, Firefrost Gaming has a fully automated subscription system that: - Takes payments via Stripe โœ… - Links Discord users automatically โœ… - Shows subscribers in admin panel โœ… - Tracks revenue in Financials โœ… - Ready for Discord role auto-assignment (future task) ๐Ÿ”œ **SOFT LAUNCH READY!** ๐Ÿš€๐Ÿ”ฅโ„๏ธ --- *Credit: Gemini's "Stateless OAuth Bridge" architecture* *Documented by: The Validator (Chronicler #57)* *Date: April 3, 2026*