The green checkmark hides a distributed system
Sending a text message looks like one action from an application screen. Underneath, several components take turns owning the work. A backend accepts intent. A queue holds it. A device claims it. Android allows or interrupts execution. A radio hands the message to a carrier. A delivery report may return later, out of order, or not at all.
Flattening that sequence into sent or failed produces a tidy interface and a weak operating model. When a message is delayed, duplicated, or uncertain, the operator needs to know which boundary was crossed and which component can act next.
Pigeon turns an Android phone with a SIM card into programmable messaging infrastructure. It uses equipment under the operator's control rather than hiding the carrier boundary behind an abstract status. That ownership makes more of the lifecycle visible, but it does not make the lifecycle simple.
Delivery is not one event. It is a sequence of claims made by components with limited knowledge.
The phone and backend have different jobs
Pigeon is split between a Kotlin Android application and a FastAPI backend with PostgreSQL. The Android app runs a foreground service and acts as the carrier-network endpoint. The backend owns the queue, message history, device registry, and API surface. The backend never touches the carrier network. The phone does not hold application state. A polling loop connects them.
That separation creates a useful boundary. The backend can decide that work is eligible and assign it to a device. Only the phone can report that it has begun sending through the radio. Neither fact means the carrier has confirmed delivery. Each component records what it knows without borrowing certainty from another component.
The full lifecycle is explicit:
queued: Backend accepted work that has not been assigned.assigned: A device owns the next attempt.sending: Device began carrier-facing work.sent: Device handed the message to the radio.delivered: A carrier delivery report returned.failed: A component recorded a terminal failure for the attempt.received: Device recorded an inbound message.
These states are not labels added for reporting. They define ownership. The boundary between assigned and sending is especially important because it contains many failures that look identical from outside. A device may be offline, Android may interrupt its process, authentication may be stale, or the device may have completed work before the backend received the update.
Android interruption is normal operation
A phone is not a small server with a dependable background process. Android limits background execution for good reasons. Doze, system scheduling, and aggressive battery management by some device vendors can interrupt long-running work. A messaging service has to expect that behavior instead of classifying every interruption as an exceptional outage.
Pigeon polls rather than holding a persistent socket open. A missed polling cycle can recover at the next cycle. A dropped long-lived connection needs supervision that the operating system may not continue to provide. Polling accepts some delay in exchange for a recovery path that fits the environment.
This is an operator-centered trade. It does not pretend the handset is always available. It gives the system repeated opportunities to reconcile queue state with device state. The foreground service makes ongoing work visible to Android and the user, while explicit message states show whether the device has merely accepted ownership or has reached the carrier-facing step.
Retry is its own state machine
The hardest failure is not always a clean rejection. Consider a device that goes offline while holding assigned work. It later returns with a token that rotated during the interruption. The device still knows the message identity, but its credential is stale. If retry logic treats authentication as a generic exception, two bad outcomes become possible. The message may be dropped because refresh failed at the wrong point, or it may be replayed after already reaching the radio.
Pigeon treats retry as its own state machine. Token refresh and message identity are handled separately. Authentication answers whether the device may communicate with the backend. Message identity answers whether a specific unit of work is new, in progress, or already recorded. A stale credential should not turn into a duplicate SMS.
This separation also improves diagnosis. An operator can distinguish a device that cannot authenticate from a message that cannot advance. The response differs. Credential recovery belongs to device access. Message reconciliation belongs to lifecycle state. Combining them in one exception handler would make both harder to reason about.
Sent and delivered are different claims
Carrier delivery reports are best-effort. They can arrive late. They can arrive out of order. Sometimes they do not arrive. Pigeon therefore uses sent to mean the device handed a message to the radio. It uses delivered only when a report comes back.
That wording is intentionally narrow. sent does not claim that the recipient's handset displayed a message. delivered does not create a universal guarantee beyond the report that was received. The system preserves the strongest fact available without inflating it.
Collapsing both states into one green checkmark would save a column while erasing the most useful distinction during support and debugging. If a message remains sent, the operator knows the carrier-facing handoff occurred but confirmation did not return. If it remains assigned, the investigation stays between backend and device. If it reached sending and failed, the carrier-facing attempt has a different history.
Current scale keeps the claim bounded
Pigeon is a personal-scale system. Throughput is bounded by one SIM and handset. It is not distributed through an app store. Outbound SMS works through the lifecycle, while outbound MMS remains future work. The system does not guarantee delivery because the carrier does not offer that guarantee.
Owned infrastructure provides control over the device, application, queue, and state transitions. It does not provide control over Android scheduling, radio conditions, carrier behavior, or the recipient's handset. Good operations begin by marking those boundaries accurately.
Pigeon's central lesson is modest: lifecycle precision is more valuable than status optimism. A queue item, device assignment, radio handoff, and delivery report are different facts. Keeping them separate lets interruption recover, lets stale authentication stay separate from message identity, and gives an operator a specific next question when work stops moving.