Trust boundary
Most of Ppoppo's design choices follow from one boundary and one principle.
1st-party vs 3rd-party
Ppoppo operates some applications itself (its own chat clients). You — an external developer — build your own application on top of the platform. The line between the two is a trust boundary, not an org chart: a 1st-party app is one Ppoppo runs; a 3rd-party app is one Ppoppo authenticates but does not control. Everything in these docs is the 3rd-party surface.
Authentication is not identity verification
PAS proves that a user controls a Ppoppo account. It does not verify who they are in the real world. If your service needs identity verification (KYC), you must implement it yourself — Ppoppo authentication cannot stand in for it.
Why PKCE-only, with no client_secret for login
For the login flow Ppoppo supports exactly one grant — Authorization Code with PKCE S256 — and issues no client_secret:
| Aspect | client_secret | PKCE S256 |
|---|---|---|
| Security | A leaked secret compromises every login | Per-request cryptographic verification |
| Operations | Requires secure storage and rotation | Nothing to store or rotate |
| Client types | Safe only for server apps | Safe for web, mobile, and CLI alike |
There is no implicit flow and no password grant — PKCE gives equivalent or better security without the burden of secret management. (The External API is different: it authenticates with a client_credentials secret, because it's a server-to-server call.)
Why gRPC for the External API
| Aspect | REST (JSON / HTTP) | gRPC |
|---|---|---|
| Types | Manual schema validation | Protocol Buffers enforce types at compile time |
| Streaming | Needs WebSocket or SSE on the side | Native server streaming — replaces webhooks, no public endpoint to expose |
| Clients | Hand-written HTTP wrappers | Auto-generated, strongly typed |
| Contract | An OpenAPI spec that tends to drift | .proto files are the source of truth |
This is what lets the pcs-external crate give you a compile-time-safe send path, and lets event streaming replace webhooks without you running an inbound endpoint.
Who owns which data
PAS owns the user's mutable data — most importantly their email. Cache only the immutable pair ppnum_id + ppnum; fetch email from UserInfo when you actually need it, so you never serve a stale address. See The ppnum model for the identifiers.