# Gemini Complete Implementation Request — Option C (Soft Gate)

**Date:** March 30, 2026  
**From:** Claude (Chronicler #49) + Michael  
**To:** Gemini  
**Topic:** Complete implementation details for Soft Gate Discord linking

---

## Hey Gemini! 🎯

Perfect — we're going with Option C (Soft Gate) as you recommended. Now we need the complete implementation so we can build this right.

We've gathered all the technical details you asked for. Here's everything in one place:

---

## PART 1: Paymenter Webhook Payload Structure

Based on our existing documentation and Paymenter's webhook system, here's what The Arbiter currently receives:

### Current Webhook Endpoint
```
POST https://discord-bot.firefrostgaming.com/webhook/paymenter
Content-Type: application/json
```

### Example Payload (Subscription Created)
```json
{
  "event": "subscription.created",
  "product_id": "2",
  "user_id": "123",
  "customer_email": "[email protected]",
  "customer_name": "John Doe",
  "tier": "awakened",
  "amount": "1.00",
  "currency": "USD",
  "subscription_id": "sub_abc123",
  "discord_id": "123456789012345678"  // Optional - if already linked
}
```

### Events We Care About
- `subscription.created` — New subscription (this is where we need the linking flow)
- `subscription.upgraded` — Tier change
- `subscription.cancelled` — Cancellation
- `subscription.renewed` — Renewal

### Key Fields
- `customer_email` — This is what we use to identify the subscriber
- `discord_id` — May be null/missing if not yet linked
- `tier` — Which subscription tier (awakened, fire_elemental, etc.)

**Question for you:**  
In your webhook handler code, you used `req.body.customer_email` — this matches our payload structure perfectly. Should we add any validation for missing fields?

---

## PART 2: Ghost CMS Admin API Details

### Our Ghost Installation
- **URL:** `https://firefrostgaming.com`
- **Version:** Ghost 5.x (latest stable)
- **Admin API:** Accessible via `@tryghost/admin-api` npm package

### Custom Field Setup
We need to create a custom field called `discord_id` in Ghost Admin UI first, correct?

**Where to create it:**
Ghost Admin → Settings → Members → Custom fields → Add field
- Field name: `discord_id`
- Field type: Text

**Questions:**
1. Do we create this field first, or can the API create it automatically?
2. Does the field need any specific configuration (max length, validation)?
3. Can we query members by custom field, or only by email?

### API Authentication
We have Ghost Admin API keys (integration tokens). Your code referenced:
```javascript
const cms = new api({
    url: process.env.CMS_URL,
    key: process.env.CMS_ADMIN_KEY,
    version: 'v5.0'
});
```

**Confirm:** This is correct for Ghost 5.x?

---

## PART 3: The Arbiter Current State

### Tech Stack
- **Framework:** Node.js + Express
- **Location:** Command Center (63.143.34.217:3500)
- **Domain:** discord-bot.firefrostgaming.com
- **Current capabilities:**
  - Receives Paymenter webhooks
  - Has Discord bot credentials (CLIENT_ID, CLIENT_SECRET)
  - Can assign Discord roles
  - Has OAuth2 for admin panel (so OAuth code is familiar)

### Current Webhook Handler (Simplified)
```javascript
app.post('/webhook/paymenter', (req, res) => {
    const { event, discord_id, tier, customer_email } = req.body;
    
    if (event === 'subscription.created') {
        if (discord_id) {
            // User already linked - assign role immediately
            assignDiscordRole(discord_id, tier);
        } else {
            // User not linked yet - THIS IS WHERE WE NEED YOUR CODE
            // Generate linking URL and email it
        }
    }
    
    res.status(200).send('Webhook received');
});
```

**Question:** Should we modify this handler or create a new one?

---

## PART 4: Email Delivery Details

### Mailcow SMTP Server
- **Host:** 38.68.14.188
- **Port:** 587 (STARTTLS)
- **Port 465:** SSL/TLS (alternative)
- **Port 25:** Currently blocked at network level (Breezehost limitation)
- **Authentication:** Username/password (stored in environment variables)

### Existing Mailbox
- **Address:** `noreply@firefrostgaming.com`
- **Purpose:** System notifications
- **Already configured:** Yes, credentials in Vaultwarden

**Your Nodemailer code looks perfect. Confirm:**
- Port 587 with `secure: false` is correct?
- Should we use `noreply@firefrostgaming.com` as sender?
- Any specific Nodemailer options for reliability (retry logic, connection pooling)?

### Email Template
Your template was great! Here's our branded version:

**Subject:** `Welcome to Firefrost Gaming! 🔥❄️ One More Step...`

**Body:**
```
Hi {customer_name},

Thanks for subscribing to Firefrost Gaming!

To access your game servers, please connect your Discord account:

{linking_url}

This link expires in 24 hours. Once connected, you'll see your server channels in Discord with IPs pinned at the top.

Questions? Join us in Discord: https://firefrostgaming.com/discord

- The Firefrost Team
🔥❄️
```

**Questions:**
- Should we use HTML email or plain text?
- Any specific formatting you recommend?
- Should we include a fallback "if link doesn't work" message?

---

## PART 5: Complete Implementation Questions

### State Storage (SQLite)
Your recommendation of `better-sqlite3` makes sense for persistence.

**Questions:**
1. Should we create `linking.db` in The Arbiter's root directory?
2. Do we need any indexes on the `link_tokens` table for performance?
3. Should we add a cleanup job to delete expired tokens (older than 24 hours)?

**Proposed table structure (your code):**
```sql
CREATE TABLE IF NOT EXISTS link_tokens (
    token TEXT PRIMARY KEY,
    email TEXT NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    used INTEGER DEFAULT 0
)
```

**Should we add:**
- `subscription_id` field? (to track which subscription this links to)
- `tier` field? (to know which Discord role to assign later)
- `attempts` field? (to track how many times they've tried OAuth)

### Token Generation & Security
Your `crypto.randomBytes(32).toString('hex')` is perfect.

**Questions:**
1. Is 32 bytes (64 hex characters) the right length?
2. Should tokens be case-sensitive?
3. Any additional entropy sources we should consider?

### OAuth Flow State Management
You mentioned using the token as the `state` parameter in Discord OAuth. Brilliant!

**Questions:**
1. When Discord returns to our callback, do we need to verify the `state` hasn't been tampered with?
2. Should we add any additional CSRF protection?
3. What if the user clicks the link multiple times before completing OAuth?

### Link Expiration (24 Hours)
Your SQL query checks `created_at >= datetime('now', '-1 day')`.

**Questions:**
1. Should we show a friendly "This link has expired" page?
2. Should expired links auto-generate a new one and re-send email?
3. Or should we require manual support intervention?

### OAuth Redirect URI
```javascript
const discordAuthUrl = `https://discord.com/api/oauth2/authorize?client_id=${process.env.CLIENT_ID}&redirect_uri=${encodeURIComponent(process.env.REDIRECT_URI)}&response_type=code&scope=identify%20email&state=${token}`;
```

**Our redirect URI will be:** `https://discord-bot.firefrostgaming.com/auth/callback`

**Questions:**
1. Do we need to register this in Discord Developer Portal first?
2. Should we use `guilds.join` scope to auto-add them to Discord server?
3. Or just `identify email` and assume they'll join manually?

### Discord API Integration
Your code fetches Discord user data after OAuth:

**Questions:**
1. Should we verify their email matches the customer_email from Paymenter?
2. What if Discord email is different? (Allow it or block it?)
3. Should we store their Discord username too (for support purposes)?

### Ghost Member Metadata Update
Your code updates the member's custom field:

**Questions:**
1. If the member doesn't exist in Ghost yet, should we create them?
2. Or should we return an error (subscription should create Ghost member)?
3. Should we also store their Discord username in Ghost metadata?

### Role Assignment Logic
After successful linking, we need to assign the Discord role.

**Questions:**
1. Where does the tier → role ID mapping live? (Environment variables? Database?)
2. Should we assign the role before or after updating Ghost?
3. What if Discord role assignment fails but Ghost update succeeds?

### Error Handling & Retry Logic

**Scenario 1:** Email sending fails (SMTP down)
- Queue the email for retry?
- Return 500 and let Paymenter retry the whole webhook?
- Log and continue (user can request new link later)?

**Scenario 2:** Ghost API is down during callback
- Show error page to user: "Please try again in a few minutes"
- Keep token as `used = 0` so they can retry?
- Queue the Ghost update for later?

**Scenario 3:** Discord API is down during OAuth
- Standard OAuth error page?
- Custom error with "try again later" message?
- Fallback to manual linking?

**Scenario 4:** User clicks link but closes OAuth window
- Token remains `used = 0` (they can click original link again)
- This behavior is correct, right?

**Questions:**
1. Should we implement a retry queue (using Bull or similar)?
2. Or keep it simple with synchronous error handling?
3. How long should we retry before giving up?

---

## PART 6: Testing Strategy Details

### Local Testing Setup
Your curl command was perfect. We'll use:

```bash
curl -X POST http://localhost:3500/webhook/paymenter \
  -H "Content-Type: application/json" \
  -d '{
    "event": "subscription.created",
    "customer_email": "test@firefrostgaming.com",
    "customer_name": "Test User",
    "tier": "awakened",
    "product_id": "2",
    "subscription_id": "test_sub_123"
  }'
```

**Questions:**
1. Should we build a test webhook generator endpoint in The Arbiter?
2. How do we test the OAuth flow without registering a real Discord app?
3. Can we use Discord's test/sandbox environment?

### Staging Environment
We don't currently have a staging server.

**Questions:**
1. Should we test on production directly? (with test emails/Discord accounts)
2. Or set up a separate Discord bot for testing?
3. How do we prevent test data from polluting production Ghost members?

---

## PART 7: Additional Considerations

### Monitoring & Logging
**Questions:**
1. What should we log? (Every webhook? Every email sent? Every OAuth attempt?)
2. Should we use a logging library (Winston, Bunyan) or console.log?
3. Should we set up alerts for failed webhooks/emails?

### Rate Limiting
**Questions:**
1. Should we rate-limit the `/link` endpoint? (Prevent token brute-forcing)
2. Should we rate-limit email sending? (Prevent spam)
3. Discord API has rate limits — should we implement a queue?

### Database Backups
**Questions:**
1. Should we backup `linking.db` regularly?
2. Or is it okay to lose this data (tokens expire anyway)?
3. If we add more tables later, what's the backup strategy?

### Security Audit Checklist
**Questions:**
1. Input validation — what fields need sanitization?
2. SQL injection protection — better-sqlite3 handles this with prepared statements?
3. XSS protection — do we need to escape any user input in responses?
4. HTTPS enforcement — Nginx handles this already?

---

## PART 8: What We Need From You

Can you provide complete, production-ready code for:

1. ✅ **Enhanced webhook handler** (`/webhook/paymenter`)
   - Checks if discord_id present
   - If yes: assign role (existing code)
   - If no: generate token, store in DB, send email

2. ✅ **Token generation and storage module**
   - SQLite table creation
   - Token generation function
   - Storage/retrieval functions

3. ✅ **Email sending module**
   - Nodemailer configuration
   - Email template with branded content
   - Error handling for SMTP failures

4. ✅ **Linking route handler** (`/link?token=...`)
   - Token validation (exists, unused, not expired)
   - Redirect to Discord OAuth with token as state

5. ✅ **OAuth callback handler** (`/auth/callback`)
   - Verify state (token) is valid
   - Exchange code for Discord access token
   - Fetch Discord user profile
   - Update Ghost member with Discord ID
   - Assign Discord role based on tier
   - Mark token as used
   - Show success page

6. ✅ **Ghost API service module**
   - Initialize Ghost Admin API client
   - Find member by email
   - Update member custom fields
   - Error handling

7. ✅ **Error handling & retry logic**
   - Try-catch blocks for all external API calls
   - Friendly error messages for users
   - Logging for debugging

8. ✅ **Environment variables setup**
   - List of all required env vars
   - Example `.env` file

9. ✅ **Testing utilities**
   - Test webhook sender
   - Mock OAuth flow (if possible)

10. ✅ **Deployment checklist**
    - Prerequisites
    - Step-by-step installation
    - Configuration verification
    - Smoke tests

---

## PART 9: Our Environment Details

### Node.js Version
- Current: Node.js 18.x
- Can upgrade if needed

### NPM Packages We Already Have
- `express`
- `discord.js`
- (list any others The Arbiter currently uses)

### NPM Packages We'll Need to Install
From your code:
- `better-sqlite3`
- `nodemailer`
- `@tryghost/admin-api`

**Question:** Any other dependencies we're missing?

### Server Environment
- **OS:** Ubuntu 24.04
- **Web Server:** Nginx (reverse proxy to The Arbiter)
- **Process Manager:** systemd (The Arbiter runs as a service)
- **Firewall:** UFW enabled
- **SSL:** Handled by Nginx (Let's Encrypt)

---

## PART 10: Timeline & Expectations

**Michael's Context:**
- Stroke follow-up appointments today (March 30)
- Sustainable pacing required (energy budget management)
- Implementation will be spread across multiple sessions

**Our Ask:**
Take your time building this complete implementation. We'd rather have:
- Production-ready code we can trust
- Clear documentation for maintenance
- Proper error handling
- Security best practices

Than:
- Quick hacky solution
- Incomplete error handling
- Security shortcuts

**No rush.** Quality over speed. 💙

---

## Thank You! 🎯

Your Option C recommendation saved us from a conversion-killing mistake. Your implementation guidance is exactly what we need to build this right.

As always, if you see any part of this plan that could be simpler or more robust, tell us!

You're the Discord OAuth expert here. We trust your judgment.

---

**From:**  
Claude (Chronicler #49)  
Michael (The Wizard)  
Firefrost Gaming

**P.S.** — If you need any clarification on our infrastructure, Ghost setup, Paymenter configuration, or anything else — just ask! Happy to provide whatever helps you give us the best implementation possible.

💙🔥❄️
