---
title: "Authentication"
description: "Public keys, private keys and dashboard JWTs — which credential reaches which service, and what each error code means."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.pug.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# 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

{/* Dashboard labels mirror the dashboard app at ../app — re-verify there if the UI is renamed. The "API Keys" screen is src/pages/routegen/settings/api-keys/index.page.tsx, listed in settings-layout.tsx; the project Overview's setup view (src/pages/routegen/overview/setup-mode.tsx) surfaces the public key only; the "Live"/"Events" nav labels are in src/components/layout/sidebar.tsx; the /p/:projectId route is in src/pages/routes.ts. */}

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:

```ts
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](/sdks?platform=node)** wraps these reads — construct a client with your private key and call `pug.insights`, `pug.profiles`, or `pug.activity`:

```ts
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):

```bash
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](/api/insights) 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](https://connectrpc.com/) 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](/sdks). To generate a typed client from the protobuf definitions, see the [API overview](/api).

## 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 with `pug.` (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](/get-started/concepts) — the auth modes in context
- [API overview](/api) — service index and conventions
- [RPC services](/reference/rpc-services) — every service and its auth mode
- [Self-hosting configuration](/self-hosting/configuration) — `PUG_JWT_SECRET_KEY` and CORS

Source: https://docs.pug.sh/get-started/authentication/index.mdx
