The complete implementation guide from The Validator (#57) and Gemini. Used by Chronicler #58 to implement Discord-Stripe OAuth linking. Contains: - Step-by-step implementation instructions - OAuth route code (/stripe/auth, /stripe/callback) - Webhook handler updates - Website button changes - Testing checklist - Troubleshooting guide Signed-off-by: Claude <claude@firefrostgaming.com>
13 KiB
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:
-
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)
- Client ID:
-
Environment Variables in
/opt/arbiter-3.0/.env:DISCORD_CLIENT_ID=1487080166969577502 DISCORD_CLIENT_SECRET=xOK9ZYgionyqd-huGJRE2Rym98zy0W-m DISCORD_CHECKOUT_REDIRECT_URI=https://discord-bot.firefrostgaming.com/stripe/callback -
Database:
stripe_productstable withtier_levelandstripe_price_idcolumnssubscriptionstable withdiscord_idcolumn
-
Stripe Integration:
- Webhook endpoint at
/webhooks/stripe - Handles
checkout.session.completedevent
- Webhook endpoint at
🔧 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:
DISCORD_CHECKOUT_REDIRECT_URI=https://discord-bot.firefrostgaming.com/stripe/callback
Verify it loaded:
systemctl restart arbiter-3
journalctl -u arbiter-3 -n 20 | grep -i "listening"
Step 3: Install axios (if not already installed)
Command:
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
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
// 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:
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):
<a href="https://buy.stripe.com/..." class="btn">Subscribe to Awakened</a>
AFTER (new OAuth flow buttons):
<!-- Awakened - Tier 1 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=1" class="btn">
Subscribe to Awakened ($1)
</a>
<!-- Elemental Fire - Tier 2 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=2" class="btn">
Subscribe to Elemental (Fire) ($5/mo)
</a>
<!-- Elemental Frost - Tier 3 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=3" class="btn">
Subscribe to Elemental (Frost) ($5/mo)
</a>
<!-- Knight Fire - Tier 4 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=4" class="btn">
Subscribe to Knight (Fire) ($10/mo)
</a>
<!-- Knight Frost - Tier 5 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=5" class="btn">
Subscribe to Knight (Frost) ($10/mo)
</a>
<!-- Master Fire - Tier 6 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=6" class="btn">
Subscribe to Master (Fire) ($15/mo)
</a>
<!-- Master Frost - Tier 7 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=7" class="btn">
Subscribe to Master (Frost) ($15/mo)
</a>
<!-- Legend Fire - Tier 8 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=8" class="btn">
Subscribe to Legend (Fire) ($20/mo)
</a>
<!-- Legend Frost - Tier 9 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=9" class="btn">
Subscribe to Legend (Frost) ($20/mo)
</a>
<!-- Sovereign - Tier 10 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=10" class="btn btn-sovereign">
Subscribe to Sovereign ($499)
</a>
🧪 Testing the Flow
Test 1: Complete Checkout Flow
- Click subscribe button on website
- Should redirect to Discord login
- Log in with Discord
- Should redirect to Stripe checkout
- Complete test payment
- Check database:
SELECT id, discord_id, tier_level, status FROM subscriptions ORDER BY created_at DESC LIMIT 5; - EXPECTED: New row with your Discord ID and correct tier_level
Test 2: Verify in Admin Panel
- Go to
https://discord-bot.firefrostgaming.com/admin/players - EXPECTED: See your Discord username, avatar, and tier badge
- NOT: See "N/A" or "Unlinked"
Test 3: Multiple Tiers
- Test purchasing different tiers (1, 5, 10)
- Verify each creates correct database entry
- 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:
- Go to Discord Developer Portal
- Add
https://discord-bot.firefrostgaming.com/stripe/callback - Save changes
Issue: Webhook receives no Discord ID
Cause: client_reference_id not being set in session creation
Fix: Verify callback route includes:
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:
- ✅ User clicks subscribe → redirects to Discord login
- ✅ After Discord login → redirects to Stripe checkout
- ✅ After payment → subscription in database has discord_id filled in
- ✅ Admin panel Players page shows Discord username + avatar
- ✅ No "N/A" or "Unlinked" entries for new subscriptions
📊 Database Schema Reference
subscriptions table:
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:
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
- Test with real Discord accounts (use test mode Stripe cards)
- Verify admin panel shows linked subscriptions
- Update Ghost CMS website buttons to use new OAuth flow
- Remove old direct Stripe checkout links
- 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
/opt/arbiter-3.0/.env- Add DISCORD_CHECKOUT_REDIRECT_URI/opt/arbiter-3.0/src/routes/stripe.js- Add /auth and /callback routes, update webhook- Ghost CMS website - Update subscribe button URLs
- 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