porcupine Documentation GitHub ↗
Porcupine v0.10.0 View Markdown ↗

Access and interfaces#

The HTTP API, CLI and MCP use the same project grants. The management API reference lists operations and their required permissions.

Technical access#

An operator bootstraps the deployment and creates its first technical access. Operators can enable email accounts or configure an upstream OIDC provider and explicit human-to-project bindings for interactive MCP. Each technical credential has an explicit scope, one or more projects, or the whole instance, and a set of permissions.

OAuth client credentials authorize machine API and MCP access when the operator configures an issuer. Clients exchange an ID and secret through HTTP Basic at /oauth/token; only the resulting short-lived access token goes in Bearer authorization. SQLite stores separate secret and token hashes. Creation returns a client secret once. Logs must not contain raw credentials. An administrator can also issue a static service key for a single audience. HTTP and MCP share operation policies.

Each request checks token expiry, parent revocation, and current grants. Effective authority intersects token scope and issued projects with current permissions and projects. A later expansion of parent grants cannot expand an old token. Client revocation invalidates subsequent uses of its opaque access tokens. Interactive MCP uses rotating refresh tokens and per-consent grants with either login method.

A configured issuer rejects legacy API and MCP Bearer keys by default. Explicit legacy allowance supports migration. Without OAuth configuration, legacy mode remains available for offline use. Public single-project PostHog ingestion keys stay separate, write-only browser credentials and are not OAuth clients.

Huma provides typed management requests, validation, and generated OpenAPI. SQLite stores client credentials, token hashes, and project grants. Explicit permission checks run inside the Go process; there is no policy server or role inheritance.

Instance scope is an explicit administrative grant. A project credential cannot create an instance credential, target another project, or grant permissions outside its own authority. Credential creation must validate both requested scope and permissions. An event payload cannot override the project determined by authentication.

Permission vocabulary#

The accepted permission names are:

Permission Intended authority
instance:admin All instance and project operations, including access revocation. Requires * project scope.
events:read Read events and analytics within scope.
imports:write Create and resume historical uploads within a project, together with events:write.
events:write Ingest events within scope.
events:edit Queue a revision-checked full event correction within scope.
events:delete Queue a revision-checked deletion tombstone within scope.
management:read Inspect permitted project or instance configuration.
management:write Change permitted configuration and manage access without escalation.
identity:write Reserved for future identity administration.
pipelines:read Inspect project pipeline and run metadata, also requiring events:read.
pipelines:write Configure project destinations and retries, also requiring events:read.

Event editing and deletion are implemented through native mutation operations. They require their explicit permissions; ingestion authority alone does not grant either capability. identity:write remains reserved, for future administrative operations. Capturing a supported $identify or $set uses the project's events:write permission. Public capture credentials let callers supply analytics IDs and traits. Application authentication and authorization must use a separate source of authority.

Ingestion compatibility#

Native and Segment inputs accept product events with stable producer message IDs. Public capture uses the project's ingestion key. A server can use an API-audience service key or access token scoped to that project.

Porcupine accepts a tested PostHog ingestion subset, including JSON capture, batches, and bounded gzip transport. The matrix names pinned SDKs and supported settings. It does not claim every endpoint, default SDK behavior, payload encoding, session behavior, or identity operation. Unsupported identity control events are rejected.

Backend capture can use an API-resource token with events:write. It must resolve to one project, either through its single-project grant or a validated public ingestion-key selector already within token scope. The OAuth client ID never becomes the visitor's distinct_id.

Porcupine's own query API serves its native analytics model. HogQL compatibility is not required.

Analytics and MCP#

The analytics API provides SQL schema discovery, observed event and sampled property discovery, and read-only ClickHouse queries. Every request targets one project and requires events:read. Agents write their own SQL. There are no named prebuilt metrics or saved SQL catalogs. Statistical meaning still depends on the event model, especially for daily cookieless identities.

MCP exposes two tools for documentation and API calls. Discovery filters operations by the caller's grants. Calls use the same local API handlers, including SQL validation, body validation, project checks, and anti-escalation rules. Event names, properties, and SQL results are untrusted data for agents.

MCP uses stateless Streamable HTTP and MCP-resource OAuth tokens in Bearer headers. RFC 8414 authorization-server metadata and RFC 9728 protected-resource metadata advertise the machine grant and pinned resource URLs. Tokens for the API resource cannot authenticate MCP. People can approve pre-registered MCP clients with email verification or upstream OIDC. The endpoint rejects cross-origin browser requests and retains the SDK's localhost DNS rebinding protection. Ingestion CORS grants do not apply to MCP.

Restricted SQL#

A parser accepts one supported SELECT or WITH query against events and local CTEs. The exact function allowlist comes from get-event-schema. Sequential CTEs, joins, subqueries, UNION, aggregation, and typed scalar parameters are supported. Unsupported syntax fails closed. Writes, qualified source tables, system metadata, external sources, table functions, FORMAT, SETTINGS, windows, lambdas, CAST, and recursive CTEs are excluded.

The configured ClickHouse account provisions schema and project readers. Client SQL executes as a restricted project-specific account, through an invoker-security view with a project predicate. A physical-table row policy also restricts that reader to the same project. The view applies FINAL for latest live revision selection. Reader settings enforce final=1 and disable prewhere movement for final reads. Column-only physical-table grants exclude private originals and tombstone internals. Clients do not receive those database credentials. Even instance administrators execute SQL through these read-only project readers.

Reader settings fix read-only mode and disable DDL and introspection. They limit execution to five seconds, memory to 256 MiB, scanned rows to five million, read bytes to 1 GiB, and results to 1,000 rows and 1 MiB. The request timeout is ten seconds, with at most two concurrent analytical requests instance-wide. Supplied SQL cannot change these settings. Parameters stay separate from SQL and require exact declared names and safe scalar types.

Public statistics include elapsed time, not physical scan rows or bytes. Delivery status reports only the requested project's pending original and mutation counts and last success. A generic worker error appears only when that project has pending work. Internal ClickHouse error messages and credentials are not returned.

make integration exercises the real ClickHouse boundaries, including a direct physical-table read as the project reader. This complements parser restrictions; SQL parsing alone is not the database isolation boundary.

Operator storage boundary#

S3 tiering is global deployment configuration. Bucket endpoint, AWS credentials, ClickHouse policy, cold volume, and hot duration have no HTTP or MCP management operation. S3 credentials go only to the ClickHouse container. Project credentials and agent clients cannot change those settings. Native cold storage preserves query access; it is separate from export pipelines.

Pipeline boundaries#

Project pipelines deliver webhook events and scheduled S3 results. pipelines:read or pipelines:write alone is insufficient; the caller also needs events:read for the same project. Native storage policy remains operator-only. One-off export configuration remains separate.

Destination credentials are encrypted with an operator key and are never returned by metadata APIs or MCP. Webhook URLs and headers are encrypted too. Workers check the creating principal's current authority. Permission to read events alone does not authorize configuring a new outbound destination.

Webhook transport permits public HTTPS destinations, checks all DNS answers at connection time, dials validated addresses, and rejects redirects and private-network targets. Custom S3 endpoints require an exact origin in the operator allowlist. These are distinct destination controls; allowing an S3 origin does not permit private webhook traffic.

Supported restores pause every destination. A restore cannot reverse a request or object write already accepted elsewhere. Stable delivery identifiers and receiver deduplication remain part of the contract. See the pipeline reference for limits, credential replacement, and retry behavior.

Ingestion under load#

Each process accepts up to 32 concurrent capture requests across ingestion routes, and a separate pool of eight for token exchange and revocation. Excess requests receive HTTP 503 with Retry-After: 1 before body decoding or SQLite access. Retry with the original event UUID or producer message ID. The general request limit is 1 MiB before and after decompression and 1,000 events per batch. Segment also enforces a 32 KiB message limit and a 500 KiB batch limit. These limits bound concurrent work in the process. Configure rate limits at your reverse proxy if you also need quotas per caller or IP.

For records used in billing or trusted reporting, create a server_only project and use an OAuth writer scoped to that project. A mixed project's public key allows any holder to submit any supported event name. Keep that distinction when interpreting SQL results. See OAuth ingestion.