Peer Identity
Every user who messages Polpo is assigned a peer identity:| Field | Description |
|---|---|
id | Canonical ID, format {channel}:{externalId} (e.g. telegram:123456) |
channel | Source channel: telegram, whatsapp, slack, discord, webchat |
externalId | Platform-specific user ID |
displayName | User’s display name (from the platform) |
firstSeenAt | When this peer was first observed |
lastSeenAt | Most recent message timestamp |
linkedTo | Optional canonical ID for cross-channel identity linking |
.polpo/peers/peers.json.
DM Policy
The DM policy controls who can message Polpo through a channel. It’s set per-channel via thegateway.dmPolicy field:
| Policy | Behavior | Use Case |
|---|---|---|
open | Anyone can message | Personal use, development |
allowlist | Only pre-authorized peers | Team with known members |
pairing | Unknown users get a pairing code to request access | Secure team onboarding |
disabled | All inbound messages silently ignored | Notification-only channel |
Static Allowlist
Thegateway.allowFrom array pre-authorizes specific users:
"123456789") and full peer IDs ("telegram:987654321") are accepted. Use ["*"] to allow everyone (equivalent to "dmPolicy": "open").
Dynamic Allowlist
Peers can be added or removed at runtime via the REST API:.polpo/peers/allowlist.json.
Pairing
Pairing is a secure onboarding flow for new users. It works like device pairing — the user gets a code, an admin approves it.Flow
Pairing Rules
| Rule | Value |
|---|---|
| Code format | 6-character uppercase alphanumeric |
| Expiry | 1 hour |
| Max pending per channel | 3 (oldest is evicted when exceeded) |
| Deduplication | Same peer gets the same code if they message again |
| Case sensitivity | Codes are case-insensitive for resolution |
Approving Pairing Codes
Codes can be approved through any interface:| Method | How |
|---|---|
| Telegram | /pair ABC123 (authorized peers only) |
| REST API | POST /peers/pair with {"code": "ABC123"} |
| TUI | Via the approvals interface |
Session Isolation
Each peer gets their own conversation session. The PeerStore maintains apeerId → sessionId mapping:
- New session — created on the peer’s first message (or after idle timeout)
- Session resume — subsequent messages continue the same session
- Idle timeout — after
sessionIdleMinutes(default: 60), the next message starts a fresh session - Manual reset —
/newcommand clears the session
.polpo/sessions/). The peer-to-session mapping persists in .polpo/peers/sessions.json.
Identity Linking
A single person may use multiple channels (e.g. Telegram and WhatsApp). Identity linking lets you connect these identities so they share the same session and context:whatsapp:39123456resolves to canonical IDtelegram:123456- Both identities share the same session
- The linked peer inherits the canonical peer’s allowlist status
Presence
Presence tracks which peers are currently active. It’s ephemeral — stored in memory only, not persisted to disk.| Field | Description |
|---|---|
peerId | Peer identifier |
displayName | Display name |
channel | Source channel |
lastActivityAt | Timestamp of last message |
activity | Current activity: idle, chatting, approving |
TTL
Presence entries expire after 5 minutes of inactivity. ThegetPresence() call automatically prunes stale entries.
API
/status slash command and the peer:presence SSE event.
Storage
All peer data persists as JSON files in.polpo/peers/:
| File | Content |
|---|---|
peers.json | Known peer identities |
allowlist.json | Dynamic allowlist |
pairing.json | Pending and resolved pairing requests |
sessions.json | Peer-to-session mapping |
Events
| Event | Payload | When |
|---|---|---|
peer:paired | { peer, channel } | Pairing code approved |
peer:message | { peerId, channel, text, sessionId } | Peer sent a message |
peer:blocked | { peerId, channel, reason } | Message blocked by policy |
peer:presence | { peerId, channel, status } | Online/offline change |