From outage detection to field resolution — how an emergency callout moves through the system ·
~16 min read~– min read
· Suggested by Bob
productengineering
A fiber line goes dark at 2 AM. A NOC operator clicks "Create ECO." What happens next — the pager calls, the field dispatch, the status tracking, the completion handshake — is a choreography of process managers, external APIs, and state machines that turns a single button press into coordinated emergency response. This cairn follows an ECO from creation to resolution, layer by layer.
Everything starts with a NOC operator and a geometry. The creation form is deliberately small — location (PostGIS GeoJSON Point or LineString), job type (CMR/MFR/FNOL), provider reference, KCCI job ID, description, optional attachments, and dual tenant context (NOC + OSP). The CreateECOCommand validates and emits a single ECOCreatedEvent, the outbox publisher (polling every 100ms) fans it to three subscribers — the ECO projection, the WebSocket publisher, and the Render integration processor — and the ECO sits at status OPEN with render integration PENDING. Neither stays that way long.
Reaching the Field: Render Integration
The Render integration processor is the first process manager to act. It looks up the OSP’s Render instance, gets an OAuth2-authenticated client (with singleflight to prevent token stampedes), and creates a task carrying the ECO’s geometry, description, and the piece of metadata that defines membership: an exact, case-sensitive General label holding the tenant-formatted ECO job ID (e.g., E2-00524-2026). A Render task belongs to the ECO only when it carries that label; the task’s subsector is the literal ECO, which satisfies Render’s required categorization field and is not an identifier. Success moves render integration status to TASKED; failure to FAILED with an error.
Pager Dispatch: Getting Humans Moving
Creating a task puts work in the system, but tasks don’t move themselves. When render integration reaches TASKED, the pager processor uses PostGIS to resolve the pager region from the geometry, loads the OSP’s regional contact list, fires a StartPagerRunCommand, and calls Twilio. State lives in pager_process_state rows with a deadline_at timestamp (default 30s, extended on each webhook) — not in memory — so a 3 AM debugger gets SELECT * FROM pager_process_state WHERE eco_id = '...' instead of framework log archaeology. A background worker polls every 5 seconds for overdue deadlines; webhooks (CALLING/ACCEPTED/DECLINED/FAILED) extend the deadline and feed the PagerRun timeline. Optimistic locking (a version column) handles the race between webhooks and the timeout worker. On DISPATCHED, the operator sees the ECO transition; the intermediate TASKED is internal plumbing.
Watching the Work: Render Polling
Once dispatched, Strike’s job shifts from orchestration to observation. A per-instance background schedule fetches each pollable ECO with Render’s exact label_name filter, while an immediate dispatch trigger avoids waiting a full interval. Fingerprints publish events only when task data changes, but every fetch still spends the Render credential’s daily allowance. Late-clone detection can revert a COMPLETED ECO to IN_PROGRESS. The full caller, cache, throttling, and capacity model is in Render Is a Shared Request Budget.
Three Layers of Status
Strike maintains three distinct status models.Layer 1: event-sourced ECO state (OPEN/IN_PROGRESS/COMPLETED) — the truth, derived by replay. Layer 2: Render task status (blueprinted → allocated → releasable → released → completed, with jeopardy as the at-risk lateral state) — external system state, polled but not displayed. Layer 3: NOC display status — the five human-meaningful states operators actually see (Pending, Assigned, In Progress, Blocked, Complete), mapped from Render’s six-state task lifecycle (jeopardy → Blocked; the others collapse into Pending/Assigned/In Progress/Complete). A fourth, internal-only render integration status (PENDING/TASKED/DISPATCHED/FAILED) lives alongside as a backend gate. Operators don’t need to know whether Render is allocated vs. releasable; the separation lets each layer change without touching the others.
Key Takeaway
The three-layer status model exists because operators and systems have different needs. Each audience gets exactly the information they need; Strike stays decoupled from Render’s implementation details.
The Mock Server: A Simulation Engine
You can’t develop an emergency response system by waiting for emergencies. The mock server (packages/mock/) is a single Go service exposing a Render mock at /render/*, a Twilio mock at /twilio/*, and a /dashboard for scenario control (port 8090 — matching the legacy Python mock so API env vars just work). The dashboard now composes test cases from separate Render, Pager, Speed, Fault, and Workflow controls instead of one scenario picker: Render can succeed, fail, or hold manually; Pager can accept on the first, second, or third contact, exhaust every contact, time out, or hold manually; Speed scales all delays; Fault injects HTTP failures; Workflow selects the ECO orchestration template. Docker Compose profiles (just docker for-api, just docker for-web) start what you’re not touching. The dashboard uses Tailwind CDN + Alpine.js — no build step, refusing to become a second frontend.
Completion and the Audit Trail
An ECO completes when every labeled task is done in Render — completed, approved, or integrated all count, and at least one labeled task must exist — but “complete” isn’t simple. The polling service marks the ECO COMPLETED and starts a grace period (default 24 hours) during which polling continues; a new task or a reopened task reactivates to IN_PROGRESS. After the grace period expires without activity, the ECO reaches hard completion and polling stops permanently. What remains is the event stream — ECOCreatedEvent, RenderIntegrationStatusUpdatedEvent, every PagerRun event, every StatusUpdatedEvent, every detected task change — and that’s the audit trail. Not a log file that rotates, not a column that gets overwritten: the data model itself.
Key Takeaway
The event stream as audit trail isn’t bolted onto the system — it’s a consequence of the architecture. Store state as events, and the audit trail is free.
Putting It All Together
The full lifecycle is a sequence of handoffs between process managers, each responsible for one phase: NOC creates ECO (T+0), Render integration tasks (T+1m), pager dispatch and supervisor accept (T+5m), tech assigned and dispatched (T+10m), investigation and follow-on tasks (T+30m–T+4h), grace period expires and ECO hard-completes (T+28h). The processors don’t talk directly — they react to events and emit commands, connected by the event bus, with the event store as the shared record of what happened. That’s how a single button press cascades into a multi-hour, multi-system, multi-human workflow where each participant sees exactly what they need.
When the Fiber Goes Dark — The domain context that explains why ECOs exist and what OSP emergency response looks like.
Events All the Way Down — The event sourcing architecture that makes this lifecycle possible, including the Watermill migration.
Render Networks — Field workforce management platform; the external service OSP contractors use for task management and crew dispatch.
Twilio — Communications API used for pager dispatch via Studio flows and webhook callbacks.
An ECO Is Born
Everything starts with a NOC operator and a geometry. Someone detects an outage — maybe an alarm fires, maybe a customer calls, maybe a monitoring system flags a loss-of-light condition — and the operator creates an Emergency Callout Order in Strike’s web UI.
The creation form captures surprisingly little data, and that’s deliberate. In an emergency, you want the fastest path from “something is broken” to “someone is on their way.” The ECO captures:
Location — a GeoJSON Point or LineString stored as PostGIS geometry. This is the incident site, and it drives everything that follows: which contractor gets paged, which Render project receives the task, which pager list applies.
Job type — CMR (Claims Management Resource), MFR (Maintenance Field Request), or FNOL (First Notice of Loss). These categories determine downstream billing and reporting.
Provider reference — the initiating NOC reference, often a CMR, MFR, or FNOL number, so Strike can reconcile the ECO back to the source record that started the callout.
KCCI job ID — a work-order handle such as E2-0000-2025 during early rollout, where the prefix identifies the customer or workstream, the middle number increments the job, and the year keeps the sequence readable in operations and billing conversations.
Description — free text from the operator describing what’s known about the outage.
Optional attachments — photos, PDFs, screenshots, or field notes that are useful as evidence but should not block creation when the emergency needs to move.
Tenant context — the NOC tenant creating the ECO and the OSP tenant who will work it. Every ECO exists at the intersection of these two entities.
The practical intake contract is slightly more precise than the UI may look. A location may need to accept multiple coordinate pairs when the incident covers a span rather than a single point; the early Arkansas and Oklahoma rollout notes bounded coordinates to the operating territory rather than accepting arbitrary map input. CMR, MFR, and FNOL are provider-side initiating references, while the KCCI job ID is the contractor-facing work-order handle. Keeping those identities separate, alongside the description and optional visual evidence, connects the NOC’s source record, Strike’s ECO aggregate, Render’s task model, and the later invoice or as-built package.
Definition
ECO as Aggregate Root: In event-sourced terms, the ECO is an aggregate — a consistency boundary that accepts commands and emits events. The CreateECOCommand is validated by the aggregate (geometry must be valid, job type must be recognized, description can’t be empty), and if it passes, a single ECOCreatedEvent is appended to the event store. That event is the ECO’s birth certificate — immutable, timestamped, and carrying the full initial state.
The moment that event hits the outbox, the system wakes up. The transactional outbox publisher — polling every 100ms — picks up the event and fans it out to three subscribers simultaneously: the ECO projection (which creates the read model for the UI), the WebSocket publisher (which notifies connected browsers), and the Render integration processor (which starts the external integration chain).
The ECO’s initial status is OPEN. Its render integration status is PENDING. Neither will stay that way for long.
Reaching the Field: Render Integration
The Render integration processor is the first process manager to act on a new ECO. It’s a single-shot workflow: receive the ECOCreatedEvent, create a task in the OSP’s Render Networks tenant, and report success or failure.
Render Networks is the field workforce management platform that OSP contractors use to manage their crews. Strike doesn’t replace Render — it creates tasks in Render and polls for status changes. Field technicians never interact with Strike directly.
The processor looks up the OSP tenant’s Render instance from the database — each OSP has its own API credentials, project name, and default task type. It creates a ClientFactory that handles OAuth2 token management with singleflight to prevent token stampedes when multiple ECOs are being processed simultaneously.
The task creation request carries the ECO’s geometry, description, and the piece of metadata that defines membership: an exact, case-sensitive General label holding the tenant-formatted ECO job ID (e.g., E2-00524-2026). A Render task belongs to this ECO only when it carries that label. The payload also sets subsector to the literal ECO — Render requires a categorization value, and a neutral constant keeps Strike’s tasks from colliding with however the OSP organizes its own work — plus task_metadata.eco_id, which is provenance for the initial task and not membership.
Render’s own cloning preserves neither labels nor task metadata, so the grouping is a field procedure as much as a data model: after cloning an investigation or creating a follow-on independently, the responder adds the canonical ECO label in Render. An unlabeled follow-on is deliberately invisible to Strike rather than silently guessed at, which is why the drill briefing, the watcher, and the close-out label check are treated as required controls.
If the Render API call succeeds, the processor emits an UpdateECORenderStatusCommand with status TASKED, carrying the project task ID back from Render. If it fails — network error, authentication failure, missing Render instance — the status becomes FAILED with an error message, and the NOC sees an error state prompting escalation.
Key Takeaway
The render integration status (PENDING → TASKED → DISPATCHED → FAILED) is an internal tracking status, not shown to NOC operators. It exists for system health monitoring and debugging. On success, tasks just appear. On failure, operators see a simplified error state.
Pager Dispatch: Getting Humans Moving
Creating a task in Render puts work into the system. But tasks don’t move themselves — someone needs to answer the phone. The pager process manager is the orchestrator that turns a TASKED ECO into an acknowledged field dispatch.
When the ECO’s render integration status changes to TASKED, the pager processor wakes up. Its first job is geographic: using the ECO’s PostGIS geometry, it resolves the pager region — a polygon that maps to a contact list of OSP supervisors responsible for that area. Each OSP tenant maintains its own regional pager lists, so the same physical location can route to different people depending on which contractor is assigned.
The processor builds a contact list, generates a message ("NOC: ECO 524 - Fiber cut on Main St"), and fires a StartPagerRunCommand to create the PagerRun aggregate. Then it calls the Twilio API to initiate the first phone call.
PENDINGRUNNINGCOMPLETEDBackground worker checksdeadline every 5 seconds.Timeout triggers failure.Process state createdTwilio call initiatedDeadline extended on activityAccepted / Exhausted / Failed
Here’s where the design gets interesting. The pager process manager doesn’t hold state in memory. It writes a pager_process_state row to PostgreSQL with a deadline_at timestamp, then returns. A background worker polls every 5 seconds for overdue deadlines. When a Twilio webhook arrives (the contractor answered, declined, or didn’t pick up), the webhook handler updates the process state and extends the deadline.
This is a deliberate architectural choice. Traditional saga frameworks keep process state in memory or in framework-managed storage. Strike uses plain PostgreSQL rows because when paging fails at 3 AM, you want SELECT * FROM pager_process_state WHERE eco_id = '...', not framework log archaeology. The debuggability wins justified the extra implementation work.
Each webhook event — CALLING, ACCEPTED, DECLINED, FAILED — is recorded in the PagerRun aggregate’s timeline, building a complete record of every attempt. The process state uses optimistic locking (a version column) to handle the race between webhook delivery and the background timeout worker. If both try to update simultaneously, one gets a version conflict and retries.
Warning
The pager processor handles duplicate events explicitly. Watermill may deliver the same event more than once, so the processor checks for an existing pager run before starting a new one. Idempotency isn’t optional — it’s a correctness requirement.
When the pager run completes with a DISPATCHED outcome, the dispatch processor updates the ECO’s render integration status to DISPATCHED. The NOC operator, watching the live status, sees the ECO transition from “page sent” to “contractor dispatched.” They don’t see the intermediate TASKED status — that’s internal plumbing.
Watching the Work: Render Polling
Once a contractor is dispatched, Strike’s job shifts from orchestration to observation. The Render polling service runs a background loop, applying each Render instance’s configured interval and kill switch before querying pollable ECOs.
The polling service queries eco_views for ECOs in a pollable state — dispatched but not yet hard-completed. For each ECO, it fetches the ECO’s labeled tasks from the Render API with the exact label_name filter, paging until the result is exhausted — one page covers an ECO in practice — using the same ClientFactory and per-tenant credentials as the integration processor.
Change detection uses fingerprinting. Each task’s relevant fields are hashed into an MD5 fingerprint. The service caches fingerprints in memory (sync.Map) and only publishes change events when a fingerprint differs from the cached version. This prevents unchanged observations from generating event noise, but it does not prevent the Render request: capacity remains one request per ECO per cycle.
Poll cycle startsQuery ECOs with active pollingFor each ECOFetch tasks from Render APIby exact labelFingerprintchanged?Skip — no changesPublish task change eventAll taskscompleted?Start completion grace periodWait for instance intervalNoYesNoYesDone
The polling service also detects late clones — when a field technician creates a new task from a completed investigation. It tracks task counts per ECO and publishes a late-clone event when the count increases, which can revert an ECO from COMPLETED back to IN_PROGRESS.
An immediate fetch also runs when an ECO becomes DISPATCHED, so a safe five-minute interval does not leave the detail screen empty for five minutes. GraphQL reads can make their own live requests too. These paths share one credential allowance but do not yet share one coordinator; Render Is a Shared Request Budget maps all of them and the durable cache between them.
Late clones are a real operational pattern. A field tech completes the investigation, reports the damage, and then creates follow-on tasks for the actual repair work. The investigation might complete before the follow-on tasks even exist. Without the grace period and late-clone detection, Strike would prematurely close ECOs.
The service includes resilience features — exponential backoff on database errors, per-ECO locking to prevent concurrent read-modify-write races on fingerprint caches, and sync.Once guards for first-poll initialization. These aren’t premature optimization — they’re responses to production patterns where a single Render API hiccup shouldn’t cascade into missed updates for every ECO in the system.
Three Layers of Status
Strike maintains three distinct status models, and understanding why is essential to understanding the system’s design philosophy.
Layer 1: Event-sourced state (the truth). The ECO aggregate’s status — OPEN, IN_PROGRESS, COMPLETED — is derived by replaying its event stream. This is the authoritative record. When a StatusUpdatedEvent is applied, the aggregate validates the transition (you can’t go from COMPLETED back to OPEN without explicit intervention) and updates its internal state.
Layer 2: Render task status (external system state). Each task in Render has its own lifecycle: blueprinted → allocated → releasable → released → completed. These statuses reflect Render’s internal workflow — contractor assignment, crew dispatch, work execution. Strike polls these but doesn’t display them directly.
Layer 3: NOC display status (human-friendly view). This is what operators actually see. The display status maps Render’s six-state task lifecycle down to five human-meaningful states:
Display Status
Meaning
Maps From
Pending
Work identified, not yet assigned
blueprinted, pending, tasked
Assigned
Allocated to a tech or crew
allocated, releasable, released
In Progress
Active work happening
released (after assignment)
Blocked
Problem preventing progress
jeopardy
Complete
Work finished
completed
Key Takeaway
The three-layer status model exists because operators and systems have different needs. NOC operators care about “is someone working on this?” — not whether Render’s internal state is allocated vs. releasable. By separating internal tracking from display, Strike can change Render integration details without touching the NOC UI, and vice versa.
The render integration status — PENDING, TASKED, DISPATCHED, FAILED — lives alongside these as a fourth, internal-only status. It’s a backend gate confirming the Render handoff succeeded. Operators never see it. When integration succeeds, tasks simply appear. When it fails, they see a simplified error prompting escalation.
This separation costs something: there are now multiple status fields to maintain, mapping logic at the API boundary, and documentation to keep synchronized. The payoff is that each audience gets exactly the information they need, training new NOC staff doesn’t require explaining Render’s internals, and the system is decoupled from Render’s implementation details.
The Mock Server: A Simulation Engine
You can’t develop an emergency response system by waiting for emergencies. The mock server — a Go application in packages/mock/ — simulates both the Render Networks API and Twilio’s call infrastructure, providing a complete end-to-end development environment.
The mock server exposes three surfaces: a Render API mock at /render/* that simulates task creation and status progression, a Twilio API mock at /twilio/* that receives call requests and sends webhook callbacks, and a control dashboard at /dashboard for composing scenarios and watching execution in real time.
The dashboard composes test cases from five independent controls rather than one preset picker. Render and Pager are set separately, Speed multiplies every delay, Fault injects API errors, and Workflow selects the ECO orchestration template:
Goal
Render
Pager
Speed
Fault
Workflow
Happy path
succeeds
accepts-first
normal
none
full-repair
Render failure
fails
Any
normal
none
full-repair
All contacts decline
succeeds
exhausted
normal
none
full-repair
Pager timeout
succeeds
timeout
normal
none
full-repair
Slow render
succeeds
accepts-first
slow
none
full-repair
Second contact accepts
succeeds
accepts-second
normal
none
full-repair
API outage drill
succeeds
accepts-first
normal
outage
full-repair
Stuck external system
manual
manual
normal
none
manual
The default Render path progresses to completion in about five seconds. The default Pager path has the first contact accept in about six seconds. Speed can compress those delays for tight development loops or stretch them for watching timing-sensitive behavior; Fault can simulate rate limits, flaky responses, or a full outage without changing the application under test.
Each ECO’s mock state is tracked independently, keyed by ECO ID. The orchestration engine manages state transitions per ECO — you can have one ECO exercising succeeds plus accepts-first while another uses succeeds plus exhausted, simultaneously. State is in-memory (acceptable for a dev tool), protected by mutex, and visible through the dashboard’s polling UI.
The mock server replaced a Python script for Render simulation and a Kotlin mock class for Twilio. Unifying them into a single Go service enabled cross-integration scenario orchestration — something that wasn’t possible when the mocks lived in separate languages and processes.
Docker Compose profiles make this practical for daily development. The principle is “start what you’re not touching” — if you’re working on the API, run just docker for-api and Docker starts PostgreSQL and the mock server. If you’re working on the frontend, just docker for-web starts everything except the frontend. The mock server’s port (8090) matches the old Python mock, so the API’s environment variables just work.
Tip
The mock’s embedded dashboard uses Tailwind CSS via CDN and Alpine.js — no build step, no npm, no bundler. It’s a dev tool that refuses to become a second frontend project. The dashboard polls /control/status every second for live updates, which is fine for a tool that runs on localhost.
Completion and the Audit Trail
An ECO completes when every labeled task is done in Render — completed, approved, or integrated. But “complete” isn’t as simple as it sounds — the system needs to account for late-arriving work.
When the polling service detects that at least one labeled task exists and every labeled task is done, it marks the ECO as COMPLETED and starts a grace period (default: 24 hours). A task counts as done at completed, approved, or integrated — Render advances a finished task along that ladder, so requiring the literal completed once held ECOs open forever. During this period, polling continues. If a new task appears — a field technician cloned the investigation to create follow-on work — the ECO reverts to IN_PROGRESS and the NOC is notified. If a completed task is reopened, same thing.
This is why the ECO label, and not Render’s subsector, is the container. Every investigation, repair, cloned task, and follow-on task carries the same tenant-formatted job ID as an exact General label, and that label-scoped result is the only thing Strike counts. Strike does not ask whether the first task is complete; it asks whether the whole labeled work set has settled. Removing a label removes membership on the next poll; adding one can pull a task back in and reactivate an ECO during grace. The polling service translates Render’s field statuses into NOC-facing ECO state, keeps polling through the grace period after apparent completion, and only treats the ECO as operationally final after that window expires without new or reopened work.
After the grace period expires without new activity, the ECO reaches hard completion. Polling stops permanently. The ECO is finalized. Any changes after this point require manual intervention — the system won’t automatically reactivate a hard-completed ECO.
OPENIN_PROGRESSCOMPLETEDHARD_COMPLETEPolling continuesduring grace periodECO createdField tech dispatchedAll tasks doneLate clone or task reopenedGrace period expires (24h)
What remains after completion is the event stream — and this is where event sourcing pays its largest dividend. Every ECO carries a complete, immutable record of everything that happened:
ECOCreatedEvent — who created it, when, with what geometry and description
RenderIntegrationStatusUpdatedEvent — when the Render task was created, which instance, the project task ID
PagerRun events — every call attempt, every response, the timeline of human acknowledgment
StatusUpdatedEvent — each transition, with timestamps and actor IDs
Task change events — every field update detected by polling, including late clones
This isn’t a log file that rotates away or a database column that gets overwritten. It’s the data model itself. When a regulatory question arises — “who was notified about this outage, and how quickly did the response happen?” — the answer is a query against the event store, not a forensic reconstruction from scattered logs.
The audit trail matters beyond compliance. Post-incident reviews can replay the exact sequence of events to identify bottlenecks — was the delay in pager acknowledgment? In field dispatch? In the completion grace period? The event stream is the single source of truth for “what actually happened.”
Key Takeaway
The event stream as audit trail isn’t a feature bolted onto the system — it’s a consequence of the architecture. When you store state as events, the audit trail is free. When you store state as mutable rows, the audit trail is an additional system you have to build, maintain, and trust.
Putting It All Together
The full ECO lifecycle is a sequence of handoffs between process managers, each responsible for one phase of the workflow:
Time
Event
System Action
ECO Status
T+0
NOC creates ECO
ECOCreatedEvent emitted
OPEN
T+1m
Render integration processor
Creates task in Render, status → TASKED
OPEN
T+1m
Pager processor triggers
Starts pager run, calls Twilio
OPEN
T+5m
Supervisor accepts page
Webhook: ACCEPTED, status → DISPATCHED
OPEN
T+10m
Supervisor assigns tech
Render task → released
IN_PROGRESS
T+30m
Tech arrives, investigates
Task updates detected by polling
IN_PROGRESS
T+35m
Tech clones follow-on task
Late clone detected, new task tracked
IN_PROGRESS
T+2h
Investigation completed
Task → completed
IN_PROGRESS
T+4h
Follow-on completed
All tasks done → grace period starts
COMPLETED
T+28h
Grace period expires
Polling stops, ECO finalized
HARD COMPLETE
Each row in this timeline is backed by an event in the store. The process managers don’t communicate directly — they react to events and emit commands. The Render integration processor doesn’t know about the pager processor. The pager processor doesn’t know about the polling service. They’re connected by the event bus, and the event store is the shared record of what happened.
This is the design that lets a single button press — “Create ECO” — cascade into a multi-hour, multi-system, multi-human workflow, while every participant (human and machine) sees exactly the information they need, exactly when they need it.
An ECO starts as a CreateECOCommand carrying geometry, job type, description, and dual tenant context. The aggregate validates and emits a CreatedEvent.
The Render integration processor creates a task in the OSP's Render tenant, labeling it with the tenant-formatted ECO job ID to group all work for the ECO. Success moves the integration status to TASKED.
The pager process manager resolves contacts by region (PostGIS), calls via Twilio, and tracks state in a PostgreSQL table — not in memory. A background worker handles timeouts.
The polling service watches Render every 60 seconds, using fingerprint-based change detection to minimize noise. It handles late clones and completion grace periods.
Three status layers serve different audiences: event-sourced state for correctness, Render task status for integration tracking, and display status for NOC operators.
The mock server simulates both Render and Twilio with independent Render, Pager, Speed, Fault, and Workflow controls plus an embedded dashboard — enabling full lifecycle testing without external dependencies.
The event stream is the audit trail. Every state change, every pager attempt, every field update is an immutable fact in the event store — regulatory compliance as an architectural consequence.
The grace period defaults to 24 hours. What operational patterns might justify making this shorter or longer? What signals could the system use to set it dynamically?
The pager process manager uses PostgreSQL for state instead of a saga framework. At what scale or complexity would this trade-off start to hurt, and what would migration to a framework look like?
The three-layer status model adds mapping complexity. Could a simpler two-layer model (internal + display) work, or does the Render integration layer earn its existence?
When the Fiber Goes Dark — The domain context that explains why ECOs exist and what OSP emergency response looks like.
The Shape of the System — Mono-repo structure, package boundaries, and the request flow that this cairn builds on.
Events All the Way Down — The event sourcing architecture that makes the ECO lifecycle possible, including the Watermill migration.
Render Networks — Field workforce management platform. External service that OSP contractors use for task management and crew dispatch.
Twilio — Communications API used for pager dispatch. Strike uses Twilio Studio flows for the call sequence and webhooks for status callbacks.