Ethan Meza
portfolio / stack
← portfolio

case study · discord bot

Stack

I built a Discord bot for TAMU ColorStack's 100+ member server. Members fill out one profile form, check in to events with a short code, and look up their own points. Officers don't touch attendance data at all.

tl;dr

role
sole developer + product owner
timeline
Aug 2026 – present
users
100+ server members and every officer
stack
Python, discord.py, SQLite, Notion API, Docker, Fly.io
100+members in the server
~85%of active members verified
5 min → <1time to check in
110check-ins in the first 4 weeks

01 the problem

The chapter talks on Discord, but attendance lived in Airtable.

members

  • Filled out an Airtable form at every event, typing their name, email, major and class year again each time.
  • Waited for an officer to give them their server roles by hand.
  • Couldn't see their own points.

officers

  • Had to dig through Airtable to see who came to what, while everything else they work on is in Notion.
  • Counted points by hand from those form responses.

The form was long enough that people skipped it, so the attendance numbers were low before anyone even looked at them.

02 users & research

Before writing code I talked to the officer board and to members after meetings, and I kept asking them between releases. A few things came up over and over:

  • Members skipped the form when they were in a hurry. One told me, "If I have to type my major again I'm just not doing it."
  • Officers wanted the data in Notion, where they already plan events. "I'd have to export the Airtable to figure out who had enough points for anything."
  • Members don't want to leave Discord. Every link out to a form lost people, so the whole flow had to happen in the server.
  • The member records are messy. Nicknames, typos and personal vs. school emails meant exact matching would lock real members out.
  • At a full meeting, anything slower than typing a short code backs up the room.

03 goals & success metrics

goalhow i measured ittargetresult
active members verified% of active members linked to a directory row80%~85%
fast check-intime for one member to check inunder 1 minunder 1 min (was ~5)
no manual point trackingofficer time spent on attendance and points~0~0
members can do it alonemembers who needed help to verifya handful3
always onoutages this semester00

04 key product decisions

where the data lives

  • keep Airtable
  • Notion only
  • SQLite + Notion sync

Officers already work in Notion, so the data had to end up there. Notion's API is rate-limited, though, and a bot that waits on it would stall when a room checks in at once. SQLite takes every write instantly, and a sync worker copies changes into Notion.

Tradeoff: two copies of the data means sync logic, which is where the race condition came from (see architecture).

one profile form, filled out once

  • a form per event
  • one Notion member form + a check-in code

The Airtable form asked for the same details at every event. Now members fill out the Notion member form once, and the bot links it to their Discord account. After that, checking in is a single command with the event code.

how strict verification should be

  • exact email match only
  • officer approves everyone
  • exact match → fallback match → officer review

Exact matching locked out real members with messy records, and approving everyone by hand doesn't scale to a 100+ person server. The bot tries an exact match, then a looser one, and only sends what's left to an officer.

how members check in

  • react to a message
  • scan a QR code
  • officer-generated check-in code

Anyone can click a reaction from home, and QR needs a screen at every event. A short, time-limited code read out in the room shows you were there and works at a volleyball practice as well as a general meeting. QR is next on the roadmap as an extra option.

who can do what

  • hardcoded officer IDs
  • one admin role
  • member / officer / admin tiers mapped to Discord roles

The officer board changes every year. Because permissions follow Discord roles, handing the bot over means reassigning roles, with no code change or redeploy.

05 technical architecture

membersdiscord
Stack botpython · discord.pydocker on fly.io
SQLitesource of truth
officersread + edit
Notiondirectory · event log · points
Members only use Discord. The bot writes to SQLite right away, and a locked sync worker copies changes into Notion for officers.

SQLite keeps check-ins fast and correct. Notion is where officers read the data. The bot never waits on Notion before answering a member.

the sync race condition for engineers

Early on, two things could touch the same member at once: a check-in awarding points and the sync worker pushing that member to Notion, or two check-ins landing together. Both read the row, both computed a new total, and whichever wrote last won. Points disappeared without any error.

I fixed it with a per-member asyncio.Lock around every read-modify-write and a version number on each row. The sync worker takes the same lock, snapshots the row, pushes it to Notion, and only clears the dirty flag if the version is unchanged.

async with locks[member_id]:
    row = db.get(member_id)
    row.points += award
    row.version += 1
    row.dirty = True
    db.save(row)
identity verification for engineers

New members see a "Get Verified" message. It's registered as a persistent view, so the button still works after restarts and redeploys. Clicking it opens a modal for name and email, and then the matcher runs:

  1. exact email match against the directory
  2. normalized fallback: lowercased, trimmed, school vs. personal email, name similarity
  3. no confident match: queue it for an officer with the closest candidates attached

A match gives the member the Member role and their class-year role and links their Discord ID to the directory row.

permissions design for engineers

Every command declares a minimum tier: member, officer or admin. One check maps the user's Discord roles to a tier before the command runs, so members never see officer commands like starting attendance or adjusting points. A new officer gets access as soon as their role changes.

deployment for engineers

The bot runs in a Docker container on Fly.io, with the SQLite file on a persistent volume and the bot token and Notion key in Fly's secret store. Shipping a change is one deploy, and no laptop has to stay on. It has run since August without an outage.

06 execution & iteration

I shipped small versions, watched how officers and members used them, and let their feedback decide what came next.

  1. v1
    verification + roles. Linked Discord accounts to the directory and assigned roles automatically. Learned: exact matching failed on messy records.
  2. v2
    fallback matching + officer review. Most members verified on the first try, and the rest went to a short officer queue.
  3. v3
    attendance codes + points. Replaced the per-event Airtable form. An officer starts attendance with one command, members type the code, and points show up right away.
  4. v4
    SQLite + Notion sync. Moved everything off Airtable into a shared Notion workspace, with SQLite underneath for instant writes. Fixed the sync race condition with locks.
  5. v5+
    points by event type, leaderboard, role tiers. General meetings are worth 3 points and smaller events 1. Members can check their standing any time, and officers got a screen-capture tutorial for running attendance.

07 results

Numbers from the Notion event log, Sep 13 to Oct 6, 2026:

13events run on check-in codes
110check-ins logged
186points awarded automatically
~85%of active members verified
5 min → <1time to check in
0outages on fly.io
  • Officers spend no time on attendance or points now. The event log and member directory in Notion fill themselves in.
  • Even small events use codes. Volleyball practice, dodgeball, and the weekly web dev and LeetRooms sessions are worth 1 point, and members still check in.
  • 64 members have filled out the one-time profile form in the Notion directory.
  • Only 3 members have needed help getting verified, and I rarely get asked to fix anything.

08 what's next & what i learned

roadmap

  • QR code check-in as a second option next to codes
  • more analytics views in Notion for officers, like attendance trends and who's dropping off

lessons

  • How easy something is to use decides how often people use it. When checking in took five minutes of retyping, members skipped it. At under a minute, they check in at volleyball practice for a single point.
  • Design for the messy data you have. Clean records were never coming.
  • SQLite and one lock fixed the lost-points bug.
  • Ship small and let feedback pick the next version.

heads up ⌖

this site is designed for desktop.