Project: LiturgyGen — Mass readings missalette and Prayers of the Faithful generator Public repository: https://github.com/Damudachi/LiturgyGen Completed: 27 September 2026 Repository visibility: already public. The announcement says to fill this in before flipping to public; that had already happened, so this was filled in against a live public repository rather than ahead of one. Every row below was checked against the repository as it actually stands, and the two rows that are still open — 1.6 and 2.1 — are named as open rather than claimed. Access gate: A login of my own, built on Supabase Auth — a sign-in screen inside the app, with every parish’s data scoped to it. This replaced an HTTP Basic Authentication gate; section 2 below says why.
Note on this file. The template named in the announcement was not in my copy of the finals unit when I filled this in, so the rows below follow the announcement’s own four sections. If the official template differs, this maps onto it row for row.
| # | Item | Answer | Evidence |
|---|---|---|---|
| 1.1 | .env is in .gitignore and git status does not show it |
Yes | .gitignore:4 is .env, line 5 .env.*, line 6 !.env.example. git check-ignore -v .env returns .gitignore:4:.env, and the same rule covers server/.env and client/.env. git status is clean of all three. |
| 1.2 | A .env.example ships with placeholder values only |
Yes | Three of them: /.env.example (compose only), server/.env.example, client/.env.example. Every value is a placeholder — change-me, devpassword@localhost, change-this-to-something-long-and-random. None has ever held a real value. |
| 1.3 | No connection string, key, token or password hardcoded anywhere in source, including comments and commented-out code | Yes | grep -rniE "(password\|secret\|api[_-]?key\|token\|bearer)\s*[:=]\s*['\"][^'\"]{4,}" across server/src, server/db, client/src and desktop returns nothing outside node_modules. grep -rnE "postgres(ql)?://" over the same paths returns nothing. The only place a connection string is named is db/pool.js, which reads process.env.DATABASE_URL and exits with a message if it is unset. |
| 1.4 | Nothing secret is in the git history | Yes | Ran the announcement’s command, git log -p \| grep -i -E "password\|secret\|api[_-]?key\|postgres://", over all 18 commits. Nineteen hits, every one of them either a placeholder in a committed .env.example, a ${VAR} reference in compose.yml, or a line of documentation about not committing secrets. No real value has ever been committed. |
| 1.5 | Any key that was ever committed has been rotated | Not applicable | No key, token or credential has ever been committed, so there is nothing in the repository to rotate. The credentials that exist now - the Supabase service role key and the database password - were generated straight into Render’s settings panel and have never touched a file here. One rotation is still outstanding and is not a repository problem: the database password was pasted into a chat transcript during deployment, so it should be reset in the Supabase dashboard. See the open actions at the end of this file. |
| 1.6 | Secret scanning and push protection are on | No — do this today | The repository is already public. Both features are free and available right now at Settings → Code security and analysis, and neither is on. Push protection can no longer prevent anything already pushed; it protects every commit from here. The audit at 1.3 and 1.4 is what says nothing has leaked so far, and that audit is the reason this row is a “not yet” rather than an emergency. Turning both on is the single highest-value thing left in this section. |
This project has workflows, so this section applies: .github/workflows/ci.yml and .github/workflows/deploy-pages.yml.
| # | Item | Answer | Evidence |
|---|---|---|---|
| 1.7 | Secrets are read as $ and never appear literally in the YAML |
Not applicable | Neither workflow uses a secret at all. grep -rn "secrets\." .github/workflows/ returns nothing. The two variables the Pages build reads are vars.VITE_USE_MOCK_API and vars.VITE_API_BASE_URL — repository variables, not secrets, and deliberately so: every VITE_ value is compiled into the built JavaScript and is downloadable by anyone, so a secret could never live in one. This is written into the workflow’s header comment so the next person does not put one there. |
| 1.8 | No secret is ever printed — no echo of one, no env dump, no set -x |
Yes | The only echo calls in either workflow write ready=true/false to $GITHUB_OUTPUT and emit ::notice:: / ::warning:: lines about whether a client build exists and whether the repository is private. No set -x, no printenv, no env dump anywhere. Nothing to mask, because nothing secret enters a job. |
| 1.9 | Uploaded artifacts contain no .env or generated config |
Yes | One artifact: actions/upload-pages-artifact with path: client/dist. find client/dist -name ".env*" returns nothing — Vite does not copy .env into a build, and dist/ contains only hashed JS, CSS, fonts, index.html and 404.html. |
| 1.10 | Third-party actions are pinned to a commit SHA | Not applicable, with a caveat | All six uses: entries are GitHub’s own first-party actions: actions/checkout@v4, actions/setup-node@v4, actions/upload-pages-artifact@v3, actions/deploy-pages@v4. There are no third-party actions to pin. Major-version tags on first-party actions are the documented convention. If I add any action outside the actions/ org, it gets a full SHA. |
| 1.11 | A workflow triggered by a fork’s pull request does not receive secrets | Not applicable | No workflow uses a secret, so there is nothing a fork could receive. ci.yml runs on pull_request and only installs, tests and builds. I have not looked for a way around the restriction and will not. |
| 1.12 | If a secret ever appeared in a log, it has been rotated | Not applicable | No secret has ever been available to a workflow, so none can have been logged. |
| 1.13 | A CI database, if any, is a throwaway with invented data and its URL is an Actions secret | Not applicable | CI runs no database. The 93 server tests are parser, calendar, document and middleware tests that touch no database, and the seed step CI exercises is the placeholder path a fresh clone takes. |
LiturgyGen has a login, and it writes to a database. Section 2 says that is itself the access layer, so the rows below record how it is built rather than which gate was bolted in front of it.
Which option: A login of my own, built on Supabase Auth. The announcement opens section 2 with “Some of you built a login, with Firebase or Supabase or your own sessions. If you did, that is your access layer” — this is that. It replaced an HTTP Basic Authentication gate, which was the right size of answer while LiturgyGen served one office and stopped being one the moment a second parish could sign up: a single shared password cannot say which parish is asking, and every query now has to know.
I do not have a domain in my own Cloudflare account, and the announcement is explicit that Access cannot carry a policy on a trycloudflare.com or workers.dev hostname. A login built on Supabase needs no domain of its own, works on any host, and is what is running on grading morning.
One thing the move cost, written down because it is the kind of detail that is easy to leave out: the gate guards /api and only /api. Registered above express.static it served a signed-out browser a JSON error instead of the page that draws the sign-in form, so nobody could ever sign in. The built client is therefore public — it is the same bundle for every parish and holds no data; all the scoping is behind /api.
| # | Item | Answer | Evidence |
|---|---|---|---|
| 2.1 | An access layer is in place | Yes | server/src/middleware/requireAuth.js, registered in src/app.js before every /api route. It verifies the Supabase access token and resolves the caller to a parish; a request with no token, a forged token or the old Basic header all get 401, verified by driving the middleware directly against the live project. A failure to check returns 503 and never next() — the one place the app fails closed. |
| 2.2 | The gate covers every route | Yes, with three deliberate exceptions | /healthz, /readyz and GET / are registered above the gate. GET / is a landing page: a fixed string of HTML built in src/views/pages.js, with no database call, no credential and nothing derived from the request, so there is nothing in it to leak. It exists because the browser’s sign-in prompt is browser chrome and cannot be styled or explained, and meeting it cold - then landing on raw JSON after pressing Escape - reads as a broken site rather than a private one. The app moved to /app, which is gated along with /assets/* and every /api/* route. /healthz and /readyz are also above the gate: A platform health check cannot authenticate, and a gated one gets the service marked unhealthy and killed. Neither leaks anything: one answers {ok:true}, the other {ok, db:'up'\|'down'}. Every /api/* route is behind the gate, including all nineteen write routes. |
| 2.3 | Credentials are environment variables set in the host’s settings panel, not in source and not in the repository | Yes | There is no shared password any more — each person has their own account, and the server holds only SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY, both read from process.env and set in Render’s panel. server/.env.example carries placeholders. The service role key bypasses RLS, so it is server-only and never in a VITE_ variable: everything prefixed VITE_ is compiled into a file anyone can download. |
| 2.4 | The marker can open the app | Yes | A marker account is created like any other — sign up with an email address on the live site. The address and password for one are in the private workspace project/README.md, alongside the live URL. Never in the public repository. |
| 2.5 | Supabase Row Level Security | Yes — on for every table | Verified, not assumed. All seven tables have rowsecurity = true with policies: organizations, memberships, potf_templates, readings_overrides, settings, scheduled_masses, readings_cache. Isolation was tested both directions with two parishes and two users — one parish’s user sees only its own prayers, settings and org, and an attempt to write a row into the other parish was rejected by the policy’s WITH CHECK. readings_cache is deliberately shared and read-only to clients: scripture for a date is the same fact for every parish, and only the server (service role) writes it, so a browser cannot poison it. Supabase’s own advisor was run after the migration; it flagged two SECURITY DEFINER helpers as callable over REST and both were revoked. |
| 2.6 | Every database query taking user input uses parameters, never string concatenation | Yes | Every one of the twenty query call sites binds its values with ? or @named. The one exception was routes/settings.js, which assembled its WHERE clause by joining strings — always with bound values and behind isIsoDate() validation, so never injectable, but the wrong shape. Fixed on 27 September: replaced with four fixed query constants chosen by which bounds are present, so no SQL is built at run time at all. Verified against ?from=DROP TABLE and a malformed to=. |
| 2.7 | Error responses do not print stack traces or connection details to the browser | Yes | The error handler in src/app.js returns 'Something went wrong on the server.' for any 5xx when NODE_ENV=production, and logs the detail server-side. 4xx errors return their own message, which is written for the user and names no internals. db/pool.js exits at boot with a plain message rather than surfacing a connection error into a response. |
| 2.8 | Debug, seed or reset routes added during development are deleted | Yes | grep -rniE "'/(debug\|reset\|seed\|dev\|test\|admin)" server/src/routes/ returns nothing; there were never any. Seeding is a command (npm run seed), not an endpoint. DELETE /api/readings/cache is a real user-facing feature in Settings, not a debug route, and it is behind the gate. |
| 2.9 | Rate limiting | Yes | express-rate-limit on /api in src/app.js, applied after the gate so a signed-in office is measured separately from anonymous traffic at the door: 600 requests a minute, which is generous because opening a month fires one calendar request plus one check per chosen day and a 200-day batch polls its own progress. It is a brake on abuse, not a quota. Disabled under NODE_ENV=test so the suite is not throttled. Sign-in attempts are rate-limited by Supabase, not here. |
| 2.10 | helmet |
Yes | In src/app.js, with a Content-Security-Policy written to fit rather than switched off. styleSrc allows 'unsafe-inline' because the built client needs it; scriptSrc does not, which is why the landing page’s fragment forwarder is a file at /auth-forward.js and not an inline block. connectSources() derives connect-src from SUPABASE_URL instead of hardcoding it or leaving it open, and server/test/csp.test.js exists because getting that wrong caused a real outage: the browser blocked every sign-in request before it left the page, fetch rejected with “Failed to fetch”, and nothing was logged anywhere. A policy that is too tight fails silently in the browser with no server-side trace, so it gets a test. |
| # | Item | Answer | Evidence |
|---|---|---|---|
| 3.1 | No name, student number, email or phone number in the repository | Yes | git ls-files \| xargs grep -ril for my surname and both my email addresses returns nothing. LICENSE carries the GitHub handle Damudachi, not a name. All coursework — reports, journals, planning documents, whose filenames carry my surname — lives in Docu/, which is git-ignored (.gitignore:66), and the two files from it that used to be tracked have been removed from the index. |
| 3.2 | No identity in the commits | Partly — historical exposure, live and declared | Re-counted 2026-10-11 over 58 commits: 28 are authored with a personal gmail address and 30 with Damudachi@users.noreply.github.com. This cell said “all 18 existing commits”, which was wrong on both the number and the implication that the switch fixed it going forward — ten of the 28 are dated after the switch (seven on 2026-10-04, three on 2026-10-11, the last of them committed after this row was written warning against it), because they were made through GitHub’s web interface, which commits as the account’s primary address and never reads a local git config. Commit db5e93b also has a reflection-journal PDF in its tree whose filename carries a surname, and 7e30ee4 carries a stray git(old)/ directory of reflogs. The repository is public, so both are readable now, not hypothetically. Deleting a file does not remove it from history. Fixed for local commits only: git config user.email is set to Damudachi@users.noreply.github.com for this repository, so every commit made from a checkout is clean. An edit made in the GitHub web interface is not — it commits as the account primary address and never reads a local config, which is how the 2026-10-11 commits happened. Closing that needs the primary address on the GitHub account changed to the noreply one. Not fixed retroactively: that needs git filter-repo and a force push, which rewrites all 58 commit ids and would break every commit link in AI-USAGE.md. Weighed and declined: a personal gmail address in a commit log is a low-harm exposure, and no credential or student number was ever committed. Stating it here rather than claiming a clean history. |
| 3.3 | Seed data is invented, not real people | Yes | server/db/seed.sql holds five placeholder prayers written for this tool and three invented Mass dates. client/src/api/seed.json holds a computed liturgical calendar, citations, and the same placeholder prayers. No name of any real person appears in either. |
| 3.4 | No classmates’ details anywhere | Yes | No classmate’s details anywhere, which is the row. The broader claim this row used to make — “no accounts and no personal data of any kind” — is no longer true and is corrected here rather than left standing: Supabase Auth now holds an email address and a password hash per member of staff, which is personal information under the Philippine Data Privacy Act. The scope is narrow and worth stating precisely. The application’s own tables store no email: memberships holds a user_id and a role, and the server reads the address out of the token when a screen needs to say who is signed in. No name, no phone number, no address, nothing about a parishioner, nothing about anybody who has not signed up, and deleting the Supabase user removes it. Everything else in the database — liturgical dates, scripture citations, prayer text — is not personal data. |
| 3.5 | No face in a screenshot that is not mine, stock or generated | Yes | The five screenshots in docs/assets/ show only the application. There is no longer any raster image inside the app at all: the chapel seal was the only one, and the mark is now an inline SVG of an open book. The seal was the chapel’s emblem rather than the application’s, which stopped being the right thing to show once a second parish could sign in. |
Not on the announcement’s list, but it is the thing most likely to go wrong here and it belongs in an honest checklist.
| # | Item | Answer | Evidence |
|---|---|---|---|
| 4.1 | The office’s transcribed prayers stay out of the repository | Yes | The General Intercessions volumes (ST PAULS Philippines) are published, copyrighted books. The transcriptions live in server/data/orillo/, git-ignored, and the public installer is built without them. A fresh clone runs on placeholder prayers and reports which sections it could not find. |
| 4.2 | Scripture text is not redistributed | Yes, after a near miss | The readings are the New American Bible, copyright USCCB and the Confraternity of Christian Doctrine. They are fetched at runtime and cached on the user’s own machine, never committed. The first version of server/tools/build-demo-seed.mjs serialised whatever buildDay() returned straight into the committed client/src/api/seed.json, which would have published the full reading text and, on my machine, the office’s transcriptions. It now replaces every body of text with one stand-in line and ships citations only. Checked with grep -c "not included in the demo" (24) and a grep for distinctive phrases from the readings (0). |
| 4.3 | The printed ORDO and the scans are not committed | Yes | Needed info/ is git-ignored, along with server/data/orillo/. |
| Section | Yes | No / pending | Not applicable |
|---|---|---|---|
| 1. Secrets | 6 | 1 (secret scanning — overdue, repo is already public) | 6 |
| 2. The door | 6 | 0 | 0 |
| 3. Personal data | 4 | 1 partial (history, declared) | 0 |
| 4. Copyright | 3 | 0 | 0 |
The repository is already public, so these are overdue rather than pending:
checkCredentials, get the tests green, and verify the gate against the deployed URLserver/test/basicAuth.test.js pass; the server suite is 132 (131 passing, 1 skipped without a real PostgreSQL). Verified live against https://liturgygen.onrender.com: / with no header returns 401 with WWW-Authenticate: Basic realm="LiturgyGen", the configured credential returns 200, a wrong password returns 401, and /healthz and /readyz answer unauthenticated as intended.project/README.mdREADME.md, which is git-ignored along with the rest of Docu/. Still outstanding: open the URL in a private window as the marker would, and reset the database password in the Supabase dashboard, which was pasted into a chat transcript during deployment. Also outstanding, and cheap: turn on leaked-password protection in Authentication - Policies, which Supabase’s own advisor flags as off.Worth being clear about the order of risk. The repository being public is not itself a problem: no secret has ever been committed, and the audit above is what says so. The problem would be a deployed app with nineteen open write routes on a public URL. As of 4 October 2026 the app is deployed, and the gate in front of those routes is verified working - which is what makes that acceptable rather than reckless.
The riskiest thing about this project is not injection or credentials. The secrets are a connection string and a Supabase service role key, neither of which has ever been committed and both of which live only in the host’s settings panel. It is that the app’s entire value comes from text somebody else owns — two published books of intercessions and the New American Bible — and that a public repository redistributing either would be a real problem with my name permanently attached. The first draft of the demo seed generator would have done exactly that. What I did about it: the books never enter version control, the scripture is fetched at runtime and stripped out of anything committed, and the public installer ships without the office’s data. What I knowingly accepted: a gmail address left in eighteen historical commits, and one Supabase advisory left standing — public.user_org_ids() is a SECURITY DEFINER function the authenticated role can call over RPC. Its whole body is select org_id from memberships where user_id = auth.uid(), so a caller can only ever read back their own parish ids, which they already know; it is SECURITY DEFINER precisely so the memberships policy does not recurse into itself. Moving it to a schema PostgREST does not expose is the clean fix and rewrites every policy that calls it, so it is named here rather than done the week of the recording.