Skip to content

Architecture Detail

How the pieces connect, for contributors and anyone vetting the internals. See Architecture for the high-level comparison of deployment modes.

Component layers

graph TB
  subgraph adapters ["Chat Adapters"]
    A1[Telegram]
    A2[Matrix]
    A3[Signal]
    A4[IRC]
    A5[XMPP]
    A6[Mastodon]
    A7[Email]
    A8[Mattermost]
    A9[Discord]
    A10[Apprise]
  end

  subgraph daemon ["Daemon"]
    CMD[Command parser]
    ROSTER[Roster + quotas]
    RENDER[Message renderer]
  end

  subgraph hooks ["Hook Layer"]
    PH["provider-{tor,i2p,rns,web}.sh"]
    TH["teardown-{tor,i2p,rns,web}.sh"]
    SHIM[plp-runtime.sh]
  end

  subgraph transports ["Transport Relays"]
    TOR[tor-party-line]
    I2P[i2p-party-line]
    RNS[reticulum-party-line]
  end

  adapters --> CMD
  CMD --> ROSTER
  ROSTER --> PH
  PH --> SHIM
  SHIM --> transports
  transports -- address JSON --> PH
  PH -- address JSON --> RENDER
  RENDER --> adapters
  ROSTER --> TH
  TH --> SHIM

Message flow

A room open follows this sequence:

sequenceDiagram
  participant Sub as Subscriber
  participant Adapter as Chat Adapter
  participant Daemon as partylinepagerd
  participant Hook as provider hook
  participant Shim as plp-runtime.sh
  participant Relay as Transport relay

  Sub->>Adapter: "tor" / "web" / "i2p" / "rns"
  Adapter->>Daemon: parsed command
  Daemon->>Daemon: validate, check quota/tier
  Daemon->>Daemon: generate 256-bit secret (CSPRNG)
  Daemon->>Hook: call hook, secret on stdin
  Hook->>Shim: up <dir> <service>
  alt Standard (compose)
    Shim->>Relay: docker compose up -d
  else Full (direct)
    Shim->>Relay: start process, write PID file
  end
  Relay-->>Hook: readiness signal + address
  Hook-->>Daemon: {"address":"...","ttl_secs":N}
  Daemon->>Daemon: validate address format
  Daemon->>Daemon: render per-recipient (timezone, format)
  Daemon->>Adapter: broadcast message
  Adapter->>Sub: delivered (HTML / Markdown / plain)

Runtime shim branching

plp-runtime.sh is the single dispatch point for all transport lifecycle operations. Every provider hook calls it instead of docker compose directly.

flowchart TD
  HOOK[Provider hook] --> SHIM{plp-runtime.sh}
  SHIM --> CHECK{PARTYLINEPAGER_RUNTIME?}

  CHECK -- compose --> COMPOSE_UP["docker compose up -d"]
  CHECK -- compose --> COMPOSE_DOWN["docker compose down"]
  CHECK -- compose --> COMPOSE_EXEC["docker compose exec"]
  CHECK -- compose --> COMPOSE_LOGS["docker compose logs"]

  CHECK -- direct --> PID_START["Start process, write PID"]
  CHECK -- direct --> PID_STOP["Read PID, collect tree, kill"]
  CHECK -- direct --> DIRECT_EXEC["Run command in-process"]
  CHECK -- direct --> DIRECT_LOGS["Tail log file"]

In compose mode, PLP_COMPOSE_ARGS carries extra flags (override files, profiles). In direct mode, the shim manages processes through PID files under $PLP_PID_DIR and collects child process trees up to 5 levels deep for clean shutdown.

State and configuration

No database. The daemon reads and writes flat files:

File Purpose
config/policy.toml Tiers, providers, quotas, room TTL
config/adapters.toml Chat network credentials
config/state/subscribers.json Roster: addresses, tiers, notes
config/state/pending.json Pending signup confirmations
config/state/room.json Currently live room (if any)
config/state/runtime.json Daemon PID, startup timestamp
.env UIDs, compose file list, runtime overrides

All state files are re-read on each relevant operation. No in-memory cache outlives a single request cycle.