This document walks a brand-new developer from a clean machine to a fully-deployed, working Tribe Connect bot. Follow the sections in order — each one builds on the previous.
- What This Bot Does
- Prerequisites
- Clone the Repository
- Create the Discord Application
- Set Up Firebase
- Create the X / Twitter App
- Get a RapidAPI Twitter241 Key
- Configure Your Local
.env - Install Dependencies and Test Locally
- Deploy Slash Commands
- Invite the Bot to a Test Server
- Smoke-Test the Bot Locally
- Deploy to Railway
- Switch X OAuth to Production URL
- Verify the Production Deployment
- Initialize the Bot on Your Discord Server
- Environment Variable Reference
- Files You'll Touch
- Troubleshooting
Tribe Connect is a Discord bot that lets community members link their X (Twitter) account, earn points for engaging with tweets the admin posts (reposts, comments, early-engager bonuses), spend points in a marketplace, and enter raffles. Admins manage points and view a leaderboard.
Stack:
- Runtime: Node.js (ESM,
"type": "module") - Bot library: discord.js v14
- Database: Google Firestore (via
firebase-admin) - Auth server: Express on
PORT(handles X OAuth callback, exposes/health) - Twitter data: Twitter241 via RapidAPI (retweeters/commenters lookup)
- Hosting: Railway.app (recommended)
Install on your machine:
| Tool | Version | Why |
|---|---|---|
| Node.js | 18+ (20 LTS recommended) | Runs the bot |
| Git | any | Clone & push the repo |
| A code editor | VS Code recommended | Editing |
| Railway CLI | optional | Easier deploys/logs |
Accounts you'll need (all free to start):
- GitHub account
- Discord Developer Portal
- Google Firebase Console
- X Developer Portal
- RapidAPI
- Railway.app (sign up with GitHub)
git clone <your-repo-url> tribe
cd tribeImportant: Keep the repo private — even with
.envgitignored, the codebase represents engagement logic you probably don't want public.
- Go to https://discord.com/developers/applications.
- Click "New Application", give it a name (e.g.,
Tribe Connect), accept ToS. - From General Information, copy the Application ID → this is
DISCORD_CLIENT_ID.
- In the left sidebar click Bot.
- Click "Reset Token" and copy the token → this is
DISCORD_TOKEN.- You will only see this token once. Store it somewhere safe immediately.
- Under Privileged Gateway Intents, leave them all OFF. The bot only uses the unprivileged
Guildsintent. - Under Bot Permissions, you can ignore this section — we'll set permissions via the invite URL below.
- Sidebar → OAuth2 → URL Generator.
- Scopes: check
botandapplications.commands. - Bot Permissions: check at minimum:
Manage Roles(assigning marketplace reward roles, raffle reward roles)Manage Channels(used by/initiateto create the dedicated channels)Manage Messages(cleaning up dashboards)View ChannelsSend MessagesEmbed LinksAttach Files(for/export usersCSV)Read Message History
- Copy the generated URL at the bottom — you'll use it in Step 11 to invite the bot.
- Go to https://console.firebase.google.com → Add project.
- Name it (e.g.,
tribe-connect), disable Google Analytics (not needed), finish. - From Project Settings (gear icon) → General, copy the Project ID → this is
FIREBASE_PROJECT_ID.
- Left sidebar → Build → Firestore Database → Create database.
- Start in production mode (we'll lock rules below).
- Choose a region near your users (e.g.,
us-central1oreurope-west1). This cannot be changed later.
The leaderboard and CSV export use a collectionGroup query that needs an index.
- Firestore → Indexes tab → Add index.
- Configure:
- Collection ID:
guilds - Query scope: Collection group (not Collection)
- Fields:
guildId— Ascendingpoints— Descending__name__— Descending (Firestore adds this automatically)
- Collection ID:
- Click Create. It takes a few minutes to build.
If you skip this, leaderboard / export commands will throw
FAILED_PRECONDITION: The query requires an index— and the error message will include a direct link to create the index it needs.
Firestore → Rules → paste:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// The Admin SDK bypasses these rules — this denies all other access.
match /{document=**} {
allow read, write: if false;
}
}
}Click Publish. Because the bot uses the Admin SDK (service account), it bypasses these rules — but no one else can touch your data.
- Project Settings → Service accounts tab → Generate new private key → Generate key.
- A JSON file downloads. Treat this like a password.
- Move it into the repo root and rename it to
serviceAccountKey.json:
mv ~/Downloads/tribe-connect-*.json ./serviceAccountKey.jsonThe file is already in .gitignore — verify with git status that it does not appear.
Go to https://developer.x.com/en/portal/dashboard and complete the developer signup. A free tier is sufficient for OAuth (we read user identity only — bulk tweet data comes from RapidAPI).
- Projects & Apps → New Project → name it
Tribe Connect, pick any use case. - Inside the project create an App (or reuse the auto-created one).
- Inside the app → User authentication settings → Set up (or Edit).
- Configure:
- App permissions: Read
- Type of App: Web App, Automated App or Bot
- Callback URI / Redirect URL — for now use the local URL:
(You'll add the production URL alongside this one in Step 14.)
http://localhost:3100/auth/x/callback - Website URL: anything valid (e.g.,
https://example.com)
- Save.
In the app → Keys and tokens tab:
- OAuth 2.0 Client ID and Client Secret → click Regenerate if not visible. Copy both:
X_CLIENT_IDX_CLIENT_SECRET
- Bearer Token (optional) →
X_BEARER_TOKEN. The bot only uses this for a couple of public endpoints; you can leave it blank.
The bot uses Twitter241 to fetch the list of users who reposted/commented on a tweet — this is what powers /verify on tweet dashboards.
- Go to https://rapidapi.com/davethebeast/api/twitter241.
- Sign up / log in.
- Click Subscribe to Test → pick a plan (the free tier works for development; pick a paid plan for production based on your server size).
- On any endpoint page in the Code Snippets panel, copy:
X-RapidAPI-Key→RAPIDAPI_KEYX-RapidAPI-Host→RAPIDAPI_HOST(should betwitter241.p.rapidapi.com)
cp .env.example .envOpen .env and fill in everything you collected above. A working local config looks like this:
# Discord
DISCORD_TOKEN=MTQxxxxxxxxxxxxxxxxxxxxxxxxxxx.GxxxxX.xxxxxxxxxxxxxxxxxxxxxxxxxx
DISCORD_CLIENT_ID=1234567890123456789
# Firebase
GOOGLE_APPLICATION_CREDENTIALS=./serviceAccountKey.json
FIREBASE_PROJECT_ID=tribe-connect-abc12
# X OAuth
X_CLIENT_ID=eVFtb0RMRxxxxxxxxxxxxxxxxxxxxx
X_CLIENT_SECRET=cmqOYDes2snxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
X_REDIRECT_URI=http://localhost:3100/auth/x/callback
X_SCOPES=tweet.read users.read like.read offline.access
X_BEARER_TOKEN=
# RapidAPI
RAPIDAPI_KEY=252b7e5f8amshxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
RAPIDAPI_HOST=twitter241.p.rapidapi.com
RAPIDAPI_BASE_URL=https://twitter241.p.rapidapi.com
RAPIDAPI_TIMEOUT_MS=10000
RAPIDAPI_MAX_PAGES=3
# Public URL
BOT_PUBLIC_URL=http://localhost:3100
PORT=3100A full description of each variable is in Section 17.
Critical:
X_REDIRECT_URImust be character-for-character identical to the Callback URI you configured in the X Developer Portal. A trailing slash mismatch will break OAuth.
npm install
npm startYou should see:
✅ Firebase credentials written to file (Linux/macOS only — see note)
✅ Logged in as Tribe Connect#0000
✅ Auth server listening on http://localhost:3100
Health check: http://localhost:3100/health
✅ Setting up cron jobs for Tribe Connect#0000
Windows note:
npm startrunschmod +x start.sh && ./start.sh && node src/index.js. On Windows,chmodand./start.shwon't work in PowerShell/cmd. Either:
- Run the bot directly:
node src/index.js, or- Use Git Bash / WSL to run
npm start.
start.shexists for Railway's Linux containers; locally you don't need it because your service account JSON is already on disk.
Verify the health endpoint in a separate terminal:
curl http://localhost:3100/health
# → okIf anything throws on boot, jump to Troubleshooting.
Slash commands must be registered with Discord before they appear in the UI. This is separate from the bot running.
node src/deploy-commands.jsExpected output:
Started refreshing N application (/) commands.
✅ Successfully reloaded N application (/) commands.
Re-run this any time you add/modify commands in src/commands.js. Global commands can take up to an hour to propagate; a freshly-deployed command usually appears within a few minutes.
- Paste the OAuth2 URL you generated in Step 4.3 into a browser.
- Pick a server you have Manage Server permission on (create a private test server if you don't have one).
- Click Authorize.
The bot should now appear in the server's member list (offline until you start the bot locally or remotely).
With npm start still running:
- In your test server, type
/and confirm Tribe Connect commands appear. - Run
/initiate. This creates the marketplace, tweets, raffles, and intro channels along with theTribe Channelsrole. - Run
/connect x. You should get a button that openshttp://localhost:3100/auth/x/start?discordId=…and redirects to X. Authorize, then verify you land on the styled success page. - Run
/profile. You should see your Discord/X linkage. - Run
/engage points repost:2 comment:3to set point values. - Run
/tweet <any tweet URL>to spawn an engagement dashboard.
If all six work, your local setup is healthy.
git add .
git commit -m "Initial commit"
git remote add origin git@github.com:YOUR-ORG/tribe.git
git push -u origin mainConfirm .env and serviceAccountKey.json are not in the commit (run git ls-files | grep -E '\.env$|serviceAccountKey'; output should be empty).
- Go to https://railway.app/dashboard → New Project → Deploy from GitHub repo.
- Authorize Railway against your GitHub if prompted, then pick the
triberepository. - Railway autodetects Node.js (Nixpacks) and starts building. Wait for the first build to finish — it will crash because env vars aren't set yet. That's expected.
- Project → Settings → Networking → Generate Domain.
- Copy the URL (e.g.
https://tribe-connect-production-abc123.up.railway.app). You'll need it in the next step.
Project → Variables tab → Raw Editor, paste this (substitute your real values):
DISCORD_TOKEN=<from Step 4.2>
DISCORD_CLIENT_ID=<from Step 4.1>
FIREBASE_PROJECT_ID=<from Step 5.1>
GOOGLE_APPLICATION_CREDENTIALS=/app/firebase-credentials.json
GOOGLE_APPLICATION_CREDENTIALS_JSON=<see 13.5 below>
X_CLIENT_ID=<from Step 6.4>
X_CLIENT_SECRET=<from Step 6.4>
X_REDIRECT_URI=https://<your-railway-domain>/auth/x/callback
X_SCOPES=tweet.read users.read like.read offline.access
RAPIDAPI_KEY=<from Step 7>
RAPIDAPI_HOST=twitter241.p.rapidapi.com
RAPIDAPI_BASE_URL=https://twitter241.p.rapidapi.com
RAPIDAPI_TIMEOUT_MS=10000
RAPIDAPI_MAX_PAGES=3
BOT_PUBLIC_URL=https://<your-railway-domain>
PORT=3000GOOGLE_APPLICATION_CREDENTIALS_JSON is special: it holds the entire JSON of your service account.
- Open
serviceAccountKey.jsonin a text editor. - Copy the entire file contents (everything from the first
{to the last}). - In Railway, set
GOOGLE_APPLICATION_CREDENTIALS_JSONto that value (the Raw Editor accepts multi-line JSON without issue).
When the container boots, start.sh writes that JSON to /app/firebase-credentials.json, which GOOGLE_APPLICATION_CREDENTIALS already points to. No code changes needed.
Railway redeploys automatically after variables change. Watch Deployments → View Logs; you want to see the same boot sequence from Section 9.
Slash commands are deployed once per application (not per host), so if you already ran node src/deploy-commands.js locally against the same DISCORD_CLIENT_ID/DISCORD_TOKEN, they're already live. If you skipped local testing, run it now against your production env:
# From your local machine, with .env temporarily holding the production token, run:
node src/deploy-commands.jsX allows multiple callback URIs per app. Add the production one alongside the local one:
- https://developer.x.com/en/portal/dashboard → your app → User authentication settings → Edit.
- Under Callback URI / Redirect URL, add:
https://<your-railway-domain>/auth/x/callback - Save.
The X library on the bot side picks whichever URI matches X_REDIRECT_URI in your env — keep both registered if you want both local and prod working.
curl https://<your-railway-domain>/health
# → okIn Railway logs you should see exactly the local boot output. From your Discord server:
/profilereturns without error./connect xopens the X OAuth flow on your production URL./leaderboardrenders (will be empty if no one has points yet).
If /connect x opens but X returns "redirect_uri mismatch", X_REDIRECT_URI in Railway doesn't match what's registered in the X Developer Portal — fix one to match the other.
Run these in order in the server where you'll actually use the bot:
| Command | Purpose |
|---|---|
/initiate |
Creates the marketplace, tweets, raffles, intro, and logs channels plus the Tribe Channels role. |
/engage points repost:<n> comment:<n> early_bonus:<n> |
Sets point payouts per action. |
/announce enabled:true role:@Tribe |
(optional) Tags a role when a new tweet dashboard is posted. |
/authorize add role:@Mods |
(optional) Grants non-admins access to manager commands. |
/marketplace show |
Renders the marketplace UI in the marketplace channel. |
/marketplace add name:"Custom Role" price:1000 role:@VIP |
Adds your first item. |
You're live.
| Variable | Required? | Default | What it does |
|---|---|---|---|
DISCORD_TOKEN |
Yes | — | Bot login token. From Discord Developer Portal → Bot. Never commit. |
DISCORD_CLIENT_ID |
Yes | — | Discord application ID. Used by deploy-commands.js to register slash commands. |
FIREBASE_PROJECT_ID |
Yes | — | Firebase project ID (e.g. tribe-connect-abc12). |
GOOGLE_APPLICATION_CREDENTIALS |
Yes | — | Path to the service account key JSON file. Local: ./serviceAccountKey.json. Railway: /app/firebase-credentials.json. |
GOOGLE_APPLICATION_CREDENTIALS_JSON |
Railway only | — | Full JSON contents of the service account. start.sh writes this to the path above at boot. Unused locally. |
X_CLIENT_ID |
Yes | — | OAuth 2.0 client ID from the X Developer Portal. |
X_CLIENT_SECRET |
Yes | — | OAuth 2.0 client secret. Never commit. |
X_REDIRECT_URI |
Yes | — | OAuth callback. Must match the X Developer Portal verbatim. |
X_SCOPES |
No | tweet.read users.read like.read offline.access |
OAuth scope list. Don't change unless you intentionally need more/less access. |
X_BEARER_TOKEN |
No | — | App-only Bearer for occasional public endpoints. Most flows go through RapidAPI; safe to leave blank. |
RAPIDAPI_KEY |
Yes | — | Twitter241 RapidAPI key. The bot throws on boot if missing. |
RAPIDAPI_HOST |
No | twitter241.p.rapidapi.com |
RapidAPI host header. |
RAPIDAPI_BASE_URL |
No | https://twitter241.p.rapidapi.com |
Base URL for RapidAPI requests. |
RAPIDAPI_TIMEOUT_MS |
No | 15000 |
Per-request timeout in ms. |
RAPIDAPI_MAX_PAGES |
No | 3 |
How many pages to walk when fetching retweeters/commenters. |
BOT_PUBLIC_URL |
Yes | — | Base URL the bot tells users to visit for OAuth. Must be reachable from the user's browser. |
PORT |
No | 3000 |
Port the express auth server binds to. Railway sets this automatically; match it to BOT_PUBLIC_URL locally. |
src/config.js will throw on startup if any Yes variable is missing.
A new developer typically modifies only these files during setup. Everything else can stay untouched.
| File | When you change it | What to do |
|---|---|---|
.env |
Always | Copy from .env.example, paste real secrets. Gitignored. |
serviceAccountKey.json |
Always | Drop the Firebase service account file here. Gitignored. |
.env.example |
If you add a new env var | Document it so the next developer knows it exists. |
src/commands.js |
When adding a slash command | Register the new SlashCommandBuilder. Re-run node src/deploy-commands.js after. |
src/index.js |
When adding a slash command | Wire the new command name to its handler in the InteractionCreate block. |
src/handlers/<name>.js |
When adding a slash command | Create the handler. |
package.json |
If adding npm deps | npm install <pkg> updates this. |
railway.json |
Rarely | Only if you need a custom build/start command. |
start.sh |
Rarely | Only if the Railway boot sequence changes. |
You should never need to edit anything under src/firebase/, src/auth/, src/rapid/, or src/discord/ for a routine setup — those are infrastructure.
Missing env var: X — Your .env doesn't have variable X (or, on Railway, the Variables tab doesn't). Check Section 17.
Missing RAPIDAPI_KEY in .env — src/rapid/rapidClient.js throws this on import if RAPIDAPI_KEY isn't set. Same fix.
Error: Could not load the default credentials — firebase-admin can't find the service account. Verify GOOGLE_APPLICATION_CREDENTIALS points to an existing file with valid JSON. On Railway, double-check that GOOGLE_APPLICATION_CREDENTIALS_JSON was pasted in full (curly braces included) and that start.sh ran (look for ✅ Firebase credentials written to file in the logs).
"Something went wrong" on X's page — X_REDIRECT_URI doesn't match what's registered in the Developer Portal. They must be byte-identical: scheme, host, port, path, no trailing slash differences.
Bot says "Connect your X account first" after a successful OAuth — Firestore writes are failing silently. Check Firestore Console → Data → users collection; if it's empty after a connect attempt, the service account doesn't have permission. Re-download the service account key.
Commands don't appear in Discord — You need to run node src/deploy-commands.js. Wait 1–2 minutes after running. If still missing after 10 minutes, kick the bot and re-invite it.
Only some commands appear — You added a command to src/commands.js but didn't re-deploy. Re-run the deploy script.
"X API is temporarily busy" — RapidAPI rate-limited the bot. The client already retries with backoff; if it persists, you've exceeded your RapidAPI plan's per-minute quota. Either upgrade the plan or reduce RAPIDAPI_MAX_PAGES.
Verify returns repost: ❌ even though you reposted — RapidAPI caches data and may lag 1-2 minutes behind X. Ask the user to wait and try again.
The bot prints a detailed permission diagnostic when it hits Missing Permissions (50013). Read the output — it tells you exactly which permission is missing and whether the bot's role is positioned correctly in the role hierarchy. Most fixes are:
- Move the bot's integration role above the
Tribe Channelsrole in Server Settings → Roles. - Re-run
/initiateso channel overrides are reapplied.
You missed Step 5.3. Click the link in the error message — it opens the Firebase Console pre-configured with the exact index you need.
- Hot reload: This project doesn't include a watcher. Use
nodemon:npm install -D nodemonthen runnpx nodemon src/index.js. - Inspect Firestore: Use the Firebase Console's Data tab — collections are
users/,guilds/,tweets/,oauth_states/. - Resetting your dev data: Delete documents directly in the Firebase Console.
- Multiple Discord apps: If you want a separate dev bot, repeat Section 4 for a "Tribe Connect Dev" app and keep two
.envfiles (.envand.env.prod).
You're done. Once npm start boots clean locally and Railway shows green, point a community at the bot and let /connect x do the rest.