1. What This Site Is

bhs1977reunion.com is the official website for the Bettendorf High School Class of 1977 50th Reunion, scheduled for October 15–17, 2027 at the Hotel Blackhawk in Davenport, Iowa.

The site serves two audiences:

  • Classmates — can check in, browse the directory, post memories, play the yearbook photo game, register as veterans, and view the In Memoriam page.
  • Committee members — have access to these admin pages for managing classmate records, sending bulk emails, moderating content, tracking analytics, and coordinating reunion planning.
The event date is October 15–17, 2027. The website will be the primary communication channel between now and the reunion. Keep it running, keep the email list healthy, and send regular updates to classmates via the Broadcast tool.

Tech Stack in Plain English

TermPlain English
AstroThe framework that builds the website's HTML pages. You don't need to touch this unless you're adding new pages.
NetlifyThe company that hosts and serves the website. It also runs the server-side logic ("functions") that power things like check-in and email.
SupabaseThe database where all classmate records, photos URLs, memory posts, and analytics live. Think of it as the filing cabinet.
Cloudflare R2Cloud storage for photos (yearbook portraits, memory wall images). Like a shared hard drive in the cloud.
ResendThe email delivery service used for bulk broadcasts to classmates.
GitHubWhere the website's source code lives. Every change is tracked here. Pushing to GitHub triggers a new deploy.

2. Services & Accounts

The site depends on six external services. Each has its own login. See the Passwords Annex for credentials.

Netlify

app.netlify.com → Sites → bhs1977reunion

What it does: Hosts and deploys the website. Every time code is pushed to GitHub, Netlify automatically rebuilds and re-deploys the site within 2–3 minutes.

What you'll do there:

  • Check deploy status (did the latest build succeed?)
  • View/edit environment variables (the secret keys that power every feature)
  • Manage Netlify Identity users (classmate accounts)
  • View function logs when something breaks
Critical: The environment variables in Netlify are the master keys to everything. If you lose them and the site goes down, you cannot recover without them. Keep the Passwords Annex up to date.

How to check if the site is working

  1. Go to app.netlify.com and sign in.
  2. Click the site "bhs1977reunion".
  3. The "Production deploys" list should show a green "Published" badge with a recent timestamp.
  4. If it shows "Failed", click on the failed deploy to read the build log and find the error.

How to add or change an environment variable

  1. In Netlify, go to Site configuration → Environment variables.
  2. Click "Add a variable" or find the existing one and edit it.
  3. You must trigger a new deploy for the change to take effect: go to Deploys → "Trigger deploy".

GitHub

github.com/ZoomMimi/bhs77-reunion

What it does: Stores the website source code and tracks every change ever made. Pushing new code here is what triggers a Netlify deploy.

What you'll do there:

  • Browse the code if you need to understand how something works
  • Pull changes if you're doing local development
  • Push updates to deploy a change to the live site
Never commit .env files or API keys to GitHub. All secrets live in Netlify's environment variable settings, not in the code.

The deploy workflow

  1. Make a code change locally on your computer.
  2. Run git add <file>, then git commit -m "describe what changed".
  3. Run git push origin main.
  4. Netlify detects the push and starts building within 30 seconds. Watch status at app.netlify.com.
🗄

Supabase

supabase.com → Project: bhs1977reunion

What it does: This is the database — the single source of truth for every classmate record, photo URL, memory post, game score, and analytics event.

What you'll do there:

  • Browse/edit classmate records directly if the admin UI can't handle an edge case
  • Run SQL queries to answer one-off questions ("how many people checked in this week?")
  • Download a database backup (do this monthly)
  • Monitor database size and API usage

How to back up the database

  1. Go to supabase.com and open the project.
  2. Click "Database" in the left sidebar, then "Backups".
  3. Supabase creates automatic daily backups on paid plans. You can also download a manual export.
  4. For a full export: go to the SQL Editor and run pg_dump, or use the Supabase CLI.

Key tables to know

TableContains
classmatesEvery classmate — name, email, phone, photo URL, check-in status, attendance interest
memory_postsMemory Wall posts
veteran_profilesVeterans honor roll submissions
email_logHistory of every broadcast email sent
game_best_scoresYearbook photo game leaderboard
app_settingsGlobal config including the Gmail OAuth refresh token

Cloudflare R2

dash.cloudflare.com → R2 Object Storage

What it does: Stores all photos — yearbook portraits, colorized portraits, and memory wall images. Photos are served directly from Cloudflare's global CDN, which means they load fast worldwide.

Two buckets:

  • bhs1977-yearbook-photos — all yearbook portraits (black & white originals, colorized versions, thumbnail variants)
  • bhs1977-memory-photos — photos attached to Memory Wall posts

What you'll do there (rarely):

  • Check how much storage is being used (free tier: 10 GB)
  • Manually delete orphaned files if needed
  • Regenerate API keys if they're compromised (then update Netlify env vars)
R2 has a generous free tier (10 GB storage, 1 million reads/month). At current usage this site won't hit limits before the reunion. No action needed unless you add a large volume of new photos.

Resend

resend.com → API Keys + Broadcasts

What it does: Sends bulk email to classmates. When you use the "Broadcast" admin page to send a newsletter or announcement, Resend is the engine that delivers it.

What you'll do there:

  • Monitor delivery rates and bounce counts after a broadcast
  • Check if emails are landing in spam (look at the engagement rate)
  • Regenerate the API key if needed (then update Netlify env vars)

The sending address is committee@bhs1977reunion.com, which is verified with Resend via DNS records. Don't change these DNS records or email will break.

G

Google / Gmail

50thbhs1977reunion@gmail.com

What it does: The committee Gmail account is connected to the site via OAuth so the admin Gmail page can read the inbox, reply to classmates, and send personalized outreach emails. It's also where classmates email the committee directly.

Important: The Gmail connection uses an OAuth "refresh token" stored in the database. If the Google account password changes or someone revokes the app's access, the Gmail admin page will stop working. To reconnect:

  1. Go to /admin-gmail and click "Connect Gmail".
  2. Sign in with the committee Google account.
  3. Approve the permissions. The new refresh token is saved automatically.
🌐

Domain Name (bhs1977reunion.com)

See Passwords Annex for registrar

What it does: The domain name is the website's address. It's registered with a domain registrar and pointed at Netlify via DNS records.

Domain renewal is critical. If the domain expires, the website goes dark immediately. Set auto-renewal on and make sure the registrar has a current credit card and email address. The renewal date and registrar are in the Passwords Annex.

DNS records to never delete:

  • A / CNAME records pointing to Netlify — these serve the website
  • TXT records for email (SPF, DKIM, DMARC) — these prevent emails from going to spam

3. How It All Fits Together

The request lifecycle (what happens when someone visits the site)

  1. Browser → Netlify CDN: The visitor's browser requests a page. Netlify serves the pre-built HTML from its CDN — this is fast because no server needs to "think".
  2. Client-side check-in gate: The page checks localStorage for a bhs-checkin-done key. If missing, the visitor is redirected to the check-in page.
  3. Check-in flow: Classmate enters their email → site looks them up in Supabase → sends a 6-digit code via Resend → classmate enters code → they're in.
  4. Dynamic data: Once inside, the page calls Netlify Functions (server-side code) to fetch live data from Supabase — classmate directory, memory posts, leaderboard, etc.
  5. Photos: Images are loaded directly from Cloudflare R2's CDN. The database stores the URL; the browser fetches the actual file from R2.

Authentication: two tiers

TierWhoHow it worksWhat they can do
Public Anyone No login required Homepage, In Memoriam, Contact form — always accessible with no barrier
Classmate Any class member Check-in form + 6-digit email code → bhs-checkin-done in browser storage Browse directory, post memories, play game, view veterans
Committee Committee members only Full Netlify Identity account (email + password) with "committee" role All classmate access + all admin pages

Committee accounts are created by an existing committee member going to Netlify → Identity → Invite user. The COMMITTEE_EMAILS environment variable controls which email addresses get the "committee" role automatically on signup.

How code changes reach the live site

Edit code locally git push to GitHub Netlify detects push Netlify builds site (2–3 min) Live site updated

You can watch this happen in real time at app.netlify.com → Deploys.

4. Admin Pages Guide

All admin pages require a committee-role Netlify Identity login.

Classmates /admin

The main dashboard. Search and filter all classmates, edit records, review pending check-in requests, find duplicates, and manage committee assignments. This is where you'll spend most of your time.

Tip: Use the filter bar to find specific subgroups (e.g. "attending but no email confirmed"). The status dots on each row show check-in, email, and attendance at a glance.
Analytics /admin-analytics

Dashboard of key metrics: roster health, email status breakdown, attendance interest, engagement over time, geographic distribution. Use this to guide outreach priorities.

Broadcast /admin-broadcast

Compose and send bulk emails to classmates. You can filter by audience (everyone, attending only, checked-in only, etc.), preview the email, send a test to yourself, and then send for real. Merge fields like {{FirstName}} personalize each message automatically.

Before sending: Always use "Count" to see how many will receive it, then "Test" to send yourself a preview. Never send without a test first.
Gmail /admin-gmail

Access the committee Gmail inbox (50thbhs1977reunion@gmail.com) directly in the browser without leaving the admin area. Read threads, reply, and send outreach emails to classmates. The "Outreach" tab lets you find classmates with no email confirmed and send personalized probes via Gmail.

Photos /admin-photos

Manage yearbook portraits. Upload a photo and match it to a classmate. Also contains the Scan Processor for extracting crops from full yearbook page scans. Most portrait work was completed in 2025–2026 — you may only need this occasionally.

Memory Wall /admin-memories

Moderate Memory Wall posts. Pin important ones to the top, delete inappropriate content, and view reports from classmates who flagged a post.

Veterans /admin-veterans

Review and manage the Veterans Honor Roll. Toggle a profile's public visibility, edit service details, and approve submitted profiles.

Photo note: A veteran's uniform/service photo is independent of their directory photo. The vet photo only appears on the Veterans page — the classmate directory and admin list always show the yearbook portrait.
Export /admin-export

Download a CSV of classmate data for use in spreadsheets, mail merges, or sharing with a caterer/venue. Filter by audience and choose which columns to include.

Team /admin-committee

Manage committee member accounts and assign classmates to specific committee members for personal outreach. The status board shows each assignment's progress.

Planning /admin-planning

Countdown to the event, survey result snapshot, and a task checklist for reunion logistics.

5. Common Tasks

How do I add a new committee member?
  1. Go to app.netlify.com → your site → IdentityInvite users.
  2. Enter their email address and click Invite. They'll receive a sign-up link.
  3. Add their email to the COMMITTEE_EMAILS environment variable (comma-separated list). Without this, they'll sign up as a regular member, not a committee member.
  4. After adding to COMMITTEE_EMAILS, trigger a new Netlify deploy (Deploys → "Trigger deploy").
  5. They can now log in and access all admin pages.
How do I send an email blast to classmates?
  1. Go to Broadcast.
  2. Choose your audience from the filter dropdown (e.g., "All with email confirmed").
  3. Type your subject and compose your message. Use {{FirstName}} to personalize.
  4. Click Count — verify the number of recipients looks right.
  5. Click Test — a copy goes to your own email. Review it carefully.
  6. Click Send. Done. Check Resend.com afterward for delivery stats.
Rate limit: There's a 10-minute cooldown between sends with the same subject and audience. If you hit it, wait and try again.
A classmate can't log in / isn't in the system — how do I add them?
  1. Go to Classmates and search for their name.
  2. If they're there but with the wrong email, click their record → edit the email field.
  3. If they're not there at all, click Add Classmate and fill in their details.
  4. Once their email is in the system, they can use the check-in form at bhs1977reunion.com/checkin to verify their account.
How do I handle a deceased classmate?
  1. Go to Classmates and find their record.
  2. Open their record and set Is Deceased to true.
  3. They will automatically move to the In Memoriam page and be hidden from the directory.
  4. Optionally upload an obituary image via the memorial admin tools.
A classmate has opted out of all contact but may still want to check the website — what do I do?

Use the Do Not Contact (public access OK) flag. This is different from a full block — it stops all outreach while leaving the website quietly available to them.

  1. Find the classmate in Classmates.
  2. Open their record and check Do Not Contact (public access OK).
  3. Save. They will now be excluded from all broadcasts and committee contact lists.
  4. The website's public pages (homepage, In Memoriam, contact form) remain accessible to them with no login required — they can quietly check back any time.
Philosophy: Respecting an opt-out doesn't have to mean slamming the door. A classmate who said "not interested" today may feel differently in six months. Leave the door open quietly.
A classmate's email is bouncing — what do I do?
  1. Go to GmailBounces tab. The system periodically scans for bounce replies and flags those addresses.
  2. Find the classmate in Classmates.
  3. Update their email address to a correct one if you have it, or clear the email field and note it in their record.
  4. The "Mark Corrected" button in the Bounces tab clears the bounce flag once the email is fixed.
The Gmail connection stopped working — how do I reconnect?
  1. Go to Gmail. You'll see a "Connect Gmail" button if the token is expired.
  2. Click it and sign in with 50thbhs1977reunion@gmail.com.
  3. Approve all the requested permissions.
  4. The new refresh token saves automatically. The Gmail tab should now show the inbox.

This can happen if: the Google account password changed, someone revoked the app's access in Google Account settings, or the token simply expired after a very long idle period.

The site is down — how do I diagnose it?
  1. Check Netlify first: app.netlify.com → your site → Deploys. Is the latest deploy green ("Published") or red ("Failed")?
  2. If Failed: click the deploy → read the build log → find the error. Usually it's a missing package or a syntax error in recently changed code.
  3. If Published but site is broken: open browser DevTools (F12) → Console tab → look for red errors. Then check the Network tab for failed API calls.
  4. If it's a specific feature broken (not the whole site): check Netlify → Functions → look for errors in the relevant function's log.
  5. If in doubt: roll back. In Netlify Deploys, find the last known-good deploy and click "Publish deploy" to instantly revert.
How do I make a code change without knowing how to code?

For simple text/content changes, you can use the GitHub web editor:

  1. Go to github.com/ZoomMimi/bhs77-reunion.
  2. Navigate to the file you want to edit (e.g., src/pages/event.astro for event details).
  3. Click the pencil icon to edit.
  4. Make your change, scroll down, and click "Commit changes".
  5. Netlify will automatically deploy within a few minutes.

For anything beyond simple text changes, work with a developer. The codebase is well-organized and a developer familiar with JavaScript/Astro can get up to speed quickly.

6. Environment Variables Reference

These are the secret keys configured in Netlify → Site configuration → Environment variables. Without them, the site's server-side features (check-in, email, photos, database) won't work. Never put these in code or commit them to GitHub.

VariableWhat it's forWhere to get a new one
Database (Supabase)
SUPABASE_URL The web address of the Supabase database. Starts with https:// and ends with .supabase.co Supabase dashboard → Project Settings → API
SUPABASE_SERVICE_ROLE_KEY The master admin key for the database. Never expose to the browser — server-side only. Supabase dashboard → Project Settings → API → service_role key
Photo Storage (Cloudflare R2)
R2_ACCOUNT_ID Your Cloudflare account identifier Cloudflare dashboard → R2 → "Account ID" in the right sidebar
R2_ACCESS_KEY_ID API username for R2 Cloudflare → R2 → Manage R2 API tokens → Create token
R2_SECRET_ACCESS_KEY API password for R2 — only shown once at creation Same as above — copy it immediately, can't be retrieved later
R2_YEARBOOK_BUCKET Name of the yearbook photo bucket. Value: bhs1977-yearbook-photos Static — only changes if the bucket is renamed
R2_MEMORY_BUCKET Name of the memory wall photo bucket. Value: bhs1977-memory-photos Static — only changes if the bucket is renamed
R2_YEARBOOK_URL Public CDN URL for yearbook photos. Value: https://pub-6bd15a0a68834b6099281409988528ea.r2.dev Cloudflare → R2 → click the bucket → "Public access" tab
R2_MEMORY_URL Public CDN URL for memory wall photos. Value: https://pub-a80d21e2dfa34e36a91a041b8c6fd32a.r2.dev Same as above for the memory bucket
Email Delivery (Resend)
RESEND_API_KEY API key for sending bulk email. Looks like re_xxxxxxxxxx resend.com → API Keys → Create API key
Gmail Integration (Google OAuth2)
GOOGLE_CLIENT_ID The app's Google identity — ends with .apps.googleusercontent.com console.cloud.google.com → Credentials → OAuth 2.0 Client IDs
GOOGLE_CLIENT_SECRET The app's Google secret key — short base64 string Same place as Client ID
AI / Claude (Anthropic)
ANTHROPIC_API_KEY Used for yearbook photo extraction via Claude's vision AI. Looks like sk-ant-v4-... console.anthropic.com → API Keys
Committee Configuration
COMMITTEE_EMAILS Comma-separated list of email addresses that get committee admin access on sign-up. E.g., alice@example.com,bob@example.com You maintain this list manually in Netlify env vars
COMMITTEE_EMAIL The primary committee contact address. Defaults to committee@bhs1977reunion.com Set to whatever the active committee email address is

7. Bus-Factor Recommendations

These are specific changes recommended to make handing off this site as painless as possible if the primary maintainer is suddenly unavailable.

Critical

1. Fill in the Passwords Annex on this page and print it

The Passwords Annex below is a template. Fill in every credential, print a copy, and store it in a sealed envelope with a trusted person (attorney, spouse, or another committee member). A digital copy should also be in a password manager that at least one other person has access to (e.g., 1Password, Bitwarden — both have family/team plans).

Critical

2. Add a second committee member with full access to all services

Right now, if the primary admin loses access to their email or laptop, everything stalls. Designate a backup person and give them:

  • Committee-role Netlify Identity account
  • Access to the Netlify team (app.netlify.com → Team settings)
  • Access to the Supabase project (Supabase → Project Settings → Team)
  • Access to the Cloudflare account (Cloudflare → Manage account → Members)
  • Access to the GitHub repo (GitHub → Settings → Collaborators)
Critical

3. Transfer domain registration to a shared account

The domain bhs1977reunion.com is the most fragile dependency. If the registrar account's email goes dark, domain renewal fails, and the site disappears. Actions:

  • Move the domain to an account with a committee group email (e.g., 50thbhs1977reunion@gmail.com) rather than one person's personal email.
  • Enable auto-renewal with a credit card that won't expire before October 2027.
  • Note the renewal date in the Passwords Annex.
Important

4. Move Netlify billing to a shared payment method

If the Netlify account is under one person's personal credit card, a lapsed payment could take the site offline. Transfer billing to the committee's credit card or a card held by multiple people.

Important

5. Schedule a monthly database backup

Supabase keeps automatic backups on paid plans, but you should also export a manual backup monthly and store it somewhere independent (Google Drive or Dropbox). This protects against the worst-case scenario of the Supabase project being accidentally deleted.

To export: Supabase dashboard → Database → Backups → Download, or use the SQL editor to export the classmates table as CSV.

Important

6. Use Gmail delegation instead of sharing the password

Sharing the Gmail password works but creates two risks: Google can lock the account if it detects logins from multiple locations, and there's no audit trail of who sent what. The better approach is Gmail delegation — it's free and built into Gmail.

How to set it up:

  • Sign in to 50thbhs1977reunion@gmail.com.
  • Go to Settings (gear icon) → See all settings → Accounts tab.
  • Under "Grant access to your account," click Add another account.
  • Enter each committee member's personal Gmail address. They'll get a confirmation email.
  • Once they accept, they can switch to the committee inbox from their own Gmail account (top-right account switcher) without ever knowing the password.

If a committee member leaves, just remove their delegation. No password change required. The account owner should be whoever is most permanent — ideally tied to the committee's own Gmail, not a personal address.

Nice to Have

7. Create a HANDOFF.md file in GitHub

Add a brief text file to the repository root that lists: the names and emails of people with access to each service, the domain renewal date, and a link to the password document location. A future developer can find this in 30 seconds without knowing the site's history.

Nice to Have

8. Consider migrating to a paid Supabase plan

The free Supabase plan pauses projects after 1 week of inactivity. If the site goes quiet for a week (say, right after the reunion), the database could pause and the site would appear broken. A paid plan ($25/month) removes this risk. Alternatively, set up a weekly cron job that makes a simple API call to keep the project active.

8. Passwords & Credentials Annex

Security notice: This page is protected behind committee login. However, do NOT store actual passwords in this web page — this text lives in the source code and could be exposed if the repository is ever made public. Instead, use this as a checklist of what to document, and store the actual credentials in a password manager or sealed physical document.

Print this section, fill in the blanks, and store it securely. Update whenever credentials change.

Domain Registrar

Registrar name:___________________________
Login URL:___________________________
Account email:___________________________
Password:___________________________
Domain renewal date:___________________________
Auto-renewal enabled?Yes / No
Card on file (last 4 digits):___________________________

GitHub

Repository URL:github.com/ZoomMimi/bhs77-reunion
Account email:___________________________
Password:___________________________
2FA backup codes location:___________________________

Netlify

Login URL:app.netlify.com
Account email:___________________________
Password:___________________________
Plan / billing email:___________________________
Site name:bhs1977reunion

Supabase

Login URL:supabase.com
Account email:___________________________
Password:___________________________
Project URL:ehxvwtvqgtquzavpmecx.supabase.co
Service role key (first 8 chars):___________________________
Full service role key location:Netlify env vars → SUPABASE_SERVICE_ROLE_KEY

Cloudflare (R2 Storage)

Login URL:dash.cloudflare.com
Account email:___________________________
Password:___________________________
Account ID:e947fa2737535e30b5e3f82af1075cd5
R2 API token (in Netlify):R2_ACCESS_KEY_ID + R2_SECRET_ACCESS_KEY

Resend (Email Delivery)

Login URL:resend.com
Account email:___________________________
Password:___________________________
API key (in Netlify):RESEND_API_KEY env var
Verified sending domain:bhs1977reunion.com

Committee Gmail Account

Address:50thbhs1977reunion@gmail.com
Password:___________________________
Recovery phone:___________________________
Recovery email:___________________________
Google OAuth app:console.cloud.google.com → Credentials
Client ID/Secret (in Netlify):GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET

Anthropic (Claude AI — yearbook extraction only)

Login URL:console.anthropic.com
Account email:___________________________
Password:___________________________
API key (in Netlify):ANTHROPIC_API_KEY env var
Note: Only needed if re-running yearbook photo extraction. Low priority after portraits are complete.

Who to Call for Help

Original developer:Michael Bruhn · bruhnmichaell@gmail.com
Backup committee contact:___________________________
Web developer on call:___________________________