Architecture & Security
A conceptual overview of how Pharlo is built and how it protects your accounts and data — for developers and technical evaluators deciding whether to integrate.
This page describes the system at a conceptual level. It intentionally omits internal hostnames, secret formats, and implementation details that would not help an integrator.
High-level
Pharlo is a content posting platform with two integration surfaces over one backend:
- REST API — for backend and automation use (Symfony 7, PHP 8.3).
- MCP server — for AI agents, exposed at
https://pharlo.io/_mcpover Streamable HTTP.
Both surfaces resolve to the same domain model (assignments, connections, platforms, stats, webhooks) and share authorization, billing, and platform-delivery logic.
Authentication & authorization
| Surface | Auth |
|---|---|
| REST API | API key as a Bearer token (ds_live_...). |
MCP server (/_mcp) | OAuth 2.0 Bearer (authorization code + PKCE, dynamic client registration). |
| Web app / console | Session auth (JWT + Clerk). |
- MCP OAuth issues short-lived access tokens (~1 hour) and rotating refresh tokens (~30 days). The
ds_live_...key is entered only on the consent screen; it is not sent as a header to/_mcp. - Scopes restrict what a token can do (e.g.
assignments:read,assignments:write,connections:read,stats:read). Clients request only the scopes they need. - Discovery is standards-based via
/.well-known/oauth-authorization-serverand/.well-known/oauth-protected-resource.
Platform connections
- Connecting a channel runs an OAuth consent flow with the platform (Google/YouTube, Facebook).
- Platform tokens are stored server-side and refreshed automatically; they are never returned to clients. When a token can no longer be refreshed, the connection is flagged and the user re-authorizes.
Multi-tenancy
- Work is scoped to a workspace/organization, resolved per request (e.g. from an organization header).
- A request only ever sees the connections, assignments, and stats of its own workspace.
Publishing pipeline
- A publish request becomes an assignment that moves through a lifecycle
(
pending → uploading → uploaded → processing → published, orfailed/cancelled). - Large local media uses resumable upload sessions instead of inline transfer; publishing happens once the session completes.
- Delivery to platforms runs asynchronously through a message queue, with retry for failed assignments (without recreating them).
Webhooks
- Outbound webhooks notify your endpoint of events (e.g. publish results).
- Each delivery is signed with HMAC-SHA256 over the raw JSON body and sent in the
X-Webhook-Signature-256header assha256=<hex>. Verify it with a constant-time comparison — see the webhook debugging playbook and the examples in the repo.
Analytics
- Stats are collected through an analytics pipeline (ClickHouse) and exposed via the stats tools and endpoints. Live-fetch calls read from the platform APIs; cheaper calls serve cached snapshots.
Billing & metering
- Usage is metered in credits. Read-only and discovery calls are cheap or free; writes and live fetches cost more. MCP calls carry a surcharge over equivalent REST calls.
- When credits run out, the API returns
insufficient_credits(HTTP 402); a configured spending cap returnsuser_spending_limit_exceeded(HTTP 402). See the error catalog (opens in a new tab).
Reliability & observability
- Asynchronous delivery, automatic token refresh, and idempotency support (
idempotencyKey) reduce duplicate publishes and transient failures. - The platform is monitored with standard observability tooling (metrics, logs, alerting).
Data handling
- Platform credentials are stored server-side and never exposed to API/MCP clients.
- Agents and integrators should likewise never log or echo API keys or OAuth tokens — see Agent Rules.