Helpme.software · Support and ticketing for K-12 and the studios that serve it

A help desk that cannot hold a student record, and tells you what it is not measuring.

Every help desk can open a ticket. The interesting question in a school is what the ticket is ALLOWED to contain, who can count the queue, and whether the dashboard is honest when its inputs are missing. Those three answers are usually a configuration exercise, a training slide and a chart that draws a zero.

Here they are structural. The ticket table was written so student-shaped columns are not representable on it. Queue counts below five render as “<5”, because in a small school a count is a name. And the SLA endpoint returns a flag reading provisioned:false rather than a compliance percentage it cannot compute yet — which is a strange thing to put on a sales page, and the reason to trust the rest of it.

0student-identifying columns representable on the support-ticket table — first name, last name, student id, subject id and date of birth are unrepresentable by construction, and a database check forbids a parent or student surface from coupling a subject
<5the smallest number the queue will show you. Any aggregate cell below five renders as that literal string; the raw count is never sent to the client, because in a narrow scope a small count is an identifier
11registered help-desk API routes running today — five for the ticket lifecycle, six for the operator surface (queue, SLA, assign, escalate, analytics, satisfaction)
12SLA policy cells built in, three escalation tiers by four priorities, in business hours against an injected calendar. An unrecognized tier falls back to the strictest cell, never the most forgiving

Why a school buys a help desk twice

The general-purpose desk is excellent software pointed at the wrong risk.

The category leaders are genuinely good products. Zendesk and Freshdesk have spent fifteen years on the shape of a ticket, and nobody sensible starts a help desk by claiming otherwise. The problem is narrower than “they are not built for education”, which is what every vertical vendor says and almost none can substantiate.

It is this. A general-purpose desk models a CUSTOMER. Its privacy posture is a set of custom fields, retention rules and access roles that an administrator configures correctly on Tuesday and that a new integration quietly widens in March. Nothing in the schema objects, because the schema was designed to hold whatever a business needs to hold about the person who wrote in.

A school does not have customers. It has minors, their guardians, and staff acting on their behalf, under a review that asks what your system CAN do rather than what it is set to do today. “We turned that field off” is a weaker answer than “that column does not exist”, and a district counsel can tell the difference immediately.

So we started at the migration rather than the settings screen, and then held ourselves to the same standard everywhere else: the queue suppresses a count that could identify someone, the denial for another person’s ticket is indistinguishable from a nonexistent one, and the report tells you when it is not measuring. The rest of this page is the honest inventory of what that has produced so far, including the parts that are not on.

The privacy wall — a schema, not a setting

Five walls, each one at the layer where a configuration mistake cannot reach it.

The test for any of these is the same: after the wall is in place, is the wrong thing still POSSIBLE? A setting fails that test. A column that does not exist passes it.

The ticket table has no student-shaped column

The migration that creates it was written so first name, last name, student id, subject id and date of birth are unrepresentable, and a database CHECK forbids a parent or student surface from coupling a subject — there is no roster join to make. A ticket holds an adult contact and opaque correlation handles: a request id, a screen label, a role, and tenant identifiers. The two later columns added to it are an adult owner handle and two opaque references to a page or an order, both uuids with no foreign key.

Tenancy is row-level security with a command split

A tenant reads its own school’s tickets under a select policy. Only a system lane may write, raise or close a ticket that belongs to no tenant, and a tenant may only write a ticket pinned to its own school — so a tenant cannot privatize a platform ticket and then read it. The event log inherits the same wall, so the audit stream never crosses a tenant boundary either.

The operator queue is fail-closed on scope

A viewer’s scope claim is derived once, on the server, from their session — never from a parameter. A ticket is admitted only if one of its own scope keys is in the allowed set. An unknown or ambiguous claim resolves to NO keys, which means an empty queue rather than everything. The studio and school scopes are separated at the same chokepoint, so a studio ticket cannot surface in a school reader’s queue.

Small counts are suppressed, not just hidden

Every aggregate count in the queue runs through small-cell suppression before it leaves the server: a value below five is replaced by the literal string “<5” and the raw number is not in the payload. This is in the shared engine the route calls, not a formatting rule in one interface that a second client could skip.

A denial looks exactly like a nonexistent thing

A requester reading someone else’s ticket in the same tenant gets a plain 404 — the same response a made-up identifier gets. Not a 403, which would confirm the row exists and turn the endpoint into an enumeration oracle. The requester’s own list is resolved from an owner-scoped read and then filtered a second time by the engine, belt and braces.

The family portal generates nothing

Deflection suggestions are RETRIEVAL over published, audience-filtered knowledge-base articles. No text generation runs on the family-facing surface at all, and an unprovisioned or empty knowledge base produces an honest “no suggestions” rather than an invented article. That is a rule about who the surface faces, not a temporary limitation.

What is built — each card names the file it was read out of

Ten live capabilities, four of which carry a named honest-off seam inside them.

Every card below has a registered route or a mounted screen and a real caller. The label Live means it works end to end. The label Partial means the surface is live and a seam inside it is honest-off — the body says which seam and why, because that is the part a demo would hide. The file path under each card is there so you can hold the claim to something.

Build state of 16 described surfaces: 5 live, 5 live with a named honest-off seam inside, 6 built and not switched on.FILLED = LIVE · HALF = LIVE WITH AN HONEST-OFF SEAM · DASHED OUTLINE = BUILT, NOT SWITCHED ONTicket lifecycleLIVEOwner isolationPARTIALOperator triage queueLIVESLA policy gridPARTIALAssignmentPARTIALSupport analyticsPARTIALAgent workspaceLIVEFamily request portalLIVEStatus probeLIVEEscalation & CSATPARTIALCorridor up-routingHELDEmail intake & omnich…HELDAutomation rulesHELDAI triageHELDDeflection analyticsHELDPresence, audit repla…HELD
Figure 1 Every surface this page describes, by build state. Filled tiles are live with a real caller; half-filled tiles are live with a named honest-off seam inside them; dashed outlines are built, tested and not switched on. Drawn from the same lists as the cards below and in the next section, so there is no separate summary here to fall out of date.

Ticket lifecycle

File, read, list, transition, assign — on durable rows, with the legal-move table at the server

Five routes carry a ticket from filed to closed. A requester files one; the OWNER is read from the authenticated session server-side, never from the request body, so a forged payload can never own somebody else’s ticket. A lifecycle move is checked against a single legal-transition table before anything is written: an illegal move (closing something twice, reopening by the wrong verb) returns a 409 with the from-state and the attempted action, and the row is not touched. Rows are durable in the support-ticket table, not a queue in memory that a restart forgets. This is the foundation everything else on this page reads.

Live · 5 registered routes

apps/api/src/routes/help-desk-tickets.ts

Owner isolation

A requester sees their own tickets and nothing else — and the denial is a uniform 404

A requester’s list is resolved from their own owner-scoped read and then passed through the engine’s owner filter a second time, belt and braces. Staff hold the queue scope; a requester never does. The detail worth the paragraph is the SHAPE of the denial: a same-tenant ticket owned by another requester returns exactly what a nonexistent ticket returns — a plain 404, no “forbidden”, no different latency, no hint. A 403 tells you the row exists. That is an enumeration oracle, and we do not ship one. For a REQUESTER that wall goes all the way down: their list and their detail read both run under the tenant select policy at the database, narrowed again to their own owner column. The STAFF path is where we have to be precise, because the honest answer is not the flattering one. The staff QUEUE read runs under the same tenant policy and is scope-walled above it. But the staff PER-TICKET read — fetching one ticket by id, and the transition and assignment that reuse it — goes through a repository helper that opens a SYSTEM-lane transaction, and the select policy admits a system lane unconditionally. So on that one path a cross-tenant row is NOT invisible at the database, and no scope claim is re-checked above it either. For the platform support role that is the intended reach. For the narrower account-manager role, which the queue does wall, it is wider than the queue implies, and we are not going to describe it as a wall while it is a hole. It is logged as a defect, not a feature.

Live · requester fail-closed; staff per-ticket read is system-scoped

apps/api/src/routes/help-desk-tickets.ts

Operator triage queue

The scoped queue, with a count that refuses to identify anyone

The operator queue derives the viewer’s scope from the session — district, school, studio, rep, or platform — and admits a ticket only if one of its own scope keys is in that allowed set. An unknown or empty claim sees an EMPTY queue rather than everything. Then the part no general help desk does: every aggregate count runs through small-cell suppression. A cell holding one, two, three or four tickets renders as the literal string “<5”, not as its number. In a district where one small school has two open tickets and everybody knows that school has one nurse, the count IS the identifier. Suppressing it is not decoration.

Live · scope-walled + small-cell suppressed

apps/api/src/routes/help-desk-ops.ts

SLA policy grid

Twelve business-hour targets, a clock that pauses, and a fail-closed fallback — with the stamps written but not yet read

The policy is code, not a config table you have to populate before the product works: three escalation tiers by four priorities, twelve cells, each a pair of BUSINESS-hour budgets for first response and resolution. A ticket whose tier or priority does not parse falls back to the STRICTEST cell, so a mistyped ticket is never quietly handed a lax target. The clock runs on an injected business calendar, so a weekend does not breach anything, and it PAUSES while the ticket is waiting on the reporter — the ball is in their court and the budget should not burn. A live clock flips to at-risk at 80 percent consumed. HONEST-OFF, and this is load-bearing — but the precise reason changed and the page has to change with it. The first-response, resolved and closed stamps ARE persisted now: the lifecycle write records them on the ticket row, keyed off the landing status, and the ticket read returns all three. What is still missing is the READER. The SLA projection that feeds the roll-up hands the engine a hard-coded null for first-response and resolution, so the clocks still run on the opened anchor alone and the endpoint still returns provisioned:false. So this is not an absent column any more; it is a written column with zero production readers. That is a smaller, duller job than a migration, and stating it as “not persisted” would send somebody to build a schema change that already shipped.

Live · honest-off: stamps persisted, SLA projection still reads null

packages/shared/src/help-desk-sla.ts

Assignment

Manual assignment is wired. The least-loaded auto-pick is not, and says so.

A staff member assigns a ticket to an owner handle; the write is the same helper the lifecycle route uses, so there is one assignment path and not two that can disagree. The response also reports the tier the routing rule would target. What is NOT wired: the least-loaded automatic pick, because it needs an agent roster and no roster source is provisioned. The endpoint returns autoPick with provisioned:false and the reason. We could have shipped a round-robin over whoever last touched a ticket and called it auto-assignment. It would have looked identical in a demo and been wrong in week two.

Live · manual only; auto-pick unprovisioned

apps/api/src/routes/help-desk-ops.ts

Support analytics

Volume and backlog age are real numbers. Latency, SLA compliance and CSAT trend report unavailable.

Ticket volume bucketed by day, week or month, and backlog age in bands, both computed from the persisted rows and both scope-filtered and small-cell safe. Those are real. Response latency, SLA compliance percentage and CSAT trend each return a flag reading available:false. For CSAT the input genuinely does not exist — there is no store. For latency and compliance the input DOES exist now: the response and resolution stamps are written on the ticket row. The analytics projection simply does not read them — it omits both fields on the way to the engine. No injected function is passed either, so the engines cannot fabricate a number even by accident. Every help desk we compared draws these charts unconditionally. A median-first-response chart computed from stamps nothing reads is not a chart; it is a picture of a zero.

Live · 2 of 5 metrics provisioned

apps/api/src/routes/help-desk-ops.ts

Agent workspace

The staff cockpit — queue, detail, assign, move, and a canned reply you fill yourself

A mounted staff screen: the scoped queue, ticket detail, owner reassignment, a legal lifecycle move, and a canned-reply composer over four human-authored templates. The composer’s honesty rule is small and worth stating: a placeholder you did not fill stays on screen as a literal {{key}}, and the render is flagged incomplete. It is never blanked out, because a half-filled reply that LOOKS finished is the one that gets sent. The API re-checks the staff role on every request; the screen mirrors that gate client-side only to choose what to mount. A restricted or unreachable read degrades to an honest empty state, never a fabricated queue.

Live · mounted staff route

apps/web/src/routes/HelpDeskAgentWorkspacePage.tsx

Family request portal

Deflect first, then file — with real published articles or an honest nothing

A parent, guardian or student opens the portal, starts typing a subject, and sees suggestions drawn from the PUBLISHED knowledge base — audience-filtered on the server, retrieval only. No generated answer: this surface faces families, so text generation is held. If the knowledge base is unprovisioned or matches nothing, the portal says it has no suggestions. It does not invent an article to fill the space. Filing goes through the same owner-isolated route as everything else, so the requester sees their own request and its status afterwards and no one else’s.

Live · mounted family route

apps/web/src/supportportal/SupportRequestPortalPage.tsx

Status probe

A status pill that signs a token, reads through the store, and writes-then-reads a key

The /status endpoint is a synthetic probe along the real user path, explicitly not a health check that returns 200 because the process is up. Three steps: mint and verify an access token, read a fixture through the repository layer, and write then read back a throwaway key-value marker. A green pill therefore MEANS you can sign in, load your work, and have an edit save. A sick dependency reports as a failed step and a degraded verdict; the route itself does not 500 and hide the fault. No tenant data is touched at any step.

Live · synthetic, not a health check

apps/api/src/routes/support.ts

Escalation & CSAT

Both compute correctly and neither writes. That is the honest state, printed in the response.

Manual escalation walks the priority and tier ladders one notch, computes the supervisor-notify intent, and fails closed on a cross-tenant actor — and then returns persisted:false, because there is no tier-persistence helper yet. The operator sees exactly what WOULD change. A satisfaction response is validated for eligibility, tenancy and rating range, then returned with persisted:false, because there is no CSAT store. Neither of these is a stub that pretends. Both are complete engines with the last mile deliberately unbuilt, and the response body says which mile.

Live surface · persisted:false on both

apps/api/src/routes/help-desk-ops.ts

Built, and not switched on — the list a sales page usually omits

Six areas of real, tested engine work with zero production callers.

We checked this the boring way: for each module, grep the API and web application for its export names and see whether anything calls them. Not a status document, not a roadmap column — the actual callers. Everything below came back empty.

Publishing it costs us something on a comparison spreadsheet and buys the only thing worth more, which is that you can believe the previous section. If a capability here is the one your purchase turns on, weigh it as a commitment with the hard part already done, and ask us for a date.

Corridor up-routing

Registered, and 503 on every write until a founder flips the flag

When a request exceeds the level of whoever received it, the corridor opens an escalation, resolves the next authority from an editable routing graph, minimizes the payload, and hands the notification chokepoint an emailed round trip. The store and the routing-rule table both exist as migrations. The entire write surface is gated behind an environment flag that DEFAULTS OFF, so every write returns 503 today. This is the standing posture: hold the ENABLEMENT, not the build. Separately, with no mail key configured the email is reported as queued-not-sent rather than faked as sent.

Built · not switched on

apps/api/src/routes/assist-escalation.ts

Email intake & omnichannel

Inbound parsing, auto-reply detection and cross-channel dedupe — no caller

Inbound email parsing with body and subject caps, auto-reply and bulk-mail detection so an out-of-office does not open a ticket, and a channel-normalizing layer that folds several intake channels into one ticket envelope with cross-channel deduplication. Both modules are written and tested. Neither has a production caller. Email-to-ticket is a headline feature of every incumbent and we are not going to imply we have it running because the parser exists.

Built · not switched on

packages/shared/src/help-desk-email-intake.ts

Automation rules

Condition matching, firing rules and a depth-capped pass — no caller

A rules engine with scope matching, condition evaluation, an action applier and a bounded multi-pass runner with a maximum recursion depth so a rule cannot trigger itself forever. Zero production callers. If a demand for “when a t3 ticket sits four hours, escalate and notify” is what decides your purchase, that rule engine is built and it is not running, and you should weigh it as a commitment rather than a capability.

Built · not switched on

packages/shared/src/help-desk-automation.ts

AI triage

Built with a grounding gate, deliberately not wired

A triage suggester with an explicit grounding predicate and a default gate. It is not wired, and every ticket the live route creates reports AI triage as disabled with the machine reason that the in-VPC model is not in canonical. This is the one held module we are in no hurry about. A support queue in a school is not where you want a model quietly re-classifying somebody’s safeguarding report as a password reset.

Built · not switched on

packages/shared/src/help-desk-ai-triage.ts

Deflection analytics

Two full deflection engines, neither called

Article suggestion, deflection-candidate scoring, outcome recording, per-tier deflection rates and top-article roll-ups — across two separate modules. The family portal’s live deflection does not use either of them: it queries the published article index directly. So “we deflect tickets before they are filed” is true, and “we measure how well deflection works” is not, and those are different sentences.

Built · not switched on

packages/shared/src/help-desk-kb-deflection.ts

Presence, audit replay, roster context

Collision warnings, an append-only trail replay and a redacted roster view — no callers

Agent presence with an is-someone-else-in-this-ticket check and a version guard on save. An append-only audit vocabulary with a trail replay. A roster-context view with a display redactor for the narrow case where a ticket legitimately concerns a class rather than a person. All three are built, tested, and uncalled. The audit VOCABULARY is used elsewhere in the platform; the help-desk replay is not.

Built · not switched on

packages/shared/src/help-desk-agent-presence.ts

An unflattering detail, published on purpose

Some of that held work is held by a bug in our own package, not by a decision.

While auditing this page we found a whole family of ticket modules — a lifecycle core, a second SLA engine, an automation engine — with no callers, and the reason turned out to be mundane. Our shared package re-exports its modules by star. Several names collide: a terminal-status check exists in three different modules, a macro renderer in two, a support-tier list in two. An ambiguous star-exported name is silently dropped, so those functions are not importable by name from the applications at all.

The proof is neat enough to be worth stating. In the macro module, exactly two exported names are unique — the catalogue and the placeholder extractor — and those are exactly the two that reach the agent workspace. The colliding ones do not, and the workspace screen carries a local re-implementation of the macro renderer and the terminal-status check with a comment explaining that it is routing around the collision.

That is a barrel problem with a known fix, not a missing feature, and it does not change what runs today. We are printing it because a page that lists what is off should also say WHY, and “we shipped a re-export that silently drops ambiguous names” is a more useful sentence to you than a roadmap quarter.

Against the incumbents — named, because refusing to say the words reads as evasion

Zendesk and Freshdesk are better at being a business help desk. We are trying to be a better one for a school.

Parity is the floor and we are not there on every axis: email-to-ticket intake, automation rules and satisfaction persistence are built and not switched on here, and those are ordinary expectations of the category. We named them one section above rather than leave them out of the table.

What the table compares is SHAPE. Not which product has more features — the incumbents do — but which risks each product’s architecture makes impossible rather than configurable, and what its billing meter rewards. No figure is quoted here, because a price we quote today is a price that moves next quarter and we are not going to build an argument on a number we cannot keep accurate.

How each product is SHAPED and metered -- published billing model and documented architecture, not internals. Read down a column.
ZendeskFreshdeskJira Service ManagementIntercomHelpme.software
Billing meterPer agent seatPer agent seatPer agent seatPer seat plus resolutionPer school / per district (planned)
Student columns unrepresentable on the ticket tableNo — custom fields hold what you put in themNo — custom fields hold what you put in themNo — a field schema you configureNoYes — at the migration
Small-cell suppression on queue countsNoNoNoNoYes — below five renders “<5”
Reports when a metric is unmeasuredCharts render regardlessCharts render regardlessCharts render regardlessCharts render regardlessYes — provisioned:false / available:false
AI triage defaultForegroundedForegroundedAdd-onThe productOff, and reported off on every ticket

Rows describe each product’s published billing model and its documented architecture, not its internals. Where we do not know, the cell says so. If a row is wrong we would rather correct it than win with it: [email protected].

Five differences that are structural, not featural

Each of these survives an administrator having a bad Tuesday.

  • 01
    The wall is a migration, so it cannot be configured off A general-purpose desk secures student data with field policies, retention rules and roles — every one of which is a setting somebody can widen, and a setting a new integration can route around. Our ticket table cannot hold a student-shaped column because those column names are not representable on it and a database check forbids the coupling. There is no screen to get this wrong on. That is the whole argument, and the rest of the page is us applying the same test everywhere else.
  • 02
    The meter is the organization, not the number of people who help Zendesk, Freshdesk and Jira Service Management all bill per agent seat. In a school that is a tax on the actual workflow: a front office is a dozen people who each answer a handful of tickets a week, so half of them get read-only access to save money and then answer by email anyway, outside the system, which is precisely where the audit trail dies. Our planned tiers are per school and per district. Everyone who helps gets an account, because we do not get paid more when they do.
  • 03
    The queue refuses to identify anyone by arithmetic Small-cell suppression is standard in education reporting and absent from every general-purpose help desk we compared. It matters exactly where a district roll-up meets a small school: two open tickets in a sensitive tier at a site with one counsellor is not an anonymous statistic. Any cell below five renders as “<5” and the raw number never leaves the server. It costs a little dashboard resolution and it closes a re-identification route nobody was watching.
  • 04
    The instrumentation admits its own gaps in the payload Our SLA roll-up returns provisioned:false with a machine reason: the first-response and resolution stamps are written on the ticket row, but the projection feeding the SLA engine still passes null for both, so the clocks run from the open anchor. Our analytics return available:false on latency, compliance and satisfaction trend — satisfaction because there is no store at all, latency and compliance because the projection does not read the stamps that now exist. Assignment reports that its auto-pick is unprovisioned. Escalation reports that its delta was not written. A dashboard that draws a chart from stamps nothing reads is not neutral — it is a confident picture of nothing, and somebody makes a staffing decision on it.
  • 05
    The stack is ours, so there is no seat being resold to you The ticket engines, the SLA policy grid, the queue projection, the suppression layer and the presenters are code we own, not a wrapper around another vendor’s desk with our colours on it. That is what makes a licensed or self-hosted deployment a real conversation rather than a reseller agreement, and it is why the file paths on this page are ours to publish. Self-serve licensing is planned, not live; we will not describe it in the present tense until it is.

Who it serves — three operators, one desk

Built for the school office, the district support lead, and the studio that serves fifty schools.

The school front office

The realistic picture is a handful of people who answer support as one duty among six. They need a queue they can see at a glance, a canned reply they can fill in without composing from scratch, and a lifecycle that will not let them close something twice. They do not need a workflow builder. They also should not each cost an agent seat, which is the quiet reason so many school tickets end up answered from a personal mailbox with no record at all.

The district support lead

One roll-up across many schools, with the scope wall derived from the session rather than from a filter in a URL, and counts that will not identify a person at a small site. Volume and backlog age are real numbers today; the compliance percentage is not, and the endpoint says so rather than letting a board slide claim it. Escalation walks a ladder and computes a correct delta — and reports that the delta is not yet durably written, which is the sentence to hold us to.

The studio serving many schools

A photography or services operation sits on the other side of the same wall: the scope chokepoint separates studio scope from school scope, so a studio ticket cannot surface in a school reader’s queue and vice versa. Tickets can carry an opaque reference to an order or a page without carrying anything about a person. If you also book time, schedule.software runs on the same estate and is a separate product with its own site — not a module you have to buy here.

How it works — the path a request actually takes

Five steps, described as they run today.

Nothing in this sequence is aspirational. Where a step has an honest-off seam inside it, the step says so in the same breath as the capability.

Step 1 — Try to make the ticket unnecessary

A parent, student or staff member starts typing a subject in the request portal and sees matching PUBLISHED knowledge-base articles, filtered on the server for their audience. The suggestions are retrieval, not generation — a real article or nothing. If the knowledge base is empty or matches nothing, the portal says so and moves on, because a fabricated suggestion costs more trust than a blank space saves.

Step 2 — File it, and the owner is decided by the session

The subject, body, surface and stated priority go up. The owner is the authenticated requester, read on the server from the session context. The reporter’s stated priority maps onto an escalation tier by a documented seam — urgent to the deepest tier, high to the middle, low and normal to the front line. The response reports, on every single ticket, that AI triage did not run and why. Nothing classified this ticket except a human’s own choice of priority.

Step 3 — It lands in a queue that is scoped, sorted and suppressed

The operator queue admits the ticket only if one of its scope keys is in the viewer’s allowed set, derived server-side from their session. Sorting is deterministic — priority descending, then age — not a learned ranking. Each row carries an at-risk flag, and the aggregate counts pass through small-cell suppression so a cell below five renders as “<5”.

Step 4 — An agent picks it up, moves it, and answers in their own words

A staff member opens it in the workspace, assigns an owner, and moves it along the lifecycle. Every move is checked against the legal-transition table first; an illegal one is refused with a machine reason and nothing is written. A canned reply is a human-authored template the agent fills in and previews. A placeholder left empty stays visible as a literal token and the render is marked incomplete.

Step 5 — The clock, the report, and what the report will not pretend

The SLA engine measures against a twelve-cell grid of business-hour budgets on an injected calendar, pausing while the ticket waits on the reporter and flipping to at-risk at eighty percent of budget. The roll-up you read carries provisioned:false. Not because the first-response and resolution stamps are missing — they are written on the row now — but because the projection feeding the SLA engine still hands it null for both, so the clocks run from the open anchor. The endpoint tells you that in the payload rather than in a footnote nobody reads. Volume and backlog age are real. Latency, compliance percentage and satisfaction trend report themselves unavailable.

Pricing — the plan, stated as a plan. Not a live checkout.

Priced per organization, because the number of people who help should not be the meter.

Per-agent-seat pricing makes sense for a support organization whose agents are full-time agents. It makes no sense for a front office of twelve people who each answer four tickets a week, and its predictable effect is that six of them get read-only access and answer from their own mailbox instead — outside the queue, outside the audit trail, outside every guarantee on this page. So the meter is the organization. Everyone who helps gets an account.

These are the planned launch tiers. There is no checkout on this site, no card is captured, and nothing here is a price you can be billed at today. Talk to us and we will tell you what access looks like for your operation, including which of the held capabilities matter to you and when they land.

Single school

School

$600 – $1,800 / yr

Per school · planned

  • Unlimited staff accounts — no agent seats
  • Ticket lifecycle on durable rows
  • Scoped triage queue with suppressed counts
  • Canned-reply catalogue
  • Family request portal with knowledge-base deflection
  • Volume and backlog-age reporting

Studio & services

Studio

$900 / yr

Per operation · planned

  • Studio scope, walled from school scope
  • Opaque order and page references on a ticket
  • Unlimited staff accounts
  • Same queue, SLA grid and suppression

Licensed

Self-hosted & white-label

Custom

Licensed engine · planned

  • The engines, not a reseller wrapper
  • Self-hosted or managed
  • Your brand, your domain
  • Held capabilities enabled on an agreed schedule

Figures are the planned launch tiers. This is not a live checkout, no card is captured on this site, and no payment is taken. There is no payment surface anywhere in the help desk itself: a search of the operations route for payment, checkout, price, invoice or billing returns no code.

The summary, in three lists

On, half-on, and off. There is no fourth list, and nothing sits in one.

On. The ticket lifecycle on durable rows, with owner isolation and a legal-transition table. The scope-walled operator queue with small-cell-suppressed counts and a per-row at-risk flag. The SLA policy grid on a business calendar. Manual assignment. Ticket volume and backlog-age analytics. The canned-reply catalogue with fail-visible placeholders. The mounted agent workspace and the mounted family request portal, including knowledge-base deflection over published articles. The synthetic status probe. The support-status tile presenter that renders an unknown channel muted rather than green.

Half-on, and it says so in the response body. SLA clocks run on the open anchor only — not because the first-response and resolution stamps are missing (they are written on the row) but because the SLA projection still reads null for both — provisioned:false. Assignment is manual, because there is no agent roster — auto-pick unprovisioned. Escalation computes a correct delta and does not write it — persisted:false. Satisfaction responses are validated and not stored — persisted:false. Latency, SLA compliance and satisfaction trend report available:false rather than plotting a zero.

Off, and built. Corridor up-routing is registered and gated behind a flag that defaults off, so every write returns 503. Email intake and parsing, cross-channel normalization and dedupe, the automation rule engine, AI triage, deflection analytics, agent presence, audit replay, roster context, per-tier queue projection and macro actions are all written, tested, and have zero production callers. That list is generated from an audit of callers, not from a plan, and it is the list to hold us to when it changes.

Talk to us · School · District · Studio · Licensed

A conversation, a scoped pilot on one queue, and no card at any point.

There is no self-serve sign-up here, because self-serve provisioning is not built and a form that cannot provision an account is a form that misstates what it does. What there is: a conversation where we walk the live surfaces and name the held ones in the same words this page uses, and then a pilot on the single queue that hurts most.

For a school we start with the front-office queue and the family portal. For a district we start with one school and the roll-up, so you can see what the suppression does to your numbers before it is everywhere. For a studio we start with the scope wall, because that is usually the question. For a licensed deployment we start with which held capabilities you need switched on and what that schedule looks like.

If a data-handling review is what stands between you and a pilot, say so in the first email and we will lead with it rather than with the product tour. The privacy and data page is the short version and the getting-started page sets out the steps.

Write to [email protected]. It reaches a person.

FAQ

Common questions, answered at the level a review will ask them.

What can I actually run on this today?

The ticket lifecycle on durable rows — file, read, list, transition, assign — with owner isolation and a legal-move table that refuses an illegal transition. A scope-walled operator triage queue with small-cell-suppressed counts and a per-row at-risk flag. A canned-reply catalogue with placeholder rendering. A family-facing request portal with knowledge-base deflection. A synthetic status probe along the real user path. Ticket volume and backlog-age analytics. That is a working support desk. It is not the whole product, and the sections above name the rest by module.

You keep saying “built, not switched on”. Is that a euphemism?

It is a load-bearing distinction and we would rather it were awkward than vague. A module in that list has source, has tests, and has zero production callers — we checked by grepping the API and web application for each export name, not by reading a status document. Email-to-ticket intake, cross-channel dedupe, the automation rule engine, AI triage, deflection analytics, agent presence and audit replay are all in that state today. The honest way to weigh them is as a commitment with the hard part already done, not as something you can use in week one.

How is this different from Zendesk or Freshdesk?

Three structural differences, and none of them is a feature we have and they lack. First, the meter. Zendesk and Freshdesk bill per agent seat. A school front office is a dozen people who each answer a handful of tickets a week, which is a dozen seats at either vendor and a reason to give six of them read-only access they will route around. Our planned tiers are per school and per district; the number of humans who help is not the meter. Second, the wall. Our ticket table cannot hold a student-shaped column — the column names are unrepresentable and a database check forbids a parent or student surface from coupling a subject. On a general-purpose help desk, that wall is a custom-field policy somebody has to keep enforcing. Third, the instrumentation. Our SLA endpoint returns provisioned:false and our analytics return available:false on the metrics whose inputs do not exist yet, rather than drawing the chart anyway.

Why does a queue count sometimes read “<5” instead of a number?

Because a small count in a narrow scope is an identifier. If a district dashboard shows one school with two open tickets in the safeguarding tier, and that school has one counsellor, the number has told you who. Every aggregate count in the queue passes through small-cell suppression: a value below five renders as the literal string “<5” and the raw number is not sent to the client. The suppression is in the shared engine the route calls, not a formatting rule in the interface that a different client could skip.

Can a support ticket ever contain a student’s name?

Not as a column. The migration that creates the table was written so the student-shaped column names — first name, last name, student id, subject id, date of birth — are not representable on it, and a database CHECK forbids a parent or student surface from coupling a subject at all. A ticket carries an adult contact and opaque correlation handles: a request id, a screen label with the query string stripped at the boundary, the actor’s role, and tenant identifiers. What we will NOT claim is that free text is impossible: the summary and detail are adult-authored prose, and a person can type anything into a text box. A content-safety pass runs server-side over that prose. The schema does not invite subject data in; it cannot stop a human from typing it.

What is the tenancy wall, exactly?

Row-level security at the database with a command-split policy. A tenant reads its own school’s tickets under a select policy; only a system lane may write, raise or close a ticket that belongs to no tenant, and a tenant may only write a ticket pinned to its own school. A tenant can never privatize a platform ticket and then read it. The exception you should know about, because it is ours and not yours: that select policy admits a SYSTEM lane unconditionally, and the staff per-ticket read (plus the transition and assignment that reuse it) opens one. On that path the database wall is bypassed by design of the helper, and no scope claim is re-checked above it. The staff queue read and every requester read are walled normally. We treat the gap as a defect and it is named in full on this page. Above that, the operator queue derives the viewer’s scope claim from the session and resolves it to a key set; a ticket is admitted only if one of its own keys is allowed. An unknown claim resolves to no keys and therefore an empty queue — fail-closed, not fail-open with a filter.

Is there AI in here?

Not running. An AI triage module with an explicit grounding gate exists in the codebase and is not wired, and every ticket the live route creates reports AI triage as disabled with a machine reason. The queue’s sort order is deterministic and its at-risk flag is an age-and-priority heuristic, not a learned score. The canned replies are four templates a human wrote and an agent fills in. The family portal’s deflection is retrieval over published articles with no generation at all, because that surface faces minors and their guardians.

What does the SLA grid actually promise?

It is policy-as-code, not a signed service agreement, and the distinction matters. The built-in grid is three escalation tiers by four priorities — twelve cells — each holding a first-response and a resolution budget measured in BUSINESS hours against an injected calendar, so a weekend does not breach anything. The clock pauses while a ticket waits on the reporter. A live clock flips to at-risk at eighty percent of budget. An unrecognized tier or priority falls back to the strictest cell in the grid rather than the most forgiving. A deployment can override any cell. And today the compliance number is not computed — not for want of the response stamps, which are now written on the ticket row, but because the SLA projection does not yet read them and passes null instead. The endpoint reports that rather than hiding it.

Does anything here take money?

No. There is no payment surface anywhere in the help desk — a search of the operations route for payment, checkout, price, invoice or billing returns no code. The family portal client states plainly in its own header that it handles no money. Pricing shown on this site is the planned launch tier, not a live price you can be billed at, and this marketing site runs no checkout and captures no card details.

Why is so much of the engine work not switched on?

Two reasons, and one of them is unflattering enough to be worth printing. The first is deliberate: several seams are held until their durable half exists. Escalation computes a correct delta and refuses to write it because there is no persistence helper; the corridor is gated on a flag that defaults off. Holding an enablement is safer than shipping a write that half-works. The second is a plain defect we found while auditing this page. The shared package re-exports by star, and several module names collide — a terminal-status check exists in three modules, a macro renderer in two, a support-tier list in two. An ambiguous star-exported name is silently dropped, so those functions are not importable by name from the applications at all. The two names in the macro module that are unique are exactly the two that reached the workspace screen. That is a fixable barrel problem, not a missing feature, and we would rather you read it here than discover it in a code review.

Can we run this ourselves, or white-label it?

That is the intent, and it is the plan rather than a live self-serve offering. The stack is wholly owned — the ticket engines, the SLA policy, the queue projection, the suppression layer and the presenters are ours, not a wrapper around somebody else’s desk — so there is no per-agent seat we are reselling to you at a markup. Talk to us about a licensed or self-hosted deployment and we will tell you which pieces are on and which are held, in the same words this page uses.

What happens if I ask you for something on the held list?

You get a date and a caveat, or a no. What you will not get is the feature described in the present tense on a call and then a discovery in month two. The held list on this page is generated from a real audit of production callers, and we intend to keep it accurate as things switch on. If it is stale when you read it, that is a bug in this page and we would like to know: [email protected].