thinkbridge Academy needs a bigger screen

This app is built for a laptop or desktop — please switch to a computer to continue.

thinkbridge Academy

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.

Read time: 12 minutes. Audience: the cohort. What you'll get: the architecture, the tech choices, the lessons, and an honest accounting of what is finished and what is still ahead.

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

LayerChoiceWhy this and not the obvious alternative
Static siteEleventy 3.1.5Plain HTML and templates with no client framework runtime. Pages load instantly because there is nothing to hydrate.
ServerPocketBase 0.36 with Goja JSVMTyped 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 proxyCaddy 2One block per site in plain text. Routes API and auth paths to PocketBase; serves the static build for everything else.
Public accessCloudflared tunnelNo port forwarding, no public IP, no TLS to manage. Cloudflare terminates TLS at the edge and forwards through an outbound tunnel.
Process supervisionmacOS launchdNative to the host. Restart-on-crash, log redirection, scheduled jobs. Nothing extra to install.
EmailResendAPI-key in one HTTP call; no SMTP server to run; deliverability outsourced.
Team chatTeams via Power Automate webhookThe team is already in Teams. A Power Automate flow turns a JSON POST into a channel message.
SourceForgejo, on-hostLives at localhost:3000/clients/. Self-hosted git with PRs and CI. No external SaaS in the supply chain.
End-to-end testsPlaywrightEvery 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.

  1. 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.
  2. You click the link. PocketBase consumes the token, sets a thinkschool_session cookie scoped to the host, and you land on home.
  3. 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.

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 old thinkschool.zpqv.io is decommissioned. The Mac keeps dev + Forgejo + the build pipeline; the box is the only prod.

Lessons worth carrying forward

A short list of things that cost an hour each to learn.

Named matchers in Caddy are global across imported files. A 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.
On macOS, 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.
PocketBase 0.36's Goja JSVM does not inherit file-scope 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.
A signal nobody renders is not a signal. The Teams ping was firing correctly the whole time. The admin UI just never looked at the flag. Always render the outcome of a side effect somewhere a human will see.
Don't develop in the deploy target. The working copy and the served directory were once the same folder. A cleanup command wiped it, taking the only copy of the source with it. The platform was rebuilt from session logs and a half-stale remote. Now dev lives in a separate workspace, the canonical source is the Forgejo repo, and the served tree is a disposable webhook checkout. The source of truth must never be the thing a stray 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 modecut (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.

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.