Authentication
Pug uses three auth boundaries. Each boundary maps to specific RPC services — using the wrong credential type returns 401 Unauthenticated or 403 Permission Denied.
Quick decision guide
Are you building a browser or mobile app?
└─ Yes → SDK API key (write-only, safe for client code)
Are you calling from your backend or a trusted server?
└─ Yes → Shared API key or JWT
Are you building a dashboard UI integration?
└─ Yes → JWT (obtained via user sign-in)Request headers
All authenticated requests require a project scope header:
x-project-id: <project-id>Dashboard (JWT)
Browser sessions for growth managers. Obtained via sign-in (email/password, magic link, or Google OAuth).
Authorization: Bearer <jwt>
x-project-id: <project-id>Services: OrgsService, ProjectsService, DashboardsService, OrgEmailProvidersService, CustomersService
JWTs are short-lived and refreshed by the dashboard client automatically. Do not hard-code JWTs in scripts — use the SDK API key or shared key for server automation.
SDK (API key)
Client-side SDKs authenticate with the SDK API key from project settings. Write-only and untrusted — suitable for browsers and mobile apps.
Authorization: Bearer <sdk-api-key>
x-project-id: <project-id>Services: EventsService.BatchCreate, ProfilesSDKService.Identify
Never expose a shared/private API key in client-side code. SDK keys cannot read analytics data.
Shared (API key or JWT)
Server-side or trusted integrations. Accepts either a shared API key or a user JWT.
Authorization: Bearer <shared-api-key-or-jwt>
x-project-id: <project-id>Services: InsightsService, ProfilesService, ActivityService
Use a shared key for cron jobs, internal tools, and backend services. Use JWT when acting on behalf of a logged-in dashboard user.
Connect RPC client example
All APIs use Connect RPC over HTTP/2. Install the client:
npm install @connectrpc/connect @connectrpc/connect-web @bufbuild/protobufSDK auth — send events from Node.js
import { createClient } from '@connectrpc/connect'
import { createConnectTransport } from '@connectrpc/connect-web'
import { EventsService } from './gen/events/v1/events_connect' // generated from proto
const transport = createConnectTransport({
baseUrl: 'https://api.pug.sh',
interceptors: [
next => async req => {
req.header.set('Authorization', `Bearer ${process.env.PUG_SDK_API_KEY}`)
req.header.set('x-project-id', process.env.PUG_PROJECT_ID!)
return next(req)
}
]
})
const client = createClient(EventsService, transport)
await client.batchCreate({
events: [
{
name: 'purchase',
properties: { revenue: 29.99, currency: 'USD' },
timestamp: new Date()
}
]
})Shared auth — run an insight query
import { createClient } from '@connectrpc/connect'
import { createConnectTransport } from '@connectrpc/connect-web'
import { InsightsService } from './gen/insights/v1/insights_connect'
const transport = createConnectTransport({
baseUrl: 'https://api.pug.sh',
interceptors: [
next => async req => {
req.header.set('Authorization', `Bearer ${process.env.PUG_SHARED_API_KEY}`)
req.header.set('x-project-id', process.env.PUG_PROJECT_ID!)
return next(req)
}
]
})
const client = createClient(InsightsService, transport)
const result = await client.query({ /* InsightQuerySpec */ })Generate TypeScript clients from proto with Buf :
buf generateProto definitions: cotton/proto
API key management
| Key type | Where to find | Rotate when |
|---|---|---|
| SDK API key | Settings → API keys | Key leaked in client bundle or public repo |
| Shared API key | Settings → API keys | Employee offboarding, suspected compromise |
Rotation steps:
- Generate a new key in project settings.
- Update your SDK init or server env vars.
- Deploy.
- Revoke the old key.
Security best practices
- SDK keys in client code are expected — they are write-only. Do not treat obscurity as security.
- Shared keys stay on the server — never bundle them in frontend JavaScript, mobile apps, or public repos.
- Use environment variables —
NEXT_PUBLIC_*for SDK keys only; shared keys in server-side env only. - Proxy pattern — if you need server-side logic before ingesting events, call your own API which forwards to Pug with the SDK key.
Error responses
Connect RPC returns structured errors. Common cases:
| Code | Meaning | Typical cause |
|---|---|---|
Unauthenticated | Missing or invalid credential | Wrong key, expired JWT |
PermissionDenied | Valid credential, wrong service | SDK key calling InsightsService |
InvalidArgument | Payload failed protovalidate | Missing required field, wrong type |
NotFound | Resource doesn’t exist | Wrong project ID |
Invalid payloads include field-level details from protovalidate. Fix the named field and retry.
Further reading
- API overview — full service index
- RPC services reference — methods per service
- Self-hosting configuration —
JWT_SECRETand OAuth setup