The honest board

What a person can actually open — and everything that is written and cannot be reached.

This is the section a company selling harder would delete, and it is the reason to believe the rest of the site. A registered handler with no screen is not a feature; it is a row in a table, by name, with the measurement that established it.

The instrument is stated so a zero is provably an absence rather than a broken search.

Reachable today

What a person can actually open, and what “reachable” is allowed to mean here.

This is a two-part claim and it is worth being pedantic about, because the gap between the two parts is where technical product pages usually go wrong. A capability is on this list when a registered handler serves it and a screen a user can reach from the navigation calls it. A registered handler with no screen is not on this list; it is in the table further down, by name.

One honest bound on the whole section: this is a source-level claim about wiring. It means the code path exists from a navigation entry to a handler. It does not mean we have watched a hundred districts run a season through it.

The contract ladder

Proposal, insertion order, invoice, placement, tear sheet — and the ladder is enforced, not documented

An advertiser relationship opens, a proposal is assembled from the school’s own rate card with the total computed server-side, the proposal is sent, and it is accepted or declined. Acceptance mints an insertion order — the binding document — which is sent and signed. The signed order takes an invoice; the invoice settles; the buy fans out to its placement systems; and the placement produces a tear sheet. Every one of those moves is checked against a literal allowlist before anything is written, and an illegal move comes back as a refusal carrying the legal ladder in its own message. There is no path in this code that reaches a state by overwriting a status field.

ad-crm.ts + domain/ad-crm.ts No charge. The paid transition requires evidence — see the money section.

No creative before signature

Artwork cannot attach to an unsigned order. That is a wall, not a warning

The single most expensive mistake in school advertising sales is a page that gets designed, proofed and printed against a buy nobody ever signed. So attaching creative to an insertion order is refused outright unless that order’s status is signed — and the same wall covers attaching the inventory campaign that would fulfil it. Two endpoints, one rule, checked before any write. The refusal names the order and its actual status, so the person who hit it knows exactly which signature is missing.

ad-crm.ts:2707 None. This is a sequencing wall.

Proof of publication

A tear sheet is proof on a settled buy — and it is consent-checked before it is issued

A tear sheet is the document an advertiser keeps: this is your ad, on this page, in this publication. It is refused while the linked order’s invoice is unpaid, because proof-of-publication on an unsettled buy is a document that argues against you later. Then the part no general CRM has any reason to do: before the tear sheet is issued, the students appearing in the creatives that actually RAN are re-checked against publication consent, and a single suppressed student blocks the whole document with the count of blocked students in the refusal. The advertiser-facing artefact is the last place a consent decision should be assumed, so it is the place it is re-asked.

ad-crm.ts:3181 and :3204 None. Settlement is a precondition, not an action taken here.

Rate card and zones

The school sets its own prices, per zone, and the server is the only thing that adds them up

Sizes, publication types and sponsorship zones — back of book, love-lines pages, section dividers, per-page strip, per-spread, interleaved display, whole book — are configured per school, each zone carrying which product kinds it sells and which sizes it offers. A proposal’s total is then resolved from that configuration in integer cents, server-side. A package discount is held in basis points and applied with integer arithmetic. A client-supplied total is never trusted anywhere in this lane.

ad-crm.ts:3350 + domain/ad-crm.ts:235 Amounts are quoted in integer cents. Nothing is charged on this surface.

The activity timeline

What was said, by whom, on which day — against the school, not against a spreadsheet tab

Every touch on a prospect appends to that prospect’s own timeline, and the timeline is read and written from a page a user opens from the navigation. The stage-move endpoint also writes one for you: moving a deal logs the move and stamps the last-activity clock, so the rot detector described below has something honest to measure. A rep does not have to remember to log the thing the system already knows happened.

org-prospect-crm.ts:282 and :292 None.

The forecast

Probable revenue by owner, by stage, or by close month — derived on read, stored nowhere

The forecast groups OPEN deals into buckets and sums two different numbers for each: the raw pipeline value, and the probability-weighted expected revenue. Won and lost deals are excluded, because a forecast is about probable future revenue and a closed deal is neither. A deal with no stage is excluded too — it has no probability basis, and inventing one would be the whole problem. Owner and stage buckets come back biggest-expected-first; month buckets come back in date order with the undated ones last. It is a read-model: nothing about it is stored, so nothing about it can drift.

crm-deal-engine.ts:174 Cents in, cents out. No charge, no payout, no invoice.

The merged account read

One read that assembles the directory intel, the deal and the provisioned school

A studio’s prospecting identity is otherwise split three ways — the national-directory row, the deal, and the school workspace once it exists — with weak links between them. The account is the durable join, and the by-id read assembles all three into a single response, best-effort: an unset link comes back null rather than as an error. The list and the merged read are both driven from a page you can open from the navigation. The write side is not, and that is in the honest board below.

crm-account.ts:123 None.

Territory search

Territory search, wired — and our own comments were out of date about it

Live territory search is registered and the founder territories page calls it. Worth stating plainly because the correction runs the unflattering way for once: three comments in our own web code still describe this endpoint as dark and not live yet. Measured at tip, it is registered and it is wired. We found that by auditing our own claims rather than by trusting them, which is the same reason every other line on this page carries a file and a line number.

territory-search.ts:240 None.

Mounted, but only if you type the URL

Two screens are mounted in the router and nothing links to them. They are not broken and they are not hidden on purpose — they are simply not wired into the navigation, so in practice nobody finds them:

  • /studio/deals/:dealId — DealDetailPage.tsx, 1050 lines. The entire deal write path: read a deal, edit its amount, close date, probability, owner and win/loss reason, move it between stages, add and complete tasks, and add or remove line items. It is the only surface in the product that touches any of it.
  • /crm/deals/:prospectId/activities — DealActivityTimelinePage.tsx. The deal-scoped activity timeline. The same endpoints are also reached from the prospect detail page, which IS reachable from the navigation, so the capability is not dark — only this second door onto it is.

The first of those matters a great deal, because that one screen owns the entire deal write path. Editing an amount, a close date, a probability, an owner or a win-loss reason, moving a deal between stages, adding a task, adding a line item — all of it, on one page, with no link to it from anywhere.

And here is the part we would have preferred not to write. Two comments in our own code assert that this screen is reached from the pipeline and deal cards. It is not. We grepped for the route literal and for a navigation call across the whole web application excluding tests, and the only hits are the route declaration itself, those comments, and the page’s own header. Our own source was the thing telling us the wrong answer, which is exactly why every claim on this page names a file and a line instead of quoting a comment.

The surface, counted

100 registered handlers, across nine modules, each with its register site.

A count on a marketing page is worth exactly as much as the instrument that produced it, so here is the instrument: the number in each row is the count of handler registrations in that file, and the third column is the line in the server’s own composition root where the module is handed to the framework. “Registered” is a claim with a location attached. The total is computed from this list rather than written next to it, so it cannot drift away from the rows it describes.

Registered request handlers per route module, 100 in total across 9 modules. The largest is ad-crm.ts with 38. Full per-module figures are in the table below this figure.ROUTE MODULEREGISTERED HANDLERSad-crm.ts38org-crm-deals.ts19sponsor-crm.ts12org-territory.ts9org-prospect-crm.ts8crm-account.ts5org-prospects.ts4territory-map.ts3territory-search.ts2
Figure 1 Registered request handlers per route module, sorted largest first and drawn from the same list as the table below. The shape is the useful part: the advertiser and contract ladder is most of this surface, and the territory reads are a short tail. Nothing here is a claim about reachability. Roughly a tenth of these handlers have no screen, and those are named individually in the honest board further down.
Nine modules. The total is derived from the rows, not typed beside them.
Route moduleHandlersRegistered atWhat it owns
ad-crm.ts38app.ts:3947The advertiser relationship and the whole contract ladder: advertisers, rate card, sponsorship zones, proposals, insertion orders, invoices, package fan-out, placements and tear sheets.
org-crm-deals.ts19app.ts:5314Boards and stages, the deal itself, stage moves, tasks, line items, the lead score and the forecast read-model.
sponsor-crm.ts12app.ts:5616Sponsors, sponsor tiers, sponsorship agreements and the acknowledgment screen — the booster-and-patron side of a school’s selling, kept as its own model.
org-territory.ts9app.ts:5342Territory definition, zone and geo-zone assignment, account and district assignment, per-account territory resolution and proposal decisions.
org-prospect-crm.ts8app.ts:5313The activity timeline and the multi-contact model — several named people per school, each with their own role — plus a CSV import.
crm-account.ts5app.ts:5315The account and its merged 360: the national-directory intel, the deal and the provisioned school joined into one read, plus the district roll-up.
org-prospects.ts4app.ts:5309The original prospect board: create, list, advance a stage, and CONVERT — the verb that provisions a real school.
territory-map.ts3app.ts:5343Map-shaped reads of a territory for the platform, the studio and the founder view.
territory-search.ts2app.ts:5344Live territory search for the platform and studio surfaces.

Two things this table is deliberately not claiming. It is not claiming that all 100 handlers are reachable from a screen — roughly a tenth of them are not, and they are named in the honest board. And it is not offering the number as a measure of quality. A large surface is easy to produce. A surface where each part refuses the wrong thing is the work.

Built, not switched on

9 capabilities with no screen that reaches them. Here they are by name.

This is the section we would delete if we were selling harder, and it is the reason to believe the rest of the page. The characteristic dishonesty of a technical product page is to present a green test suite as a working feature, and this codebase has 9 places where that temptation exists. Every row below is written, registered, and called by nothing a user can open.

The instrument, so a zero is provably an absence: for each capability we searched the whole web application — excluding test files and the client module itself — for calls to its client method. The same search returns three files for the forecast read and five for the deal read, so the pattern demonstrably matches when there is something to match. Where the row says there is no client at all, we also searched the client module for the endpoint’s own path and found nothing.

Two rows deserve to be read twice. There is no way to create or edit an account from a browser — the endpoint enforces one account per studio per directory row and returns a conflict on the duplicate, a create and an edit method now exist in the browser client, and no screen calls either of them. And the district roll-up answers a question no screen asks: the route resolves a school’s district, finds its sibling schools and sums this studio’s deal value across them, and the client method that would call it has no caller. Neither of those is a bug report. They are the honest state of a product in early access, and the alternative to publishing them is letting you find them.

A correction, published rather than quietly fixed. Until 2026-07-30 this section carried an eleventh row and this paragraph named it: a 269-line drag-and-drop deal board that was fully orphaned, with no route and no navigation entry, and which was also the only caller of the deal-list endpoint. That is no longer true. The board is now mounted at a real route and has a navigation entry, so it can be opened, and the deal-list endpoint it calls is reached by a page a person can open. We removed the row because it became false, and we are telling you it was there because a claim that quietly disappears is worth less than one that is corrected out loud. Two other rows changed the same way and say so in their own third column: account writes and the district roll-up both said “no browser client at all,” and both now have a client with no caller — still unreachable, but by a different measurement than the one we first published.

Written, registered, and reachable from no screen. The third column states the measurement, not an intention.
CapabilityWhat it doesWhy nothing reaches it
Create or edit an accountOpens a CRM account for a school (one per studio and directory row, with a 409 on the duplicate) and edits its owner, notes, tags and entity links.Two browser client methods exist and ZERO pages call either one. Re-measured 2026-07-30: this row previously said there was no client at all, and that is no longer true — a create and an edit method both landed after this page first shipped. What has not changed is the conclusion: no screen calls either, so there is still no way to create or edit an account from a browser today.
The district roll-upResolves a school’s district from the national directory, finds every sibling school under it, and sums this studio’s own deal value across them.A browser client method exists and ZERO pages call it. Re-measured 2026-07-30: this row previously said searching the client for the roll-up path returned nothing, and that is no longer true. The Account-360 screen does render a district card, but it resolves from data already merged onto the account read — it does not call this endpoint. The route is written, the four phases are real, and no screen asks it the question.
The lead scoreRuns the five-band score and persists the total plus the named factor breakdown on the deal.A browser client method exists and ZERO pages call it. The scoring arithmetic is exercised by tests; nothing in the product asks for a score.
Board and stage configurationCreates a pipeline, adds stages, and edits a stage’s name, position, default probability, rot threshold and won/lost markers.Three browser client methods exist and ZERO pages call any of them. The board a deal moves across cannot currently be configured from the product; the read is wired, the writes are not.
Rename, re-default or archive a boardPatches a pipeline; removes a stage, refusing when deals still reference it.NO web client for either. Registered handlers with no client method at all.
The whole sponsor and agreement laneSponsors, sponsor tiers, sponsorship agreements, the acknowledgment screen and acknowledgment records — twelve registered handlers.NO web clients. The only sponsorship paths in the browser client belong to the public storefront in the advertising lane, not to this one. Twelve handlers, no page, no navigation entry.
Geographic zones and district assignment on a territoryReplaces a territory’s geographic zones; assigns whole districts to it.NO web client for either. The state-and-zone half of territory is wired; this half is not.
Deciding a territory proposalRecords an accept or reject decision on a disputed territory claim.NO web client. The decision record exists in the API and no screen writes one.
The territory map readsMap-shaped territory reads for the platform and studio surfaces.NO web client for the platform or studio map read. The founder map-data read IS wired and is in the reachable set above — these two are the ones that are not.

A spine, and no engine

There is a cadence table and a pure resolver. Nothing sends anything.

Sequences — multi-step automated outreach with enrolment rules, send windows and reply detection — are a standard part of what the general-CRM category advertises, and for a lot of teams they are the reason to buy one. We do not have that. What we have is the layer underneath it, and the distinction is the whole point of this section.

What exists: a cadence table in the database, scoped to a school and protected by row-level security. A pure resolver that takes a cadence definition and returns its steps in order, with a rule that refuses a definition containing duplicate step identifiers rather than resolving it into something ambiguous. A sequence step type registered in the notification rail’s own vocabulary. Isolation tests over both, proving the tenant wall at the database rather than in the application.

What does not exist: any endpoint that runs a cadence. Any screen that builds one. Any caller, anywhere in the application, of the resolver — we searched the whole application for it and found none, and the same search demonstrably reaches that package for other purposes, so the zero is an absence rather than a broken search. There is no scheduler, no enrolment, no send. Nothing sends anything.

So on the honest board earlier in this page, every row is at least a registered handler with no screen. The cadence spine sits one rung below that: it has no handler at all. Which is why it is not counted in that table, and why the comparison row for sequences is conceded outright rather than hedged into a partial win.

The correction, because it is the most useful thing on this page

The first draft of this section said something different and stronger: that the whole idea was absent from the codebase, verified two ways. That was true when it was measured. Two commits landed while this page was being written, and it stopped being true — and nothing about the sentence would have looked wrong to a reader.

That is the specific way a technical claim goes bad, and it is worth naming because it is not lying. It is a true statement with the scope quietly stripped off: at tip is a property of a measurement, not of a claim, and the tip moves. So the fix is not to write more carefully. The fix is to measure again before shipping the sentence, which is what happened here, and to publish the correction rather than silently swapping the paragraph. If you find something on this page that does not match the code, that is the response you should expect.