Skip to content
Evode Manirahari

Selected workFocusGate

FocusGate

Staying reachable without staying distracted.

Do Not Disturb decides on sender. The decision that actually matters is on content — so move it to a server and let it fail toward delivery.

Stage
Experiment
Role
Builder
Status
v1 relay backend, unvalidated
Timeline
2026
Surfaces
Inbound webhook · operator dashboard
Where it stands
v1 relay backend and operator dashboard, running locally. The concierge test that would validate it has not been run.

A working student sits down to study. Their manager might text about a shift that pays their rent, or might text nothing important for three hours. Focus mode blocks the app, so both are silenced; turning it off means checking the phone every ten minutes. Neither option is acceptable, so they pick the worse one and stay distracted.

The operating system already does app blocking well and for free. What it cannot do is read the message. FocusGate owns exactly that gap and nothing else.

Message routing

Manager texts

To the relay number, not the student

Inbound webhook

Signature verified in production

Study block active?

Block state machine

No — outside a block

Forward normally. Nothing is triaged, nothing is read.

Yes — inside a block

Triage the content, then deliver, or hold and auto-reply once.

Triage lives on the server, so one code path covers both mobile platforms and no notification interception is required.

Work messages flow through a number the relay controls. Triage happens server-side, which is why the same backend works on both mobile platforms with no notification interception.

Why a relay and not an app

The obvious build is a native app that intercepts notifications and decides which to show. On iOS that is not permitted, and on Android it is fragile. More importantly, it duplicates something the platform already does better.

Routing the messages through a number the system controls moves the decision to a server, where it is one code path for every platform, inspectable by an operator, and testable without a device. The trade is that the student has to tell their manager to use a different number — which is precisely the assumption that has not been validated yet.

The urgency decision

  • Contains an explicit urgency keyword

    Deliver now

    Deterministic floor. Runs before the model, so it holds when inference does not.

  • Model judges it urgent

    Deliver now

    Content-level judgment — the thing the operating system cannot do.

  • Model judges it not urgent

    Hold + auto-reply once

    The sender learns the student is studying, so they do not escalate.

  • Error, timeout, or ambiguity

    Deliver now — fail open

    A false interrupt is annoying. A missed shift offer costs money and trust.

Three of four paths deliver. That asymmetry is the specification, not a tuning accident.

Four paths, three of which deliver. The deterministic keyword floor runs ahead of the model, so the most urgent cases never depend on inference being available.

Every uncertain path biases toward delivery. A false interrupt is mildly annoying; a missed shift offer costs money and trust, and trust is the whole product.

Fail open, deliberately

The system fails open on every error, timeout, and ambiguous verdict. That is an unusual default for a filter — most filters fail closed and drop the uncertain case — and it comes directly from the asymmetry in the cost of the two mistakes.

It also sets the product's ceiling honestly. FocusGate can only be as valuable as it is trusted, and trust is destroyed by exactly one missed urgent message. Optimising the model for precision at the expense of that would be optimising the wrong thing.

What the backend does

01

Block state machine

Study blocks start and stop from an operator dashboard. Outside a block nothing is triaged at all — messages forward normally.

02

Hold and auto-reply

A held message auto-replies once per sender, so the manager knows the student is not ignoring them and does not escalate.

03

End-of-block digest

Everything held is delivered as a single summary when the block ends.

04

Webhook verification

Inbound webhook signatures are verified in production against the public base URL, so the relay cannot be driven by forged requests.

The test that has not been run

The pain is real — working students described it unprompted. What is unproven is whether they will route their texts through a different number and then trust the result enough to stop checking their phone.

That is a concierge test with one real student and their real manager, not a codebase. It is written up in the repository as the next step, and until it runs, everything here is a well-argued hypothesis with a working implementation attached.

Evode’s contribution

What I built

Solo build. The most valuable decision was about scope rather than code:

  • 01

    Made the call to relay rather than block — identifying that the operating system already owns app blocking and that content-level judgment was the only unclaimed gap.

  • 02

    Designed and implemented the triage path: a deterministic keyword floor ahead of the model, a model verdict for the ambiguous middle, and fail-open behaviour on every error and timeout.

  • 03

    Built the block state machine, held-message queue, once-per-sender auto-reply, and end-of-block digest.

  • 04

    Implemented the inbound webhook handler with production signature verification against the configured public base URL.

  • 05

    Built the operator dashboard for starting and stopping blocks and inspecting what the triage decided and why.

  • 06

    Wrote unit tests for triage and the block state machine, and scoped the repository explicitly for a concierge validation test rather than for scale.

Honest limits

What remains unproven

  • The concierge test has not been run. No student has routed real messages through it during a real study block.
  • Triage quality has not been measured against real messages — only against unit tests I wrote from my own assumptions about what urgent looks like.
  • The adoption assumption is the weakest link and I know it: asking someone to tell their manager to use a different number is a real cost, and it may be the thing that kills the product rather than any classification error.

Lessons

What I took from it

Name the constraint before choosing the architecture

Once trust was identified as the binding constraint rather than accuracy, fail-open stopped being a compromise and became the specification.

Do not rebuild what the platform gives you

The temptation was to build a full focus app. Everything except content judgment already existed and worked better than a rewrite would have.

Scope the repository to the next question

The backend is deliberately small because the next thing to learn is behavioural, not technical. Building for scale first would have been building for a question nobody had answered.

Reference

Stack

  • TypeScript
  • Node.js
  • Twilio
  • Claude
  • Webhooks
  • Vitest

Responsibilities

  • The product decision to relay rather than block
  • Server-side triage with a deterministic trust floor
  • Fail-open routing on every uncertain path
  • Block state machine, held messages, and end-of-block digest
  • Twilio signature verification and operator dashboard