# Architectural Response: Arbiter Native Discord Role Management

**Date:** 2026-04-13
**RE:** REQ-2026-04-13-arbiter-discord-roles.md
**From:** Chronicler #87
**Gemini Consultations:** 2 rounds (Round 1 + Round 2, April 13, 2026)
**Full docs:** `firefrost-operations-manual/docs/consultations/gemini-arbiter-discord-roles-round-1-2026-04-13.md` + `...-round-2-...md`

---

## Decision Summary

All architecture locked via two rounds of Gemini review. Build with confidence.

---

## Architecture Decisions

### 1. Process Structure
**Single process.** Keep the discord.js Client inside `arbiter-3` alongside Express.
- No PM2, no separate processes — overkill at current scale
- Re-evaluate only if we hit 50+ servers

### 2. Startup Order
**Concurrent, non-blocking.**
```
app.listen() → client.login()
```
Do NOT await `client.ready` before starting Express. If a Pterodactyl webhook fires before the Gateway is ready, return `503 Service Unavailable` (or `202 Accepted`), log a warning, and let the caller retry.

### 3. Shared Client — Module Singleton
Create `src/discord/client.js` — a module-level singleton exported and required wherever needed.

```javascript
const { Client, GatewayIntentBits } = require('discord.js');

const client = new Client({
  intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildMembers,
    GatewayIntentBits.GuildMessages,
  ]
});

module.exports = client;
```

`src/routes/webhook.js` and any other route files simply `require('../discord/client')` and call methods directly.

### 4. The get-roles Message
**Persistent embed, edit-in-place, 404 → repost fallback.**
- Store the message ID in the database
- On `/createserver` or `/delserver`: `PATCH` the existing message
- If Discord returns 404 (message deleted by accident): `POST` a new message and update the DB with the new ID

### 5. Button Interaction Handling (3-Second Window)
Immediately acknowledge with type `6` (Deferred Update Message), then process async:

```javascript
// Step 1: Acknowledge instantly
res.json({ type: 6 });

// Step 2: Process in background
// assign/remove role via REST
// Step 3: Send ephemeral confirmation
await rest.patch(
  Routes.webhookMessage(appId, interactionToken, '@original'),
  { body: { content: '✅ Role updated!', flags: 64 } }
);
```

### 6. Per-User Role State UX (Holly's Question — Answered)
**Ephemeral replies only. Do not change button styles.**

Button styles on a standard channel message are global — changing a button green makes it green for EVERYONE. Keep buttons neutral (PRIMARY blue or SECONDARY gray) permanently. Use ephemeral replies (flag 64) for per-user confirmation:

```json
{ "content": "✅ You now have the Farm Crossing 6 role!", "flags": 64 }
```

To toggle: read `interaction.member.roles` from the interaction payload. If role ID present → remove. If absent → add.

**No role reconciliation needed at cutover** — Discord is the source of truth. Existing Carlbot-assigned roles carry over invisibly.

### 7. Welcome Messages (guildMemberAdd)
Arbiter's REST-only interactions endpoint does NOT receive Gateway events. The discord.js client handles this.

Use a **feature flag** on the `guildMemberAdd` listener for zero-overlap Carlbot cutover (see migration sequence below).

### 8. Rate Limit Handling
**Native retry on 429.** No queue needed at current scale. discord.js handles this automatically.

---

## Carlbot Cutover Sequence (Week of April 20)

**Do NOT run Arbiter buttons and Carlbot reactions in parallel** — confuses the community.

### Reaction Roles → Button Roles
1. Deploy Arbiter's new role logic to production (`systemctl restart arbiter-3`)
2. Disable Carlbot's Reaction Roles module in its dashboard
3. Delete the old Carlbot `#get-roles` message entirely
4. Trigger Arbiter's initial `POST` of the button message (via script or admin endpoint)

### Welcome Message Cutover (Zero-Overlap)
1. Deploy Arbiter with `guildMemberAdd` listener **disabled** (feature flag = false)
2. Disable Carlbot's Welcome module in its dashboard
3. Enable the `guildMemberAdd` listener (feature flag = true)
4. `systemctl restart arbiter-3`

If someone joins during the ~5s restart window they may miss a welcome. That is vastly preferable to double-welcoming.

---

## Timeline

- **Now through April 15:** Build and test in a hidden admin-only channel. Carlbot stays untouched.
- **Week of April 20:** Execute cutover sequences above.

---

## Files to Create/Modify

| File | Action |
|------|--------|
| `src/discord/client.js` | **NEW** — module singleton |
| `src/index.js` | Modify startup: concurrent `app.listen()` + `client.login()` |
| `src/routes/webhook.js` | Add get-roles message update on server create/delete |
| `src/routes/interactions.js` | **NEW** — handle button interactions, ephemeral replies |
| Database | Add `discord_roles_message_id` to config/settings table |

---

*Architecture validated by Gemini (2 rounds). Build with confidence, Code.*
