Skip to main content
When users interact with Polpo through messaging channels, the PeerStore manages their identities — who they are, whether they’re authorized, which session belongs to them, and whether they’re currently active.

Peer Identity

Every user who messages Polpo is assigned a peer identity:
FieldDescription
idCanonical ID, format {channel}:{externalId} (e.g. telegram:123456)
channelSource channel: telegram, whatsapp, slack, discord, webchat
externalIdPlatform-specific user ID
displayNameUser’s display name (from the platform)
firstSeenAtWhen this peer was first observed
lastSeenAtMost recent message timestamp
linkedToOptional canonical ID for cross-channel identity linking
Identities are created automatically on first message and updated on every subsequent message. They persist in .polpo/peers/peers.json.

DM Policy

The DM policy controls who can message Polpo through a channel. It’s set per-channel via the gateway.dmPolicy field:
PolicyBehaviorUse Case
openAnyone can messagePersonal use, development
allowlistOnly pre-authorized peersTeam with known members
pairingUnknown users get a pairing code to request accessSecure team onboarding
disabledAll inbound messages silently ignoredNotification-only channel

Static Allowlist

The gateway.allowFrom array pre-authorizes specific users:
{
  "gateway": {
    "enableInbound": true,
    "dmPolicy": "allowlist",
    "allowFrom": ["123456789", "telegram:987654321"]
  }
}
Both raw external IDs ("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:
# Add to allowlist
curl -X POST /api/v1/projects/:id/peers/allowlist \
  -d '{"peerId": "telegram:123456"}'

# Remove from allowlist
curl -X DELETE /api/v1/projects/:id/peers/allowlist/telegram:123456
The dynamic allowlist persists in .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 flow: unknown user messages bot, receives a code, admin approves via /pair, user can now chat freely

Pairing Rules

RuleValue
Code format6-character uppercase alphanumeric
Expiry1 hour
Max pending per channel3 (oldest is evicted when exceeded)
DeduplicationSame peer gets the same code if they message again
Case sensitivityCodes are case-insensitive for resolution

Approving Pairing Codes

Codes can be approved through any interface:
MethodHow
Telegram/pair ABC123 (authorized peers only)
REST APIPOST /peers/pair with {"code": "ABC123"}
TUIVia the approvals interface
When a code is approved, the peer is automatically added to the dynamic allowlist.

Session Isolation

Each peer gets their own conversation session. The PeerStore maintains a peerId → 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/new command clears the session
Sessions are stored in the standard SessionStore (.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:
curl -X POST /api/v1/projects/:id/peers/link \
  -d '{"peerId": "whatsapp:39123456", "linkedTo": "telegram:123456"}'
After linking:
  • whatsapp:39123456 resolves to canonical ID telegram:123456
  • Both identities share the same session
  • The linked peer inherits the canonical peer’s allowlist status
Linking is one level deep (no chains) to avoid cycles.

Presence

Presence tracks which peers are currently active. It’s ephemeral — stored in memory only, not persisted to disk.
FieldDescription
peerIdPeer identifier
displayNameDisplay name
channelSource channel
lastActivityAtTimestamp of last message
activityCurrent activity: idle, chatting, approving

TTL

Presence entries expire after 5 minutes of inactivity. The getPresence() call automatically prunes stale entries.

API

# Get currently active peers
curl /api/v1/projects/:id/peers/presence
Presence data is also available through the /status slash command and the peer:presence SSE event.

Storage

All peer data persists as JSON files in .polpo/peers/:
FileContent
peers.jsonKnown peer identities
allowlist.jsonDynamic allowlist
pairing.jsonPending and resolved pairing requests
sessions.jsonPeer-to-session mapping
Presence is in-memory only — it resets on restart.

Events

EventPayloadWhen
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