# What only a person can supply **The engineering is done. This is everything that is not engineering.** Nothing here can be written in code, and nothing here should be guessed at. Every item is either a fact about the institute that only the institute knows, an account only the owner can create, or a decision only a person can take responsibility for. > **Never tick an item without evidence.** "Probably fine" is how a website ends > up publishing a phone number nobody answers. Each row has an evidence column; > if it is empty, the item is not done. --- ## How to read this | Marker | Meaning | | --- | --- | | ๐Ÿ”ด **BLOCKS DEPLOYMENT** | The site cannot go live without it | | ๐ŸŸ  **BLOCKS LAUNCH** | Deploy without it; do not make the site public | | ๐ŸŸก **BLOCKS A FEATURE** | Everything else works; this one thing stays off | | โšช **OPTIONAL** | Genuinely optional | | **HUMAN DECISION REQUIRED** | Not a task โ€” a judgement the institute must make and own | --- ## A. Engineering โ€” complete Recorded here so the line between "done" and "waiting on people" is unambiguous. | Item | State | | --- | --- | | Public website, all pages | โœ… Complete | | Admin panel, all entities | โœ… Complete | | Consent model, enforced in the database | โœ… 21 CHECK constraints, verified by name | | Authentication, sessions, revocation, throttling | โœ… Complete | | Security headers, CSP, CSRF, rate limits | โœ… Adversarial suite, run against a production build | | SEO, canonicals, sitemap, structured data | โœ… Automated suite | | Performance budgets | โœ… Enforced against a production build | | Spreadsheet import and export | โœ… Automated suite; import can never publish | | Accessibility (automated) | โœ… Real-browser suite, Chrome and Edge. **No screen reader, Firefox or Safari** โ€” see the Phase 14 report | | Deployment pre-flight tooling | โœ… `npm run verify:preflight` | | Production smoke test | โœ… `npm run verify:production` | **Nothing on this list is waiting on the institute.** > Counts are omitted on purpose: each suite prints its own total, and a number > written down here goes stale the moment a check is added. Phase 14 found four > such numbers already wrong in the project's README. --- ## B. Accounts and infrastructure โ€” ๐Ÿ”ด BLOCKS DEPLOYMENT | # | Item | Who | Evidence needed | Done | | --- | --- | --- | --- | :-: | | B1 | PostgreSQL account (Neon or equivalent) | Owner | Project created; pooled connection string held securely | โ˜ | | B2 | Hosting account | Owner | Project created and connected to the repository | โ˜ | | B3 | **Hosting plan that permits commercial use** | Owner | Written confirmation of the plan and its terms | โ˜ | | B4 | Production domain, registered | Owner | Registrar account, domain confirmed | โ˜ | | B5 | DNS access | Owner | Able to add records | โ˜ | | B6 | `ADMIN_SESSION_SECRET` generated | Deployer | โ‰ฅ32 chars, in the provider's settings, never committed | โ˜ | | B7 | `ENQUIRY_SECRET` generated **separately** | Deployer | โ‰ฅ32 chars, different from B6 | โ˜ | | B8 | Admin account created | Deployer | Sign-in tested on the production domain | โ˜ | > **B3 is the one people miss.** A coaching institute's website is a commercial > project. Several hosts' free tiers forbid commercial use. This is a > contractual question, not a technical one โ€” **"technically free" and "allowed > for this use" are different things** โ€” and the answer must be checked against > the provider's current terms, not against what a tutorial said. See > [`COST-AND-INFRASTRUCTURE.md`](COST-AND-INFRASTRUCTURE.md). --- ## C. Institute facts โ€” ๐ŸŸ  BLOCKS LAUNCH **None of these has been invented, and none may be.** Where the code needs a value it does not have, it renders nothing rather than a plausible guess. | # | Item | Evidence needed | Done | | --- | --- | --- | :-: | | C1 | Full postal address | In writing, matching the Google Business Profile exactly | โ˜ | | C2 | Phone number(s) | In writing; **each one called and answered** | โ˜ | | C3 | Opening hours | In writing | โ˜ | | C4 | Professional email address on the domain | Mailbox exists and is monitored | โ˜ | | C5 | Institute logo, transparent background | Supplied by the institute โ€” **not** a fake transparency made by removing white | โ˜ | | C6 | Founder / faculty details, **if they want any published** | Written approval from each named person | โ˜ | | C7 | Course details | In writing | โ˜ | | C8 | Fees, **if they want them published** | Written confirmation, with a review date | โ˜ | | C9 | Batch timings | In writing | โ˜ | > **C1โ€“C3 must agree with the Google Business Profile character for character.** > A local business whose name, address and phone differ between its site and its > profile ranks worse for it. This is the single highest-value item in this file. > **C6 and C8 are HUMAN DECISIONS.** Publishing fees invites comparison and dates > the page; not publishing them costs enquiries. Publishing a faculty member's > name and photograph requires *their* consent, not the institute's. Neither is > the agency's call. --- ## D. Students, consent and real data โ€” ๐ŸŸ  BLOCKS LAUNCH **This is the section that matters most. Everything here concerns children.** | # | Item | Evidence needed | Done | | --- | --- | --- | :-: | | D1 | Signed consent form for **every** student to be published | The physical forms, filed and referenced | โ˜ | | D2 | Each form separately covers: result, name, photograph | Photograph consent is asked **separately** and is never implied | โ˜ | | D3 | A consent reference for every record | The filing reference entered in the admin | โ˜ | | D4 | Real results, from physical result documents | The documents | โ˜ | | D5 | Photographs, where photo consent exists | The image files | โ˜ | | D6 | **The reference scheme decided** | Written down: roll number, enrolment number, or another stable code | โ˜ | | D7 | Every record reviewed in *Admin โ†’ Website preview* before publishing | Teacher confirms in writing | โ˜ | | D8 | Written sign-off that every published record is correct | From the institute | โ˜ | > **D6 blocks a clean import.** The `Reference` column is what makes a second > import a *correction* rather than a duplicate student. It must be stable โ€” a > number that will still identify the same person next year. Names cannot do > this: Phase 8 found two real records colliding on name and year. > **D2 is not a formality.** Consent to publish a result is not consent to > publish a name, and neither is consent to publish a photograph. The database > refuses the combinations that violate this โ€” but a signed form that does not > actually say "photograph" means the answer is no, whatever the spreadsheet > says. ### ๐Ÿ”ด What must NOT happen before D1โ€“D8 are complete - No real student data in any database - No student names, marks or photographs published - No spreadsheet of real students uploaded "just to test" **The database currently holds 0 rows in every content table, verified.** --- ## E. Email and notifications โ€” ๐ŸŸก BLOCKS A FEATURE Everything works without this. Enquiries are written to the database *before* notification is attempted, so a missing notifier can never lose a lead โ€” but somebody must then read the admin. | # | Item | Evidence needed | Done | | --- | --- | --- | :-: | | E1 | Mailbox on the institute's domain | Can send and receive | โ˜ | | E2 | Sending domain configured at an email provider | Provider dashboard shows it verified | โ˜ | | E3 | **SPF record** published | `dig TXT` shows it | โ˜ | | E4 | **DKIM record** published | Provider reports it verified | โ˜ | | E5 | DMARC record published | `dig TXT _dmarc.` | โ˜ | | E6 | Sender and reply-to decided | Written down | โ˜ | | E7 | `RESEND_API_KEY` and `ENQUIRY_NOTIFICATION_TO` set | In the host's settings | โ˜ | | E8 | `deliver()` implemented in `src/lib/notify.ts` | Code, plus one test send | โ˜ | > **E3 and E4 are not optional extras.** Mail from an unauthenticated domain > lands in spam. A notification that silently goes to spam is *worse* than no > notification, because nobody notices the leads stopped arriving. > **Until E1โ€“E8 are done, the daily check of *Admin โ†’ Enquiries* is the entire > notification system.** Say that out loud to whoever will be doing it. --- ## F. Privacy and data protection โ€” ๐ŸŸ  BLOCKS LAUNCH **No legal text has been written, and none may be invented.** These are the institute's obligations and the institute's decisions. | # | Item | Status | | --- | --- | --- | | F1 | Privacy policy page, written or reviewed by someone qualified | **HUMAN DECISION REQUIRED** | | F2 | Consent form wording โ€” what the parent is actually agreeing to | **HUMAN DECISION REQUIRED** | | F3 | How long enquiries are kept | **HUMAN DECISION REQUIRED** โ€” a retention script exists; the number does not | | F4 | How long audit logs are kept | **HUMAN DECISION REQUIRED** | | F5 | Whether import history is pruned | **HUMAN DECISION REQUIRED** โ€” metadata only, no personal data | | F6 | Who may access student records | **HUMAN DECISION REQUIRED** โ€” currently: anyone with the admin password | | F7 | Who may export student records | **HUMAN DECISION REQUIRED** โ€” currently: the same | | F8 | Who handles a deletion or correction request | **HUMAN DECISION REQUIRED** โ€” a named person | | F9 | What happens when a student withdraws consent | **HUMAN DECISION REQUIRED** โ€” the mechanism exists; the process does not | > **F6 and F7 deserve a real answer.** There is one admin role. Anyone who can > sign in can read every student record and export the lot as a spreadsheet. > That is appropriate for a two-person institute and inappropriate for a larger > one, and the institute is the only party who can say which it is. > **F9 is the one that will actually happen.** A parent will ask for their > child's photograph to be taken down. Somebody has to be reachable, and know > that they are the one who does it. ### The takedown procedure, exactly Verified end to end by `npm run verify:consent` (19 assertions). **It is two steps, and the difference between them matters.** **Step 1 โ€” take it off the website.** Open the record (Students & results, Student stories, or Gallery), untick the photograph permission, Save. The photograph disappears from every public page immediately. For a gallery entry the whole entry comes down with it. A colleague with the same record open in another tab cannot put it back by saving their stale copy โ€” that is refused. This is the step that answers the phone call, and it takes seconds. **Step 2 โ€” delete the file itself.** Admin โ†’ Photos, find the photograph, Delete. Step 1 removes every link to the image. It does **not** delete the bytes: the file stays reachable at its own address โ€” `/media/<32-character-hash>.jpg` โ€” for anyone who already has that exact URL. The address is unguessable and nothing on the site points at it any more, but it has not stopped existing. If the parent asked for the photograph to be *removed*, rather than just taken off the page, do step 2 as well. The library refuses the deletion while any record still uses the file and names which ones, so step 2 usually means detaching it from those records first. > โš  **Do not skip step 1 and go straight to deleting the file.** The record > would keep its permission ticked while pointing at a photograph that no longer > exists, and re-attaching a photograph later would republish it. --- ## G. Content review โ€” ๐ŸŸ  BLOCKS LAUNCH | # | Item | Done | | --- | --- | :-: | | G1 | Every page read by the institute, for accuracy and tone | โ˜ | | G2 | Every claim about the institute confirmed true | โ˜ | | G3 | Empty states accepted where content was not supplied | โ˜ | | G4 | Every published result and story checked in *Admin โ†’ Website preview* | โ˜ | | G5 | Written go-live approval | โ˜ | > **G2 exists because the previous site failed it.** The audit that started this > project found fabricated toppers and testimonials on the old site. Nothing on > the new one is invented, and G2 is the check that keeps it that way. --- ## H. Launch โ€” the irreversible-ish step Only after **every** ๐Ÿ”ด and ๐ŸŸ  item above. | # | Item | Done | | --- | --- | :-: | | H0 | Every institute fact set to `verified` in `src/config/institute.ts`, after written confirmation | โ˜ | | H1 | `SITE_IS_LAUNCHED = true`, in a reviewed commit | โ˜ | | H2 | `NEXT_PUBLIC_SITE_URL` is the live `https://` domain **at build time** | โ˜ | | H3 | Rebuilt and redeployed after H1 and H2 | โ˜ | | H4 | `verify:production` passes without `--expect-prelaunch` | โ˜ | | H5 | `robots.txt` reads `Allow: /` and still disallows `/admin` | โ˜ | | H6 | Sitemap submitted to Search Console | โ˜ | | H7 | Uptime monitoring configured | โ˜ | > **Three conditions, deliberately.** Indexing requires the code flag, a real > `https://` domain, **and** every institute fact marked verified. One > environment variable is far too easy to flip by accident, and a half-finished > site entering Google's index under the institute's name takes weeks to undo. > > The third condition was added in Phase 14. `institute.ts` had always said the > address and phone numbers "must all read verified before the site goes > public" - but nothing enforced it, so the site could have been launched and > ranked on contact details carried over from the old website that nobody had > checked. `npm run verify:preflight` now names any fact still outstanding > (`P-LAUNCH-07`). --- ## Summary | Section | Items | Blocks | | --- | ---: | --- | | A ยท Engineering | 11 | **Nothing โ€” complete** | | B ยท Accounts | 8 | Deployment | | C ยท Institute facts | 9 | Launch | | D ยท Students and consent | 8 | Launch | | E ยท Email | 8 | One feature | | F ยท Privacy | 9 | Launch โ€” **all human decisions** | | G ยท Content review | 5 | Launch | | H ยท Launch | 7 | โ€” | **54 items, none of which an agency can tick on the institute's behalf.** --- ## Media storage โ€” HUMAN ACTION REQUIRED Photographs cannot be stored on the web server: its filesystem is discarded on every deployment. They need an object storage bucket, and opening the account is the one part of this nobody else can do. | # | Action | Why it matters | Done | | --- | --- | --- | --- | | M1 | Create a Cloudflare account and enable R2 | The recommended option: free egress, an S3-compatible API, and roughly thirty thousand photographs inside the free tier | โ˜ | | M2 | Add a payment card | Cloudflare requires one before R2 can be enabled, **even on the free tier**. Nothing is charged inside the free limits | โ˜ | | M3 | Create a **private** bucket | Objects are served through the site, never directly. A public bucket would hand out a directory of student photographs to anyone who guessed the hostname | โ˜ | | M4 | Create an API token scoped to that one bucket, read + write only | An account-wide key in a web application's environment is a much larger blast radius than this needs | โ˜ | | M5 | Set all four `MEDIA_S3_*` variables in the host | **All four or none.** Three of four is refused, on purpose | โ˜ | | M6 | Run `npm run verify:preflight` and confirm `P-MEDIA-01`โ€ฆ`04` pass | The configuration is checked mechanically, not by eye | โ˜ | | M7 | Upload one photograph through Admin โ†’ Photos and confirm it appears | `P-MEDIA-05` reports NOT TESTED because credentials cannot be verified without a live call. **This step is that call** | โ˜ | | M8 | Turn on bucket versioning or a retention policy | A `pg_dump` does not back up photographs. Nothing else holds them | โ˜ | > Until M1โ€“M5 are done, uploads are **refused with an explanation** rather than > accepted and lost. The site works; the photo feature does not. That is the > intended behaviour, not a bug to work around.