This repository is used for the following apps:
- clone this repo
npm install(JaMmusic build should fail)- Request a copy of the .env file, which includes credentials to dev & test mongodbs and to connect to the Google auth service. You will need to put a copy of the .env file into the root of the backend folder and also inside of backendroot/frontend so that you can test the production build from the local backend.
After placing the new .env file into the web-jam-back/frontend folder, you need to rebuild so that these environment variables are used in the output to dist, so just run npm install again.
npm start starts the express server at localhost:7000.
npm run start:debug also starts the node debugger, which allows you to use Chrome browser to debug. You should also install the NIM add-on to Chrome and set it to automatic mode.
The .env contains a variable that points to the localhost of the front end and other required credentials.
The /inquiry route sends a booking-inquiry email via Gmail SMTP using nodemailer. As of 2026-05-17 this replaces the previous @sendgrid/mail integration (see docs/inquiry-sendgrid-crash.md for the post-mortem on the SendGrid failure mode that motivated the swap).
Two env vars must be set on the deployed environment (and in your local .env for end-to-end testing):
GMAIL_USER— the Gmail address used to authenticate and as theFromaddress. Production value:joshua.v.sherman@gmail.com.GMAIL_APP_PASSWORD— a Gmail App Password (NOT the regular account password). To generate one:- Sign in to the Google account at https://myaccount.google.com.
- Security → 2-Step Verification (must be ON; App Passwords are unavailable without it).
- App passwords → choose "Mail" and a device label like "web-jam-back".
- Copy the 16-character password Google shows. Store it as
GMAIL_APP_PASSWORDin.envlocally and as a Heroku config var in production.
The destination address (To) is hardcoded to joshua.v.sherman@gmail.com in src/model/inquiry/InquiryController.ts. Change there if you need a different recipient.
In NODE_ENV=test the route returns 200 without actually sending email, so unit tests don't require live credentials.
Replace <your-heroku-app-name> with the actual app name (find it in the Heroku dashboard or via heroku apps).
Option A — Heroku CLI (recommended):
# Set the new values. The 16-character App Password from Google has no spaces; quote it to be safe.
heroku config:set GMAIL_USER=joshua.v.sherman@gmail.com -a <your-heroku-app-name>
heroku config:set GMAIL_APP_PASSWORD='xxxx xxxx xxxx xxxx' -a <your-heroku-app-name>
# Verify the values are set (do NOT paste the password output into a public channel):
heroku config:get GMAIL_USER -a <your-heroku-app-name>
heroku config:get GMAIL_APP_PASSWORD -a <your-heroku-app-name>
# Tail logs while sending a test inquiry from the live site:
heroku logs --tail -a <your-heroku-app-name>
# AFTER verifying the test inquiry arrived in the inbox, remove the obsolete SendGrid vars:
heroku config:unset SENDGRID_API_KEY -a <your-heroku-app-name>
heroku config:unset SENDGRID_USERNAME -a <your-heroku-app-name>
heroku config:unset SENDGRID_PASSWORD -a <your-heroku-app-name>Setting a config var triggers a Heroku dyno restart automatically. The change is live as soon as the new dyno is up (usually within ~30 seconds).
Option B — Heroku dashboard:
- Log in to https://dashboard.heroku.com.
- Select the web-jam-back app.
- Settings → Reveal Config Vars.
- Add
GMAIL_USER=joshua.v.sherman@gmail.com. - Add
GMAIL_APP_PASSWORD= the 16-character password from Google. - Send a test inquiry from the live site, confirm it arrives at
joshua.v.sherman@gmail.com. - ONLY after delivery is confirmed, remove
SENDGRID_API_KEY,SENDGRID_USERNAME,SENDGRID_PASSWORDfrom the same Config Vars page (click the X next to each).
GET /livestream/current powers the CollegeLutheran livestream page's "always show a real video" behavior (CollegeLutheran#706). It queries the YouTube Data API v3 and returns:
{ "videoId": "…", "status": "live" }— the channel is live right now{ "videoId": "…", "status": "completed" }— otherwise, the most recent finished stream{ "videoId": null, "status": "none" }— on error, nothing found, or when the API key/channel are not configured (the UI then falls back to plain links)
The result is cached in-memory for 15 minutes, because each YouTube search.list call costs 100 of the free 10,000-units/day quota.
Two env vars must be set on the deployed environment (and in your local .env for end-to-end testing):
YOUTUBE_API_KEY— a YouTube Data API v3 key from the Google Cloud Console (Web Jam LLC project), restricted to the YouTube Data API v3. Secret — server-side only, never exposed to the browser (that's the whole reason this lives in the backend).YOUTUBE_CHANNEL_ID— the channel to watch. College Lutheran:UCOra1rXiO-BHzMDNlLd9hFQ(public).
Without these, the endpoint returns none and the page shows its fallback links, so it is safe to deploy before they are set. In NODE_ENV=test no real API calls are made (tests stub fetch).
Option A — Heroku dashboard (recommended):
- Log in to https://dashboard.heroku.com and select the
webjamsalemapp. - Settings → Reveal Config Vars.
- Add
YOUTUBE_API_KEY= the key from Google Cloud Console. - Add
YOUTUBE_CHANNEL_ID=UCOra1rXiO-BHzMDNlLd9hFQ.
Option B — Heroku CLI:
heroku config:set YOUTUBE_API_KEY='<your-key>' -a webjamsalem
heroku config:set YOUTUBE_CHANNEL_ID=UCOra1rXiO-BHzMDNlLd9hFQ -a webjamsalem
heroku config:get YOUTUBE_CHANNEL_ID -a webjamsalem # verify (do NOT echo the key)Setting a config var triggers a dyno restart automatically; the change is live within ~30 seconds.
Powers the CollegeLutheran and WebJamLLC homepage Facebook feeds (CollegeLutheran#740 / web-jam-back#797, multi-page web-jam-back#799), replacing the unreliable Page Plugin iframe. Multiple pages share one Meta app; each page has its own stored token and its own in-memory cache, keyed by pageId.
GET /facebook/feed?pageId=<id>— public, no auth. Returns{ posts, lastUpdated }from that page's in-memory cache, refreshed on startup and then hourly. With nopageIdit defaults to the CollegeLutheran page (FB_PAGE_ID) so the already-deployed CLC frontend keeps working until it passes the param. Empty ({ "posts": [], "lastUpdated": null }) until that page's token has been set, so it is safe to deploy before configuring anything; the UI falls back to a plain Facebook link.PUT /facebook/token— admin only (guarded byAUTH_ROLES.facebook). Body{ "userToken": "<short-lived FB user token>", "pageId": "<id>" }from the admin page's "Reconnect Facebook" button.pageIdselects which page is being reconnected (defaults to the CLC page for back-compat). The server exchanges the user token for a long-lived one, reads that page's token from/me/accounts, stores it in MongoDB (oneFacebookTokendoc perpageId), and refreshes that page's cache. The app secret never leaves the server, which is why the exchange can't happen in the browser.
When a page token dies (Graph OAuth code 190 — e.g. Josh changed his Facebook password, logged out of all sessions, or hit a security checkpoint), the server emails GMAIL_USER once per page per outage, naming the page that died, telling him to click Reconnect Facebook. The flag re-arms on that page's next healthy refresh; Heroku's ~daily dyno restart also resets it, so a dead token re-nags about once a day until fixed (intentional). The last good cache keeps serving throughout, so the feed just stops updating rather than breaking.
The single-page era stored one token doc keyed key: 'pageToken'. On startup the service migrates that doc to the CLC pageId (and drops the stale key_1 unique index) so CollegeLutheran survives the multi-page deploy without a manual reconnect.
Graph API version: pinned in one constant (FB_GRAPH_VERSION in FacebookController.ts). Meta supports each version for at least 2 years; expired versions don't hard-fail (calls auto-forward to the oldest still-supported version), and the four fields used (message, full_picture, permalink_url, created_time) are stable core fields. Bump the constant when convenient — no scheduled maintenance needed.
Env vars (set on the deployed environment and in your local .env for end-to-end testing):
FB_APP_ID/FB_APP_SECRET— the "Web Jam LLC" Meta app (Josh is app admin; the app stays in development mode, so no Meta app review is needed).FB_APP_SECRETis secret — server-side only.FB_PAGE_ID— the back-compat default page id served when?pageIdis omitted: the CollegeLutheran page202368653220334.FB_PAGES— a JSONpageId→ display-name map of every page served, e.g.{"202368653220334":"CollegeLutheran","365007513885497":"WebJamLLC"}. Drives the hourly refresh loop and the page name used in the dead-token alert email. If unset, the service falls back to the singleFB_PAGE_ID(CollegeLutheran only).AUTH_ROLES— add a"facebook": ["Developer", "clc-admin", "JaM-admin"]entry (the CLC and JaM admins, plus Developer). Any of these can reconnect any page. Without the entry, any authenticated user could update a token.GMAIL_USER/GMAIL_APP_PASSWORD— already used by the/inquiryroute; reused for the token-death alert. InNODE_ENV=testno email is sent and no Graph calls are made.
Set these the same way as the Livestream vars above (Heroku dashboard Config Vars or heroku config:set ... -a webjamsalem).
There are really only four things; most "vars" just point at them. Page access tokens are never env vars — the backend derives them on reconnect and stores them in MongoDB, one per pageId.
| Var | Repo(s) | Public / secret | Purpose |
|---|---|---|---|
FB_APP_ID (2207148322688942, "Web Jam LLC" app) |
web-jam-back and each frontend (JaMmusic, CollegeLutheran) at build time | Public — safe in the browser bundle | Identifies the Meta app; opens the FB login popup (frontends) and authorizes the token exchange (backend) |
FB_APP_SECRET |
web-jam-back only | Secret | Server-side token exchange; must never reach a frontend |
FB_PAGE_ID |
web-jam-back only | Public id | Default page when GET /facebook/feed omits ?pageId (CollegeLutheran) |
FB_PAGES |
web-jam-back only | Public ids | pageId→name map of every page served; drives the refresh loop + alert email name |
AUTH_ROLES.facebook |
web-jam-back only | — | Roles allowed to PUT /facebook/token |
Frontends need only two things: FB_APP_ID (build-injected) and the page id they show (JaMmusic hardcodes WebJamLLC's 365007513885497; CollegeLutheran uses the backend default). Locally each repo sets its own .env; in production the backend vars live on the web-jam-back Heroku app(s), and FB_APP_ID must also be present at the frontend's build step (the web-jam-back app that compiles the frontend injects it).
The Reconnect flow logs the page admin into Facebook, where the consent dialog lists the pages you manage. That selection is a replace, not an add: if you uncheck a page you previously granted, Facebook revokes the app's access to it and its stored token dies. So whenever you log in to reconnect either feed, leave both the CollegeLutheran and WebJamLLC pages checked. (Forgetting to check the page you're actually reconnecting just fails harmlessly with "page not found".)
The id stored in FB_PAGES must be the one Facebook returns from /me/accounts (that's what the token exchange matches against). To find or confirm it: in the Graph API Explorer generate a user token (scope pages_show_list, the page checked), then GET /v20.0/me/accounts and read the id next to the page name. A quick sanity check: https://www.facebook.com/<page-id> should land on that page. The page's HTML delegatePageID is not reliable — it can differ from the Graph id under the New Pages Experience.
These live in the Web Jam LLC Meta app (developers.facebook.com), not in code or env. The app stays in Development mode (no app review needed), which has consequences below.
-
Allowed Domains for the JavaScript SDK (Facebook Login → Settings) — every host that runs the SDK's
FB.loginmust be listed, or the browser throws "JSSDK Unknown Host domain". Add the same list under Settings → Basic → App Domains, and set "Login with the JavaScript SDK" = Yes. The full list across both apps and all environments:localhost # local dev for both apps (host only — ports ignored) web-jam.com # production host both apps actually serve under (APP_NAME) www.web-jam.com joshandmariamusic.com # JaMmusic vanity domain www.joshandmariamusic.com collegelutheran.org # CollegeLutheran vanity domain www.collegelutheran.org -
Who can use "Reconnect Facebook": because the app is in Development mode, only users with a role on the app (Admin / Developer / Tester) can complete
FB.login. To let someone reconnect their own page (e.g. the CLC admin), add them under App Roles → Roles → Testers; they must also be an admin of that Facebook page. -
Consent dialog reuse:
FB.loginis called withauth_type: 'rerequest'so the page picker shows every time. Without it Facebook offers "continue with previous settings" and silently reuses the last grant — which is how reconnecting one page can leave the other ungranted and 400 the next exchange (page not found in /me/accounts). Still keep both pages checked each time (see "Reconnecting a feed" above).
npm test runs the tests and generates a coverage report.
Coverage is gated: the run fails (and so does CI) if total statements or lines drop below 90%, or branches/functions below 80% (see coverage.thresholds in vitest.config.ts). The coverage badge above is a static value — bump the percentage in the README when it moves meaningfully.
if some tests fail it is probably due to the TEST database instance of MongoDb Atlas needs to be resumed.
To get the latest version of code, git pull origin dev, create your own branch, then switch to your own branch. Push code changes to your own branch and then submit a pull request to the dev branch on GitHub.