All writing

Guide

My app ships its email dark: kill-switch first, sending later

On launch day my property portal could not email a single person, by design. The one path that could have reached a real tenant never passed through my kill switch at all.

Key findings

  • Outbound email shipped dark: a comms_live flag held every send off, and flipping it on still limited delivery to a server-side allowlist holding exactly one address — mine.
  • The near-miss came through the path the flag never governed: platform sign-in emails bypass an app-level kill switch, and a stale test script's demo login had quietly become a real tenant.
  • The rule that came out of it: synthetic test identities on my own alias, plus the habit this page hands you: a written inventory of every sender the switch does not govern.

The portal's outbound email shipped dark: a comms_live flag held every send off, and the day the flag flipped live, a server-side allowlist still limited delivery to one address — mine.

One claim up front, and the rest of this page defends it: a kill switch is a map of the paths you remembered to route through it. The send that nearly reached a real tenant was never on that map, because I didn't write it. The sign-in platform did.

The running example is 918TS, the property portal I built and run for a four-unit rental. AI agents write most of its code, which is exactly why the comms design assumes some path, someday, will try to send something it shouldn't.

The kill switch governs only the routed paths: app sends pass the comms_live flag and allowlist; platform sign-in emails bypass both. Two ways email leaves the portal GOVERNED: every app-level send App email notices, replies comms_live flag off = nothing sends allowlist + per-person opt-in launch day: one address (the owner's) BYPASS: platform sign-in emails Sign-in email sent by the auth layer stale demo login persona is now a real tenant a real tenant's inbox plus real rows in a live unit Caught in review before anyone ran it. Old script barred; rewrite planned: a synthetic test tenant on the owner's alias.
Every app-level send routes through the comms_live flag and the one-address allowlist; platform sign-in emails never touch the flag. A stale demo script was the live wire, caught in review. Diagram source: this page; maps 1:1 to the comms_live flag and send_email_guarded allowlist described in the text.

Why ship email dark instead of just being careful?

Because careful is a mood and a switch is a mechanism. A property portal emails people about their homes, which is the least forgiving inbox there is, so every app-level send has to clear three stacked refusals.

The global flag comes first: comms_live false means nothing sends, anywhere. Then structure: exactly one function in the database, send_email_guarded, can make an outbound call at all, and only server roles may execute it. No client code path can email anyone. Inside that one function sit the recipient gates: a hard allowlist (the only addresses the system is permitted to email) plus a per-person opt-in. When the flag flipped live on 2026-07-22, the allowlist held a single owner-controlled address: my own. It has widened since, one deliberately reviewed person at a time. Widening is an act, never a default.

A July audit then corrected my own launch recipe, which is the good kind of embarrassing. My remaining launch task read "declare fully launched," and I assumed that meant flipping comms_live=true. The audit traced the real send path and found the flip alone would have emailed nobody — at that moment, all 15 people in the system were opted out and the allowlist still held one address. The lever I thought launched comms was the outermost of three. Being over-dark cost nothing. The reverse misreading is the dangerous one: believing a single flag holds everything back.

Two surfaces are barred outright, not gated. The feedback loop and the agent-reply loop, the places where an AI writes text people read, can never send email, under any flag state. The playbook's reasoning is the part worth stealing: a feedback sender, even a gated one, is a surface a future migration (a database change script) could accidentally widen. Removing a capability beats guarding it.

Most teams wire this in the opposite order — build the sender, style the templates, bolt on a flag after the first misfire. For anything touching tenants, patients, or customers, I think that order is indefensible. A flag added after launch governs the paths you remember, under pressure, on a day something is already wrong. The portal's incident checklist leans the other way: its first containment move for suspected live harm is "disable at the narrowest existing kill switch," an instruction that only works because the switches predate the features.

What does a kill switch actually govern?

Only the paths routed through it. That sounds like a tautology until you inventory your paths and find one you never controlled. Mine was authentication email. When someone signs in, the platform handling sign-in issues the confirmation email itself. My app code never touches that send, so comms_live means nothing to it and the allowlist never sees it.

The near-miss walked straight through that hole. A nine-step phone-walkthrough script, written in the demo era to test the portal from a phone, was still sitting among the launch tasks. Step 8 signed in as a demo persona. But the demo had since become production, and the person behind that persona was now a real tenant. Run as written, the script would have fired a real sign-in email into a real tenant's inbox, and steps 3 through 6 would have written real rows into a live unit's records. Details are generalized here to protect the people involved.

Nobody ran it. Review caught it first, and the decision queue now carries the warning: do not run the old script. Be precise about what almost failed, though — not the kill switch. The switch did its job on every path it governed. What failed was my belief that "email is dark" was a property of the system. It was a property of the routed paths only.

The quieter lesson cost me more thought: demo identities rot. The persona was safe the day it was invented; the world changed underneath it. The moment a demo becomes production, every fixture (the fake people and sample data a demo runs on) that borrowed a real identity becomes a live wire. No code diff will ever flag it, because nothing in the code changed.

What changed after the near-miss?

Three things. The script's replacement design: a synthetic test tenant (a made-up account I control) on my own email alias, the nine steps rewritten phone-ready, a cleanup step at the end. The old script stays barred until that rewrite ships, and acceptance tests leave nothing behind. The standing rule, now written into the project's status file: acceptance tests use synthetic or owner-controlled identities only, and sign-in or comms tests never go to residents. And the habit this page exists to hand you: a written inventory of the senders your switch does not govern.

That inventory is three questions. Which systems can email your users without executing a line of your code (the sign-in platform, the payment processor, the monitoring layer)? For each: governed, or ungoverned-but-known? And which of your test fixtures pointed at real people the last time the world moved under them?

This portal is one corner of a larger discipline: AI agents do the work, and layered checks treat the work as wrong until proven otherwise. The full tour is at how do I know the AI is right.

So the question I actually want answered: what can send email on your behalf, today, that your kill switch has never heard of?


Method & data

Method: the 918TS portal's dark-launch comms design and the July 2026 audit that corrected its launch recipe — the comms_live flag, the send_email_guarded allowlist, the decision queue's walkthrough warning, and the feedback-lifecycle playbook, generalized
Data: patterns and methods only; no names, dollar figures, addresses, case identifiers, or confidential content · Last checked: 2026-08-14

How this was made

AI-drafted, adversarially checked, human-directed. My AI assistant wrote this from the system's own records — the portal's decision queue, the feedback-lifecycle playbook, the agent-loop backlog doc, and the release-and-incident checklist. A separate AI session then tried to break every claim against those records, and automated privacy and readability gates ran before publish. I direct this pipeline, own every boundary in it, and audit published pages on a rolling basis — if you find an error, tell me and it goes in the corrections log, dated, never silent.

I'm Ali — I run real life-and-work admin on AI agents, then check their work in the open. More at /about.

Published under my standards. Found an error? Tell me — corrections go in the corrections log, dated, never silent.

Cite this

@online{ali2026killswitch,
  author = {Ali},
  title  = {My app ships its email dark: kill-switch first, sending later},
  date   = {2026-08-14},
  url    = {https://alidoes.ai/ai-comms-kill-switch/}
}

Caught something I got wrong? Send it directly. Confirmed corrections go in the corrections log.