How thinkbridge Academy was built
This is the platform you are using right now. The whole thing was designed and shipped over a single weekend, in the open, by one engineer working with an AI assistant. The point of this page is for the cohort to see the full shape of a real product, not a toy.
What thinkbridge Academy is
A six-week, mentor-graded curriculum that takes a CS-degreed engineer with three years of internship experience and gets them shipping production-grade code with AI in their IDE. Each piece of work is read, attempted, submitted, reviewed by a human mentor, and either passed, failed with a note, or sent back for more information. Behaviors anchored to the five thinkbridge values accrete as a separate stream of evidence.
The platform is the substrate. The curriculum is the content. The cohort is the proof.
The stack, in one table
| Layer | Choice | Why this and not the obvious alternative |
|---|---|---|
| Static site | Eleventy 3.1.5 | Plain HTML and templates with no client framework runtime. Pages load instantly because there is nothing to hydrate. |
| Server | PocketBase 0.36 with Goja JSVM | Typed collections, magic-link sessions, file storage, hooks in JavaScript with hot reload. One binary, one SQLite file. No separate web server, no separate auth service. |
| Reverse proxy | Caddy 2 | One block per site in plain text. Routes API and auth paths to PocketBase; serves the static build for everything else. |
| Public access | Cloudflared tunnel | No port forwarding, no public IP, no TLS to manage. Cloudflare terminates TLS at the edge and forwards through an outbound tunnel. |
| Process supervision | macOS launchd | Native to the host. Restart-on-crash, log redirection, scheduled jobs. Nothing extra to install. |
| Resend | API-key in one HTTP call; no SMTP server to run; deliverability outsourced. | |
| Team chat | Teams via Power Automate webhook | The team is already in Teams. A Power Automate flow turns a JSON POST into a channel message. |
| Source | Forgejo, on-host | Lives at localhost:3000/clients/. Self-hosted git with PRs and CI. No external SaaS in the supply chain. |
| End-to-end tests | Playwright | Every screen and every API path is covered. Runs against the live host with a hard TS_TEST_ALLOW_PROD=yes gate so it can't wipe data by accident. |
How a piece of work moves through the system
This is the loop everything else exists to serve.
- Admin adds you as a thinkschooler in
/admin/. The platform mints a single-use magic-link token, sends a Resend email, and posts to Teams. The admin sees a green badge if both pings landed, a red one with the reason if either didn't. - You click the link. PocketBase consumes the token, sets a
thinkschool_sessioncookie scoped to the host, and you land on home. - Home calls
/api/today, finds your current piece, and renders the card.
Admin "Open as" (impersonation), without losing your own seat. Cookies are per-origin, not per-tab, so the obvious implementation — redeem a link that overwrites thinkschool_session — logs the admin out of their own account in every tab (a real incident: an admin who'd just set a password still needed a fresh invite to get back in). The fix is an overlay: impersonation lives in a separate thinkschool_impersonate cookie, and a single global routerUse middleware — on each request where that cookie is present and the real thinkschool_session belongs to an active admin — rewrites the in-flight Cookie header so every downstream handler transparently resolves the impersonated user, while the browser keeps the admin's real cookie untouched. Exiting just clears the impersonate cookie; the admin is back instantly, no re-sign-in. The authz gate runs on every request (a stray impersonate cookie can't elevate a non-admin), and the rewrite means zero per-handler changes — the dozens of inlined session resolvers are none the wiser. Honest caveat: while active the overlay is browser-wide (all the admin's tabs show the impersonated user until Exit); true per-tab dual identity is impossible with cookie auth. One wrinkle: a PB superuser admin has no thinkschool_session cookie (they sign in with a sessionStorage bearer), so the overlay can't apply to them — for that path "Open as" sets the session cookie straight to the target (the new tab is theirs to close), which is safe precisely because their own auth isn't the cookie.
4. You open the piece, read the body, fill the exercise area, hit submit. For a code piece you paste a GitHub link (the repo, a /tree/ folder, a PR, or a CI run) which the server parses into a structured reference. The submission is written, an advisory AI review is attached for the mentor, your progress row goes to pending_mentor, and the mentor's queue ticks up by one.
5. Your assigned mentor opens /mentor/, picks your card, reads the piece, reads your work (a code submission shows a clean, clickable repo/folder/PR/CI link instead of a URL buried in prose), writes a note, picks pass or fail or needs-more-info. They can also tag a behavior (one of 25 values-anchored behaviors) with a polarity and a one-line factual note.
6. The verdict writes back. Your map updates. If passed, the next piece unlocks. If failed with a note, you retry. If sent back, you see what they need.
Every step writes an audit_log row. Every delivery attempt writes a notify_outcome row. Nothing important happens silently.
How an applicant enters the funnel
That loop is for thinkschoolers, who an admin invites. Applicants come in the other end, self-serve.
A candidate self-applies at the public /apply page (name + email). The platform mints the same magic-link it uses for staff and creates a role-applicant account. That endpoint is the only unauthenticated write that creates users, so it is deliberately conservative: a honeypot field, neutral responses (it never reveals whether an email already exists, and a staff email is a silent no-op), input length caps, an outstanding-link cap per email, and a two-tier rate limit recorded in an apply_events ledger — a per-IP window (keyed on Cloudflare's authoritative CF-Connecting-IP) plus a spoof-proof global daily send cap, so the endpoint cannot be turned into an open email relay regardless of how an attacker rotates IPs. A Cloudflare edge rule is the defense-in-depth layer on top. They land on /learn/, the always-open Build With AI prep course (open to any applicant, no cohort needed).
The Engineering Aptitude Screen is gated by a screening window. An admin opens a window on a planned aptitude-screen cohort and sets a target number of qualifiers, fluid and set at open time. Opening sweeps every current applicant into the cohort and auto-enrolls anyone who applies while it's open; /api/exam/begin refuses a new attempt unless the window is open (a resume of an in-progress exam is always allowed). The window auto-closes the moment the target number have cleared the bar — a passing submit, or a late auto-graded expiry, trips it — and the admin can reopen, raise the target, or close early. Closing stops new applicants and new starts but never drops anyone. At most one window is open at a time, so auto-enroll is unambiguous. A passed applicant is then interviewed through the same assignment and scoring machinery as a thinkschooler.
The screen is track-agnostic: it measures judgment (directing and verifying AI), not a stack, so a screened-in candidate is placed into whichever track fits rather than picking one up front. The public landing reads the placement tracks from a small read-only endpoint (/api/public/tracks), which returns only tracks an admin has marked Show on landing — tracks are private by default, and the funnel tracks are hard-excluded as a guard. So the homepage list is admin-controlled (the internal certifications stay off), grows as tracks are added, and is never hardcoded.
Two internal pages, /whats-new/ and /about-the-build/, are staff-only: hidden from the nav for logged-out visitors and applicants, and they redirect home if reached by URL.
What the data model looks like
Twenty collections do all the work.
- users — name, email, role (thinkschooler, mentor, admin, interviewer, applicant), status (invited, active, suspended, graduated, withdrawn, dropped), github username
- tracks — the curricula on offer (slug, name, summary, an editable
plan_html, order, status, ashow_on_landingflag — private by default — that gates whether the track appears on the public homepage, and acertify_onlyflag for internal certifications that are never subject to the cut), managed in the admin Tracks tab. A cohort'strackslug points here, and/plan/renders the active track's plan. Eight are live: .NET Backend, CS Lead Certification, Delivery Principal Certification, Flutter + Firebase, Web + Firebase, AI Opportunity Consulting (a cut/hiring track — assess a workflow, sequence opportunities crawl-walk-run, build a pilot, win buy-in), the Engineering Aptitude Screen (the entrance track — a 90-minute, closed-book, machine-graded MCQ on theaptitude-screenslug), and Build With AI (build-with-ai— the OPTIONAL, self-taught prep on-ramp for that screen: no mentors, no interviewers, machine-graded mastery checkpoints; completion buys readiness, not hiring credit). A track is also a timed course:duration_minutes(minutes through weeks; 0 = untimed), agrading_mode(humanmentor review, orautowhere the AI verdict decides at submit), and a per-trackreviewer_personathat swaps the AI reviewer's opening sentence (e.g. .NET's persona judges groundedness first — a textbook-correct answer that doesn't cite the learner's actual code is a fail; empty = the default senior-engineer voice). - cohorts — name, start, end, status, a
track(which curriculum it runs), and amode(cut= hiring drop,certify= no drop / clear-the-bar; acertify_onlytrack forcescertifyand is refused by the cut ranking + confirm-cut regardless of stored mode). For a timed course the clock is per-learner oncohort_members(started_at/deadline, set on Begin;expired= didn't finish in time). For anaptitude-screencohort the screening window lives here too:target_qualified(the fluid N of qualifiers that trips the auto-close),screen_open(accepting auto-enroll + new exam starts), andscreen_opened_at/screen_closed_at - cohort_members — joins users to cohorts; carries the
assigned_mentorfor thinkschoolers, plus per-member cut state (statusactive/dropped,dropped_at,drop_reason,cycle) - interviews — one row per (cohort, cycle, thinkschooler, interviewer): the senior-engineer score (with an explicit
scoredflag so a feedback-only row isn't mistaken for a real 0) and feedback, feeding the biweekly cut. The "candidate" here is a thinkschooler or an applicant — assignment, the interviewer roster, and scoring all accept both, so passed screen applicants are interviewed through the exact same machinery - exam_items — the Engineering Aptitude Screen item bank: 252 adversarially-verified MCQs across six dimensions (verification, spec decomposition, definition-of-done, calibrated trust, scope/outcome, a small fundamentals floor), each with
options, ananswer_index, a rationale, difficulty, and anactiveflag. Static; the per-candidate randomization happens at delivery - exam_sessions — one per (applicant, screen cohort): the selected balanced random 42 plus each item's option-shuffle permutation (
item_order), the candidate'sanswers, timing (started_at/deadline), the machinescore+passed, and the Tier-2integritysignals. Graded by one shared machine-only path (no LLM, no code execution) so it scales to thousands; a 5-minute cron auto-grades any session past its deadline - course_items — the practice pool for the OPTIONAL Build-With-AI prep track, disjoint from exam_items (the course teaches the construct, the screen measures it — they must never share items or the course becomes the screen's answer key). 112 authored+verified items across 7 modules; the gradable items live here while the module teaching prose is served from
lib/course_bank.js - course_progress — one row per (applicant,
build-with-ai): which module checkpoints have been cleared to mastery, the mastery trajectory, and whether the course is completed. Not cohort-bound (the course is optional self-study, open to any applicant); completion is recorded and shown as positive-only context, never written into the cut composite - graduations — the credential record: one per (cohort, thinkschooler). A mentor recommends → an admin finalizes, freezing a
summarysnapshot (composite, pieces passed by category, competencies) and minting acertificate_codethat backs the public /cert view - categories and pieces — the curriculum; pieces carry a body, an exercise type, an exercise definition, prerequisites, live status, and a
track. The platform runs multiple tracks on one instance — a cohort sees only its track's pieces, and a learner enrolled in more than one track gets a top-bar switcher that re-points Today, the Map, and the plan — so an engineering track and a Customer Success certification run side by side without leaking into each other. - progress — one row per thinkschooler per piece; the state machine
- submissions — every attempt; the mentor decision and note live here too. A code submission's GitHub link is parsed into a structured
content.repo(owner/repo, branch, subpath, kind: repo/folder/PR/commit/CI-run) - notes — free-form mentor-to-thinkschooler notes outside the submission flow
- competencies and piece_competencies — 116 competencies (64 technical, 30 values-anchored, 14 AI-consulting, 8 client-delivery), joined to pieces — and now wired for the cs-lead, delivery-principal, and ai-consulting tracks too, not just .NET
- observations — the mentor evidence ledger for the values-anchored behaviors
- magic_tokens and sessions — auth
- apply_events — the throttle ledger for the public
/api/applyendpoint (one row per accepted attempt: IP + timestamp); the per-IP window + global daily cap are counted off it, and an hourly cron prunes it - audit_log — who did what, when, with which detail blob
- secrets — API keys and webhook URLs, managed from the admin Settings tab; value lengths surfaced to admins, values never
Notifications, the discipline
Two outbound channels: email through Resend, chat through a Teams Power Automate flow. Both are best-effort. Neither blocks the underlying action. The handler still creates the user even if Resend is down; the audit captures the outcome either way.
The admin sees, inline with the magic link, a green pill if email landed and a green pill if Teams posted. Red pills if either failed, with the failure reason. A separate notify_outcome row goes into the audit log so the team can post-mortem deliveries even after the admin closed the page.
This pattern took two passes to land. The first version returned the flags from the API but the UI ignored them, so the first real add (Anushka, in the live channel) went without a Teams ping and no one noticed. The fix was adding the badges and the audit row at the same time. Lesson: a signal that nobody renders is not a signal.
How it gets deployed
There is no build server and no remote CI service — but a pre-push git hook is the gate: before any push it runs ESLint, a strict type-check of the cut math, a secret scan, npm audit, and the fast unit/property tests. Beyond that, there is dev on the Mac, prod on a Hetzner box, and a few rules.
Migration note (2026-06-27). Prod moved off the Mac onto a Hetzner box and the live domain is now
thinkbridge.academy. The oldthinkschool.zpqv.iois decommissioned. The Mac keeps dev + Forgejo + the build pipeline; the box is the only prod.
- Dev (Mac) and prod (Hetzner) are separate. Development happens in a workspace clone at
/Users/saiganesh/thinkschool.zpqv.io/, backed by its own PocketBase instance (thinkschool-dev.test, port 8128, and reachable over the tailnet athttps://sais-mac-mini.tail503402.ts.net:8444with a real Let's Encrypt cert). The dev PB's hooks symlink into that workspace, so saving a file hot-reloads dev in about a second. The tree at/Users/Shared/repos/clients/thinkschool.zpqv.io/is a Forgejo checkout (source-of-record), no longer a live deploy target. - Prod is a single-origin box.
thinkbridge.academyresolves through a cloudflared tunnel to Caddy on:80, which serves the static build and routes/api,/auth, and the/piece/<slug>/rewrite to PocketBase on:8120— one origin, so the host-scoped session cookie holds. An auditproxy on:8107fronts Anthropic for submit-time AI review (keyless; the key rides in from PBsecrets). No Cloudflare Pages. - Deploy is explicit, not on push.
git pushto Forgejo is source-of-record only (there is no thinkschool case indeploy-prod.sh). To ship: build Eleventy,rsyncdist/+pb_hooks/+pb_migrations/to the box,chown, andsystemctl restart pocketbase-thinkbridge. We don't trust JSVM hot-reload on deploy: the 2026-05-19 recovery showed rsync-over-inode doesn't reliably re-trigger the watcher, and migrations only run on startup — so a schema change must land before the hooks that expect it. Runbook:deploy/HETZNER-MOVE-NOW.md. - Content seeds run on demand, not on deploy. Curricula and track plans are seeded by explicit endpoints/scripts (
/api/admin/seed-cs-lead,seed-delivery-principal,seed-flutter-firebase,seed-web-firebase,seed-ai-consulting, anddeploy/seed-track-plans.mjs), so launching a new track on prod is a deliberate step, not a side effect of a push. - On the Mac, Caddy still serves dev from
/etc/caddy/clients/thinkschool-dev.caddy; theio.thinkpliance.caddy-thinkschool-guardlaunchd job restores it from the repo if a sibling repo'srsync --deletewipes it. On the box, prod's Caddy block isdeploy/hetzner/thinkbridge.caddy. - The Mac's cloudflared maps
*.zpqv.iotohttp://localhost:80; the dev site is deliberately NOT on that wildcard — it stays tailnet-only. Prod is on a separate tunnel on the box. - If the platform is ever down,
deploy/export-day-pdf.sh <day>renders that day's pieces to an offline reading-copy PDF straight from the live curriculum, so the cohort can keep moving.
Lessons worth carrying forward
A short list of things that cost an hour each to learn.
path_regexp apiprefix ^/api/ in one site's block will silently collide with the same apiprefix name in another site's block, and traffic ends up routed by the wrong regex. The fix is to prefix every matcher name with the site short name. We lost 45 minutes chasing a 404 on /api/ that adapted-to-JSON looked correct.
localhost can resolve to ::1 where auditd squats ports and silently 404s.
Always use 127.0.0.1:PORT in reverse_proxy directives. The IPv6 path looks healthy until you POST something.
const or function declarations into routerAdd handler closures.
Helpers must be inlined per handler. Cross-file top-level names also collide silently. We name everything with a per-file prefix.
rm can reach.
Honest status on the morning of launch
Per role, what works tomorrow and where the experience trails off.
Thinkschooler Magic-link sign-in for first arrival, plus an email-and-password sign-in at /sign-in/ for returning visits (so a browser restart no longer needs an admin to mint a fresh link), and an /account/ page in the topnav to set or change that password anytime. Today's card with an "up next" hint, the map with status pills, the piece view, submit with inline validation, see the verdict (also delivered as an email when the mentor decides), retry with a clear "send follow-up" framing. Everything is scoped to your cohort's track, and the per-track plan renders at /plan/; if you're in more than one track, the top-bar switcher re-points Today, Map, and the plan. A "What your mentor has noticed about you" panel on home shows behavior observations as they accrete. Two optional reflection prompts under each exercise ("what did you learn", "what would break this") ride along on submit and earn mentor extra credit. When a cohort finalizes your outcome, it shows at /graduation/ with a shareable, link-verifiable certificate. If your course is timed, your home page shows the duration and a one-way Begin button; once you begin, a live countdown runs and you must finish every piece before it reaches zero or the course is marked not complete. Missing: a per-piece view of which competencies the piece covers.
Mentor
Queue, review modal with piece body and your work side by side, three-way decision with required note for fail and needs-more-info, default-visible behavior observation picker, the student's optional reflections shown inline as extra credit, queue refresh on decide. Each submission carries an AI-assisted recommendation — at submit time a model (via the audit proxy) judges whether the work meets the ask and stores an advisory PASS/FAIL + concise reasoning + a draft comment (the model is told to write straightforward and to the point — no preamble or filler); the modal surfaces it with a one-click "use this comment", and the mentor still decides (the verdict→decision mapping records whether they accepted or overrode the AI). That same call also suggests one values-anchored behavior observation — a behavior picked from the values_behavior menu, a polarity, and a one-line note grounded in the work and the thinkschooler's reflections — which pre-fills the behavior picker for the mentor to confirm, adjust, or clear; the slug is validated against the menu so a hallucinated one is dropped, and the model abstains when nothing's clearly evidenced. Best-effort: if the model is unavailable the submission simply routes to a human as before. The prompt and parser live in a shared module, so an offline eval (deploy/eval-review.mjs) grades this exact prompt against real mentor decisions — replaying logged verdicts at zero token cost, or re-running the current prompt after a change. A mentor can be assigned across cohorts of different tracks, so every queue row and the review modal are labelled with the submission's track + cohort — a CS-Lead written response never reads like a .NET code submission. A code submission renders its GitHub link as a clean, clickable repo/folder/PR/CI link rather than a URL buried in prose. A per-track counts strip sits above the queue (e.g. "All 6 · .NET Backend 2 · CS Lead 1 · Delivery Principal 3"); the chips double as a local filter, but the other tracks' counts stay on screen while you focus, so it's impossible to forget a cohort has reviews waiting. The global track switcher deliberately does not drive the queue. Keyboard accessibility: escape closes, tab is trapped inside the modal, focus returns to the trigger. Missing: automatically fetching and grading the linked repo's actual code (the AI review reads the submission text + link, not yet the code behind it), a standalone "log an observation" surface for things you noticed outside a review, a ledger view of your own recorded observations, a roster of who you are responsible for, and a per-thinkschooler dashboard.
Interviewer Senior engineers sign in to /interview/, pick a cohort and the current two-week cycle, and score each candidate 0–100 with written feedback (saved per interviewer, editable on a return visit). Those scores are an equal-weighted input to the cut. The admin pairs interviewers to candidates (panel-capable — many interviewers per candidate) from the Cut tab's Interview coverage panel; an interviewer then sees only their assignees — their roster, and the score endpoint itself, are gated to the candidates they're paired with (no open "all candidates" view, no scoring someone they can't see). The coverage view shows, per candidate, who's assigned, who's actually scored this cycle — each interviewer's score and their written feedback — the average, and who's still un-interviewed, so nobody slips through when several interviewers split a cohort. The same roster + scoring works for screen applicants: a passed entrance-screen candidate assigned by an admin shows up in the interviewer's roster and is scored exactly like a thinkschooler.
Applicant
A fresh-graduate candidate created with the applicant role and added to an aptitude-screen cohort lands on /exam/. They read an honest monitoring notice, accept a one-line honor code, and begin the 90-minute screen: 42 multiple-choice questions (a balanced random draw, options and order shuffled per candidate), one at a time with a question palette, a live countdown, autosave on every answer (a refresh resumes), and a confirm-before-submit. The screen runs fullscreen and records tab-switches, pastes, and answer pace; the candidate's email is watermarked across the page. It does not use the camera, mic, or screen recording, and reads nothing outside the page. Grading is machine-only and the candidate never sees a score — submitting just thanks them. Passing leads to the same async live interview every other candidate gets. Missing: bulk applicant import (today they're added one at a time), and recruiter-facing scheduling (interviewers invite their own assignees for now).
Applicants also get an optional self-taught on-ramp at /learn/ — the Build With AI prep track. No mentors, no interviewers: seven modules (mapped 1:1 to the screen's constructs) each teach how to direct and verify AI-built software, then gate on a machine-graded mastery checkpoint that pulls fresh questions from a pool disjoint from the live screen bank, demands a high bar, and gives unlimited retries with instant per-question explanations. Modules unlock in sequence; a final interleaved checkpoint completes the course. It is deliberately optional and uncredited — the screen is still the mandatory gate, and completion buys readiness, surfaced to admins only as a positive-only “prep ✓” beside the screen score (never scored into any hiring decision, so there is no incentive to fake it). Honest scope of v1: the checkpoints are knowledge/judgment MCQs that ready a learner for the screen; the hands-on, agent-driven build capstone the panel recommended is deferred until repo-fetch-and-execute ships (until the grader can clone and run a candidate's code, a build task would be fully AI-relay-able). Today's option order is fixed per item (delivery doesn't reshuffle), so the stored answer positions are balanced 28/28/28/28 to remove any positional tell.
Admin
The console lands on a Home / Today dashboard — stat tiles (active users, pending reviews, applicants screened, active cohorts) plus a cohorts-needing-attention worklist (built from one server-side /api/admin/overview rollup, not 20 client fan-outs) that deep-links straight into a cohort's cut or screen. The nine tabs are grouped Operate · Pipeline · Configure so the applicant lifecycle reads left-to-right. It runs on a design-token system with AA-contrast text, focus-trapped dialogs and arrow-key-navigable tabs, and a laptop-only notice below 860px. Destructive actions are guarded: confirming a cut names everyone who will lose access and requires typing the cohort name, suspend/remove name the person too, and each leaves a few-second Undo. Users — searchable, role/status-filterable, paginated, now a dense table with a "⋯" overflow (so the destructive Suspend isn't one of four look-alike buttons), so a 1,000+ roster loads without the old 500-row ceiling (adding a member is a search, not a giant dropdown). Tracks — manage the curricula on offer and edit each track's plan, plus its duration (timed courses run minutes → weeks; 0 = untimed), grading mode (human mentor review, or auto where the AI verdict decides at submit), and an optional AI-review persona that overrides the reviewer's opening sentence per track (e.g. .NET's grounded-grading stance that fails ungrounded AI-relay answers), and a Show on landing toggle (private by default) that controls whether the track appears on the public homepage; the cohort form reads its list from here. Each track also has a Manage curriculum drill-down — the categories-in-order map of that track's pieces (new categories/pieces inherit the track) — so a curriculum lives under the track it belongs to rather than in its own top-level tab. Cohorts — a cohort-first compact list with each cohort's track and member count, rosters loaded per cohort on open, filterable by track and status (built for 20+ parallel cohorts of ~60), with a "View curriculum" jump to that cohort's track map. (The student /map/ stays thinkschooler-only; admins reach a track's curriculum via Tracks → Manage curriculum, or a cohort's View curriculum jump.) Competencies are their own tab — a global, bucket-organized catalog shared across tracks (each piece tags which it teaches), so unlike curriculum they aren't owned by a single track. Plus a Settings tab to maintain integration keys & tokens (Anthropic, GitHub, Teams, Resend) from the UI — write-only values, audit-logged, no CLI, with a per-key Test button that probes each one server-side (auth/connectivity) and reports pass/fail without ever exposing the value — delivery badges (green/red on every invite + resend), audit log, preview-as-student, preview-as-mentor, and IST-friendly cohort times with an inline UTC preview. A Cut tab runs the biweekly assessment review: an absolute 0–100 composite (throughput, interviewer score, pass quality, pace, mentor signal — a component with no signal yet is excluded and the weights renormalized) ranked best-first, with everyone below an admin-set threshold flagged as a recommendation. Nothing is auto-selected — the admin manually picks who to drop; confirming removes access immediately (sessions revoked, blocked everywhere) and audit-logs each drop. A compact health line shows active / idle≥2d / pending reviews. Cohorts carry a mode — cut (the hiring drop above) or certify (employee certification, never dropped). A Graduation tab produces the credential: each member's evidence summary (composite, pieces passed by category, competencies), a mentor recommends an outcome, and the admin finalizes it — freezing the snapshot and minting a certificate the learner sees at /graduation/ and anyone can verify (credential-only) at /cert/. An Aptitude Screen tab runs the entrance funnel: seed the MCQ item bank (idempotent), then per screen cohort see who started / completed / cleared the bar / was integrity-flagged, each candidate's machine score, and the advisory Tier-2 flags (tab-switches, pastes, fast answers — surfaced for a human to weigh against the live interview, never an auto-fail). The results are server-paginated and filterable (all / cleared / unassigned passers / flagged / in-progress, with live counts) so a 3,000-applicant cohort doesn't dump every row, and the admin assigns passers — including flagged-but-passing — to interviewers one at a time or in bulk (select a batch → assign to one interviewer), reusing the interview-assignment the interviewer view already reads. Missing: deeper Home-dashboard signals (per-mentor load, queue depth over time), a per-student observation rollup, and a daily delivery digest in Teams.
Everything in the missing column is real, not vapor — and tracked as priorities in the team's backlog. The shape of the platform is now set; the next pass adds the views that make six weeks of accumulated work legible.
A note on building this with an AI assistant
The whole platform was built by one engineer pair-programming with an AI assistant. The assistant wrote most of the code; the engineer made every architectural call, every product call, every "this is good enough to ship" call. Two specific patterns worked.
First, the assistant was always told the why before being given the what. "Add Teams posting because the team needs to see new users in a channel they already watch" produced different and better code than "add Teams posting to add-user." The why constrains the design.
Second, every change ended with a Playwright test. The discipline was non-negotiable. Every screen and every API path is now covered. Refactors are safe because the tests catch regressions in seconds. When a test would have been hard to write, that was a signal the change was too coupled and needed splitting.
You are about to do six weeks of work that will look a lot like this. Pay attention to how AI helps you go faster and where it makes you go slower. The platform you are using is one data point.
Working with the AI assistants
The cohort uses two AI tools every day. They are not the same; their failure modes are different. Two short guides spell out the top five things to know for this week. We will add more as the cohort matures.
- Working with GitHub Copilot the autocomplete in your IDE that finishes your sentence. Read this before you press Tab on the next completion.
- Working with Claude Code the CLI colleague you brief with full sentences. Read this before you type your next
claudecommand.
Where the source lives
The repository is hosted on the team's internal Forgejo instance at localhost:3000/clients/thinkschool.zpqv.io. It is not exposed publicly. Ask your mentor if you want a read-only walkthrough of any specific module during a 1:1.