Technical Reference

System Architecture & Data Model

Complete technical overview of the Rising with Melodies platform โ€” two systems, two databases, and how they connect.

System Overview

The platform consists of two independent systems hosted on risingwithmelodies.org (Porkbun). Both are built with vanilla JavaScript and Vite, with no backend server โ€” all logic runs in the browser and communicates directly with Supabase.

risingwithmelodies.org
โ”œโ”€โ”€ / (index.html) โ†’ Public landing page
โ”œโ”€โ”€ /concert.html โ†’ Registration (Performer / Donor / Volunteer)
โ”œโ”€โ”€ /admin.html โ†’ Concert admin panel
โ”œโ”€โ”€ /health.html โ†’ Health check
โ”œโ”€โ”€ /school/ โ†’ Music School admin (11 modules)
โ””โ”€โ”€ /docs/ โ†’ Documentation

Databases
โ”œโ”€โ”€ Concert Supabase โ†’ ululnaedwrheymybbzue.supabase.co
โ””โ”€โ”€ School Supabase โ†’ cxsmoquefrxswkyjzzni.supabase.co

Concert System

Key Files

External Services

ServicePurposeKey
SupabaseDatabase + Authululnaedwrheymybbzue
PayPalOnline paymentsClient ID in config.js
ZelleManual paymentsrisingwithmelodies@gmail.com
EmailJSConfirmation emailsservice_toeljfq
Twilio SMSAdmin alerts via Edge FunctionSupabase Edge Function /sms
MailchimpEmail campaignsSupabase Edge Function /mailchimp
FormspreeRegistration backupmjgpbrpw

Concert Database Tables

TableDescription
eventsConcert event definitions (date, venue, capacity per slot)
registrationsAll registrations โ€” performers, donors, volunteers
performersPerformer + piece details linked to a registration
slot_capacityPer-slot capacity tracking (11 AM / 12 PM)
inventoryEquipment and supplies managed in the admin

EmailJS Templates

Music School System

Two Schools

SchoolTypeLocationCurrency
RWM-USFor-ProfitAlpharetta, GAUSD
RWM-PENon-ProfitTrujillo, PeruUSD / PEN

All school tables have a school_id foreign key. The active school is stored in localStorage as rwm_school_id and persists across pages.

School Modules & Pages

PageModuleJS File
school/index.htmlDashboard Analyticsschool-app.js
school/students.htmlStudentsschool-students.js
school/programs.htmlProgramsschool-programs.js
school/staff.htmlStaffschool-staff.js
school/enrollments.htmlEnrollmentsschool-enrollments.js
school/attendance.htmlAttendanceschool-attendance.js
school/gradebook.htmlGradebookschool-gradebook.js
school/billing.htmlBillingschool-billing.js
school/scholarships.htmlScholarships & Donorsschool-scholarships.js
school/inventory.htmlInventoryschool-inventory.js
school/communications.htmlCommunicationsschool-communications.js
school/volunteers.htmlVolunteersschool-volunteers.js
school/admin.html (ESL Classes section)ESL admin โ€” students, sessions, attendance, messagingschool-esl.js
school/esl-portal.htmlESL student portal (PIN login, no Supabase Auth)school-esl-portal.js

Note: school/students.html, staff.html, enrollments.html, etc. above are legacy standalone pages โ€” most were converted to tiny redirect stubs (location.replace('admin.html#<section>')) after their original login forms were found to be non-functional. The actual UI lives in the school/admin.html single-page app.

Cross-System Data Flow

When a Donor or Volunteer registers on the concert page and completes their submission, their data is automatically saved to the school Supabase (RWM-PE non-profit):

This uses a secondary Supabase client in concert-init.js with persistSession: false (no auth required โ€” public INSERT policies on both tables).

localStorage Keys

KeySystemPurpose
rwm_sandbox_modeConcertSandbox/Live toggle for concert site
rwm_sms_enabledConcertEnable/disable Twilio SMS notifications
rwm_school_idSchoolCurrently selected school ID
rwm_school_sandboxSchoolSandbox/Live toggle for school admin
rwm_program_orderConcertLocked concert program performer order
rwm_checklistDocsBuild checklist checkbox state
esl_portal_sessionESL Portal{login_code, pin_hash, expiry} โ€” silent re-login for 7 days
esl_portal_lockoutESL PortalFailed login attempt count + lockout timestamp
esl_portal_langESL PortalEN/ES language preference
esl_portal_last_seen_<student_id>ESL PortalTimestamp used to compute the "N new" unread message count

ESL Platform

Overview

The ESL platform lives inside the school Supabase project and adds three layers on top of the existing admin system: a teacher-facing admin section, a student-facing PIN portal, and a shared messaging thread between them. Full history is in docs/esl-platform-plan.html (status banner at the top explains the actual build order vs. the original speculative plan). User-facing guides: docs/esl-teacher-guide.html and docs/esl-student-guide.html.

Tables (school Supabase project)

TablePurposeKey columns
esl_studentsOne row per ESL studentlogin_code, pin_hash (SHA-256, client-hashed) โ€” added in Phase 2
esl_sessionsInstructor assignment, Meet link, schedule per studentinstructor_id โ†’ volunteers(id)
esl_attendancePer-class attendance logattended (bool), instructor_notes
esl_messagesTeacher โ†” student message threadsender ('teacher'/'student'), sender_name, read_by_teacher, read_by_student

All four tables use the same blanket RLS policy as the rest of the school system: FOR ALL USING (auth.uid() IS NOT NULL) โ€” no per-user scoping, consistent with every other table in this project. This means the anon key has zero direct access; the student portal never queries these tables directly (see RPCs below).

Teacher role scoping

The previously-unused users.role column (supabase-school-schema.sql) is read on login in school-admin.js. When role = 'teacher', the sidebar is filtered down to just ESL Classes and Security, and the default landing section becomes ESL Classes instead of the dashboard. Accounts with no matching users row (e.g. pre-existing admins) default to full access โ€” purely additive, nothing broke for existing users when this was added.

Student portal โ€” RPCs (SECURITY DEFINER)

The student portal (school/esl-portal.html / src/school-esl-portal.js) never authenticates with Supabase Auth โ€” there's no student login session. Instead, every action re-verifies login_code + SHA-256 pin_hash against the table directly inside a SECURITY DEFINER Postgres function, the same pattern the concert project already used for get_slot_capacity/check_duplicate_registration.

FunctionPurpose
esl_portal_login(login_code, pin_hash)Verifies credentials, returns student + session + last-10-attendance as one JSON payload. Returns NULL on any mismatch (never reveals whether the code exists).
esl_portal_get_messages(login_code, pin_hash)Verifies credentials, returns the message thread, marks teacher messages read_by_student = true as a side effect.
esl_portal_send_message(login_code, pin_hash, body)Verifies credentials, inserts a sender = 'student' message.

Session persistence is just {login_code, pin_hash, expiry} in localStorage (7 days), silently re-calling esl_portal_login on page load โ€” no server-side session table. Brute-force lockout (5 attempts โ†’ 10 min) is also tracked client-side in localStorage; a soft deterrent given static hosting has no server-side rate limiting.

Notification System

Overview

A generic, reusable email notification system built 2026-07-25 after a volunteer application was silently lost to a broken form (stale JS reference on the public volunteer page โ€” see the "Broken pages" incident notes). Any table/event in the school project can be wired up to send an email without new code โ€” just a database row and a small trigger.

DB row changes (INSERT/UPDATE) โ†’ Postgres trigger (pg_net) โ†’
notify Edge Function โ†’ looks up notification_rules โ†’ Resend API โ†’ email

Components

PieceWhat it does
notification_rules tableGeneric routing table: table_name, event, recipient_email, subject_template, body_template, active. event has no CHECK constraint โ€” can be a real Postgres event (INSERT) or a custom app-defined one (e.g. APPROVED).
supabase/functions/notify/index.tsSingle reusable Edge Function. Verifies a custom Authorization: Bearer <secret> header against the NOTIFY_WEBHOOK_SECRET env var, looks up a matching active rule, renders {{field}} placeholders (in subject, body, and recipient_email) against the triggering row's data, sends via Resend.
pg_net triggersOne per table/event wired up, e.g. notify_new_volunteer (any INSERT) and notify_volunteer_approved (only fires when agreement_status transitions into 'approved'). Uses the pg_net extension's net.http_post(...) directly โ€” not the Dashboard's "Database Webhooks" UI, which wasn't available on this project (its underlying supabase_functions schema didn't exist).
ResendTransactional email provider. Sending domain risingwithmelodies.org is verified (DKIM/SPF/DMARC records added at Porkbun, 2026-07-25) โ€” emails can go to any address, not just a single test recipient. Sender: notifications@risingwithmelodies.org.

Deploying/updating the notify function

The Supabase Dashboard's paste-based Edge Function editor proved unreliable for this file (repeated silent truncation/corruption on an ~80-line paste). The Supabase CLI is installed locally (brew install supabase/tap/supabase) and authenticated โ€” prefer it for any future changes:

supabase functions deploy notify --project-ref cxsmoquefrxswkyjzzni --no-verify-jwt

The --no-verify-jwt flag is required: Database triggers send our own custom secret, not a Supabase JWT, so the platform's default JWT gate would otherwise reject every call before the function's own code (which does its own auth check) ever runs.

Adding a notification for a new table/event

  1. Insert a row into notification_rules (table_name, event, recipient_email, subject/body templates using {{field}} placeholders matching that table's columns โ€” recipient_email also supports templating, e.g. {{email}} to notify the record's own address).
  2. Add a Postgres trigger following the pattern in supabase-notify-webhook-trigger.sql or supabase-notify-volunteer-approved.sql โ€” a small plpgsql function calling net.http_post(...) with the same URL/secret header, then CREATE TRIGGER ... AFTER <EVENT> ON public.<table> ... EXECUTE FUNCTION ....

No code changes or redeploys needed for a new table โ€” the notify function itself is fully generic.

Security Notes