Authentication
Pug has three auth boundaries. Each maps to a set of RPC services — using the wrong credential returns Unauthenticated or PermissionDenied.
The three credentials
| Credential | Format | Used by | Header | Reaches |
|---|---|---|---|---|
| Public key | pub_… | Browser / app SDK | x-api-key | sdk.events.v1.EventsService, sdk.profiles.v1.ProfilesSDKService |
| Private key | prv_… | Your server | x-api-key | shared.* — insights, profiles, activity |
| JWT | session token | Dashboard | Authorization: Bearer | dashboard.* (and shared.*) |
The project is resolved from the API key, so SDK and server requests don’t send a project ID. Dashboard JWT requests optionally include an x-project-id header to scope to a project.
Where keys live
Keys live under Settings → API Keys. A project holds any number of them, so you can issue one per app or service and revoke it on its own. Your project Overview also shows the public key next to the SDK setup snippets.
- Public key is displayed in plain text — it’s safe in client code. It can write events and identify users, but cannot read analytics.
- Private key is shown once, when you create it — copy it then. Pug stores only a digest of the key, so it cannot be revealed afterwards; the list shows a mask (
prv_…3f9c) to tell your private keys apart. Treat it like a password. It can read insights, profiles, and activity.
There is no rotate button. To replace a key, create its replacement, update your SDK apiKey / server env var, deploy, then revoke the old one.
Public key — client SDKs
The Web and Flutter SDKs take the public key as apiKey and send it as x-api-key on every request:
import { init } from '@pug-sh/browser'
init('YOUR_PROJECT_ID', {
apiKey: 'pub_YOUR_PUBLIC_KEY'
})The public key is write-scoped: BatchCreate (events) and Identify (profiles). It cannot reach the shared.* read APIs, so it’s safe to ship in a browser bundle or mobile binary.
Private key — your servers
Use the private key from trusted server-side code to read analytics. The Node SDK wraps these reads — construct a client with your private key and call pug.insights, pug.profiles, or pug.activity:
import { Pug } from '@pug-sh/node'
const pug = new Pug({ apiKey: process.env.PUG_PRIVATE_KEY! })
const result = await pug.insights.query({ /* spec, timeRange, granularity */ })From any other language, send the private key as x-api-key against the shared.* services (which accept a private key or a dashboard JWT):
curl -X POST https://api.pugs.dev/shared.insights.v1.InsightsService/Query \
-H "x-api-key: prv_YOUR_PRIVATE_KEY" \
-H "Connect-Protocol-Version: 1" \
-H "Content-Type: application/json" \
-d '{ /* InsightQuerySpec */ }'Replace the body with a real InsightQuerySpec — see Insights API for the field-by-field shape and a complete example.
Never ship a private key to a browser, mobile app, or public repo.
JWT — the dashboard
The dashboard obtains a short-lived JWT when a user signs in, and sends it as Authorization: Bearer <jwt> (plus x-project-id to scope to a project). JWTs reach the dashboard.* management services and the shared.* read services. They’re refreshed automatically by the dashboard client — don’t hard-code them in scripts; use a private key for automation instead.
Connect RPC
All APIs speak Connect RPC over HTTP/2 and accept the Connect, gRPC, and gRPC-Web protocols. The request path is /<package>.<Service>/<Method>, e.g. /sdk.events.v1.EventsService/BatchCreate. For JSON requests, include Connect-Protocol-Version: 1.
To send events without an SDK, see Integrate over HTTP. To generate a typed client from the protobuf definitions, see the API overview.
Error responses
Connect returns structured errors:
| Code | Meaning | Typical cause |
|---|---|---|
Unauthenticated | Missing or invalid credential | Wrong key, missing x-api-key |
PermissionDenied | Valid credential, wrong scope | Public key calling a shared.* read API |
InvalidArgument | Payload failed validation | A rejected field — see below |
NotFound | Resource doesn’t exist | Wrong project or profile ID |
Common InvalidArgument causes:
- A required field is missing.
- A custom property key is prefixed with
$(reserved for auto-properties). kind(the event name) starts withpug.(a reserved prefix).
Responses include field-level details from server-side validation — fix the named field and retry.
Advanced — proxying
If you need server-side logic before events reach Pug (enrichment, filtering, auth of your own), run your own endpoint that forwards requests to Pug with the appropriate key. This keeps the credential on your backend instead of in the client.
Further reading
- Core concepts — the auth modes in context
- API overview — service index and conventions
- RPC services — every service and its auth mode
- Self-hosting configuration —
PUG_JWT_SECRET_KEYand CORS