BU Robotics Club · setup from nothing

Setting Up the Bunker Bot

Everything below happens in a browser. No terminal, no Node, no wrangler. Budget an hour for the first run. Do the steps in order — later ones depend on values you collect earlier.

Who owns what

You'll end up with five things. Every one of them should be owned by the club, not by you.

ThingWhereOwner
Notion integrationnotion.so/my-integrationsThe club Notion account
Discord applicationdiscord.com/developersA Discord Team, never a person
Cloudflare Worker, KV, R2dash.cloudflare.comThe club Gmail
Groq and Gemini API keysconsole.groq.com · aistudio.google.comThe club Gmail
worker.js and these docsgithub.com/<club>The club org

Do the ownership steps first, not last. Transferring a Discord app to a Team, or moving a Cloudflare account, is annoying once things depend on it. Every one of these is a five-minute job on day one and a bad afternoon in eighteen months.

To copy IDs out of Discord you need Developer Mode on: User Settings → Advanced → Developer Mode. Keep a scratch note open — you'll collect about twenty values.

Notion

  1. Create the integration. notion.so/my-integrations → New integration → name it Bunker Bot → the club workspace → Internal. Under Capabilities tick Read, Update and Insert content. Save and copy the Internal Integration Secret — it starts ntn_. That's NOTION_TOKEN.
  2. Connect the databases. Open the Club Operations page → ••• → Connections → Bunker Bot. That grants every database under it at once. Then check each of the eleven below individually — a database created after the page was connected is not connected automatically.
  3. Collect the data source IDs. They're fixed for this workspace and already filled in below; if you ever rebuild a database, its ID changes and the matching variable has to follow.
DatabaseVariableID
Bunker InventoryINVENTORY_DS37c50591-221f-80c8-ae9d-000bebcea235
Bunker Checkout LogLEDGER_DSd900e4a9-df58-4de8-baa0-60c389fdaf6d
ProjectsPROJECTS_DSfd8587db-eafe-4dd3-99cf-a5d1d688bbb1
MeetingsMEETINGS_DSd9f5fa89-e8de-4e20-b5e0-83b69f912ca2
MembersMEMBERS_DSe935e4bb-cbc6-4a92-a2f1-e086bec48a55
AttendanceATTENDANCE_DS9c1d2b64-66de-4963-a69f-8c8dc37051ba
TasksTASKS_DSf5140aa3-138a-4f57-adf7-d89c961bad4f
Purchase RequestsPURCHASES_DS2cb13198-de4b-4abf-86fc-295d3da631d2
Bring RequestsREQUESTS_DScee8c7c0-cf01-4e28-b153-a8e62a2c96b0
Nudge TemplatesTEMPLATES_DS18970188-f9bd-404b-bf2a-a999b760e00b
Member FeedbackFEEDBACK_DS9c7027ce-591e-433c-adf5-a4284285f3f3

The first three are required; the Worker refuses to start without them. The rest each switch on one feature and the bot says what's missing if you use a command that needs one.

Discord

  1. Create the application. discord.com/developers/applications → New Application → Bunker Bot.
  2. Move it to a Team, now. Settings → Team → create or pick one → transfer. An app owned by a person's account dies with that account.
  3. Copy two values from General Information: Application IDDISCORD_APP_ID, Public KeyDISCORD_PUBLIC_KEY.
  4. Bot → Reset Token → copy it (shown once) → DISCORD_BOT_TOKEN. Turn Public Bot off.
  5. Invite it. OAuth2 → URL Generator → scopes bot and applications.commands → permissions Send Messages, Embed Links, Manage Events → open the URL → pick the server.
  6. Collect three IDs from the server: right-click the server → Copy Server ID → DISCORD_GUILD_ID. Server Settings → Roles → E-Board → Copy Role ID → EBOARD_ROLE_ID. You'll collect channel IDs in a later step.

Leave the developer tab open. You come back for the endpoint URL after the Worker exists.

The Worker

  1. Create it. dash.cloudflare.com on the club Gmail → Workers & Pages → Create → Hello World → name it bunker-bot → Deploy. Note its URL: https://bunker-bot.<something>.workers.dev. That's PUBLIC_BASE, with no trailing slash.
  2. Paste the code. Edit code → select all → paste the whole of worker.js → Deploy.
  3. Bind the storage. Settings → Bindings → Add:
    Binding nameTypeCreate it as
    CACHEKV namespacebunker-cache
    IMGR2 bucketbunker-images
    AUDIOR2 bucketbunker-audio
    The names on the left must match exactly — the code looks for env.CACHE. Photos, recordings, transcripts and the label queue all live in these; nothing else is stored anywhere.
  4. Variables and secrets. Settings → Variables and Secrets. Use Secret for anything marked 🔒.
    NameValue
    DISCORD_APP_IDfrom Discord
    DISCORD_PUBLIC_KEYfrom Discord
    DISCORD_BOT_TOKEN🔒from Discord
    DISCORD_GUILD_IDthe server
    EBOARD_ROLE_IDthe E-Board role
    NOTION_TOKEN🔒ntn_…
    REGISTER_SECRET🔒make one up — 20 random characters
    PRINT_SECRET🔒a different random string; it goes on the Pi
    PUBLIC_BASEthe Worker's own URL, no trailing slash
    TZ_OFFSET-4
    INVENTORY_DS … FEEDBACK_DSthe eleven IDs above
  5. The cron. Settings → Triggers → Cron Triggers → Add → */10 * * * *. Every scheduled job hangs off this one trigger.
  6. Deploy again. Variables and bindings do not take effect until you do. This is the single most common way to lose twenty minutes.
  7. Smoke test. Open PUBLIC_BASE/health. missing should be empty and items should be the size of the inventory. If Notion is failing, fix it here before touching Discord.

Connect them

  1. Point Discord at the Worker. Developer portal → General Information → Interactions Endpoint URL = PUBLIC_BASE/ with the trailing slash → Save. Discord sends a signed ping and a deliberately bad one; if it saves, signature verification works. If it refuses, DISCORD_PUBLIC_KEY is wrong or the Worker isn't deployed.
  2. Register the commands. Open PUBLIC_BASE/register?secret=<REGISTER_SECRET>. Expect status: 200. Commands appear in the server instantly. Re-run this every time a command or one of its options changes.
  3. Try it. /help in any channel. Then /find something.

Model keys

Two free API keys, both on the club Gmail. Without them everything works except /notes.

VariableGet it atDoes
GROQ_API_KEY 🔒console.groq.comTranscribes recordings. Free tier: 25 MB per file.
GEMINI_API_KEY 🔒aistudio.google.comWrites the minutes and extracts tasks. Reads a whole meeting in one go, which Groq's free tier cannot.

You don't need to pick a Gemini model. The Worker queries the live model list, picks the best flash-class one, and remembers it — and if that model is ever retired it rediscovers by itself. GEMINI_TEXT_MODEL exists if you ever want to pin one.

Channels

Six channels, all optional, each one a variable. Make the private ones private and add the bot to them — a bot that isn't in a channel posts nothing and says nothing about it.

VariableSuggested nameVisibilityFalls back to
LOG_CHANNEL_ID#bunker-logpublic
APPROVALS_CHANNEL_ID#bunker-approvalsE-Boardlog
ASSIGN_CHANNEL_ID#assignmentsE-Boardapprovals
TASKS_CHANNEL_ID#tasksE-Boardassignments
MINUTES_CHANNEL_ID#minutesmemberslog
NUDGE_CHANNEL_ID#retentionE-Boardapprovals
ERRORS_CHANNEL_ID#bot-errorswhoever owns the botnothing — errors are lost

Right-click a channel → Copy Channel ID. Add the variables, deploy, and post the permanent check-in button once with /checkin-button, then pin it.

The printer

A Raspberry Pi in the bunker polls the Worker for labels and prints them. It needs only outbound Wi-Fi — no port forwarding, no domain, no tunnel.

  1. Raspberry Pi OS on the Pi 3, on the bunker Wi-Fi, SSH on.
  2. Copy the bunker-labels folder over and run bash install.sh. It installs the dependencies, renders a preview label, and installs the service.
  3. Edit config.toml. [worker] base is PUBLIC_BASE; secret is PRINT_SECRET. Leave the backend on dryrun for now.
  4. sudo systemctl start labeld, then in Discord /label item:BURC-0001. The Pi's log (journalctl -u labeld -f) should show the job and write a PNG to out/. /health now shows printer_last_seen.
  5. Pick a printer. The README in the folder walks through it — the Epson over Ethernet (a ten-minute test says whether it needs a driver or not), a Brother QL over USB, or anything CUPS can see. Set the backend, restart the service, print one, stick it on something.

Why the QR encodes PUBLIC_BASE/i/BURC-0001 and not the Notion page. It's shorter, so the QR is smaller and works on narrow tape. And it's permanent: if the club ever moves off Notion, one line in the Worker re-points every label ever printed. Print nothing until PUBLIC_BASE is set.

These pages

Hosted on the same Cloudflare account, free, with no domain needed.

  1. Cloudflare dashboard → Workers & Pages → Create → PagesUpload assets.
  2. Name it bunker-docs. Drag the docs folder in. Deploy.
  3. It's live at https://bunker-docs.pages.dev. Pin that in #general.

To update, upload the folder again — same project, new deployment, same URL. If the club ever gets access to its domain, Pages → Custom domains adds it in two clicks; nothing else changes.

Updating later

  1. Check the file as a module first: cp worker.js /tmp/x.mjs && node --check /tmp/x.mjs. Plain node --check worker.js parses it as CommonJS and misses module-only syntax errors.
  2. Edit code → select all → paste → Deploy.
  3. New variable or binding? Deploy again.
  4. New command or option? /register?secret=….
  5. New Notion database? Connect it to the integration.
  6. Open /health and read it.
  7. Twice a year: TZ_OFFSET is -4 from March and -5 from November. Nobody else will remember.