PushNotifiMe
Human-in-the-loop for AI agents — on your phone. Your agent calls pushnotifi_request_ack, you tap Approve / Deny / Yes / No (or type a reply) on the PushNotifi mobile app, the agent's pushnotifi_await_ack returns your answer and the workflow continues.
pushnotifi
MCP server: pushnotifi
{
"command": "node",
"args": [
"./mcp/dist/server.js"
],
"env": {
"PUSHNOTIFI_USER_KEY": "${PUSHNOTIFI_USER_KEY}",
"PUSHNOTIFI_GROUP_KEY": "${PUSHNOTIFI_GROUP_KEY}",
"PUSHNOTIFI_APPLICATION_KEY": "${PUSHNOTIFI_APPLICATION_KEY}",
"PUSHNOTIFI_API_BASE_URL": "${PUSHNOTIFI_API_BASE_URL}",
"PUSHNOTIFI_RATE_LIMIT_PER_MINUTE": "${PUSHNOTIFI_RATE_LIMIT_PER_MINUTE}"
}
}pushnotifi-recipes
Opinionated PushNotifi recipes for common operational alert patterns — failed cron jobs, webhook signature mismatches, DB migration failures, external-API error-rate thresholds, and shell/CI shortcuts via inbound webhook tokens. Use when the user describes one of these patterns or asks for a "monitor X" / "alert me when Y" template.
# PushNotifi recipes
Each recipe is small, single-purpose, and uses only `pushnotifime` (Node) or stdlib HTTP. Pick one. Do not blend recipes; the alert semantics are different.
All recipes assume the `pushnotifi-secrets` rule is in effect and read from `PUSHNOTIFI_USER_KEY`, `PUSHNOTIFI_GROUP_KEY`, and (where noted) `PUSHNOTIFI_WEBHOOK_TOKEN`.
## Recipe 1 — Failed cron job
Use when: a scheduled task may fail silently and you currently have no visibility.
```ts
import { PushNotifiMe } from "pushnotifime";
const pn = new PushNotifiMe(process.env.PUSHNOTIFI_USER_KEY!);
export async function runCron(jobName: string, runId: string, work: () => Promise<void>) {
const start = Date.now();
try {
await work();
} catch (err) {
const ms = Date.now() - start;
await pn.send({
type: "group",
send_to_key: process.env.PUSHNOTIFI_GROUP_KEY!,
title: `Cron failed: ${jobName}`,
message: `${(err as Error).message} (after ${ms}ms)`,
priority: 1,
idempotency_key: `cron:${jobName}:${runId}`,
});
throw err;
}
}
```
Invariants:
- `runId` must be a stable per-run identifier (e.g. ISO timestamp truncated to the schedule grain — `2026-05-10T03:00:00Z` for an hourly job). Never `Date.now()`.
- The function re-throws so the scheduler sees the failure too.
## Recipe 2 — Webhook signature mismatch
See the `webhook-resilience` rule. Signature failures are a security event, not noise. Always `priority: 1`. Idempotency key derived from the offending signature header so repeated attacks deduplicate.
## Recipe 3 — DB migration failure
Use when: a migration step is irreversible-ish and you need to know immediately if it broke.
```ts
import { PushNotifiMe } from "pushnotifime";
const pn = new PushNotifiMe(process.env.PUSHNOTIFI_USER_KEY!);
export async function runMigration(name: string, version: string, apply: () => Promise<void>) {
try {
await apply();
await pn.send({
type: "group",
send_to_key: process.env.PUSHNOTIFI_GROUP_KEY!,
title: `Migration applied: ${name}`,
message: `version ${version} ok`,
priority: -1,
idempotency_key: `mig:ok:${name}:${version}`,
});
} catch (err) {
await pn.send({
type: "group",
send_to_key: process.env.PUSHNOTIFI_GROUP_KEY!,
title: `Migration FAILED: ${name}`,
message: `version ${version}: ${(err as Error).message}`,
priority: 2,
idempotency_key: `mig:fail:${name}:${version}`,
});
throw err;
}
}
```
Note: success uses `priority: -1` (low) so it lands quietly; failure uses `priority: 2` (max) so it interrupts.
## Recipe 4 — External API error-rate threshold
Use when: a downstream API (Stripe, OpenAI, Shopify) flakes occasionally and per-request alerts would page you 1000 times/hour.
The pattern is a **counter + window**, not per-request. One push per window per error class.
```ts
import { PushNotifiMe } from "pushnotifime";
const pn = new PushNotifiMe(process.env.PUSHNOTIFI_USER_KEY!);
const WINDOW_MS = 5 * 60_000;
const THRESHOLD = 10;
const counters = new Map<string, { count: number; windowStart: number; alerted: boolean }>();
export async function trackApiError(api: string, errCode: string) {
const key = `${api}:${errCode}`;
const now = Date.now();
const c = counters.get(key);
if (!c || now - c.windowStart > WINDOW_MS) {
counters.set(key, { count: 1, windowStart: now, alerted: false });
return;
}
c.count += 1;
if (c.count >= THRESHOLD && !c.alerted) {
c.alerted = true;
await pn.send({
type: "group",
send_to_key: process.env.PUSHNOTIFI_GROUP_KEY!,
title: `${api} error-rate threshold`,
message: `${c.count} ${errCode} errors in ${Math.round((now - c.windowStart) / 1000)}s`,
priority: 1,
idempotency_key: `rate:${key}:${c.windowStart}`,
});
}
}
```
Invariants:
- `idempotency_key` includes `windowStart` so each window can alert once even if the process restarts and re-evaluates.
- Counters live in-process. For multi-instance deploys, replace the `Map` with Redis or skip this recipe.
## Recipe 5 — Shell / CI inbound-webhook token
Use when: you want a one-line `curl` from a shell script, GitHub Action, or Docker entrypoint without plumbing the API key.
Each user generates their own inbound-webhook token from the dashboard. Treat the token as a secret — it is per-user, not shared.
```bash
# .github/workflows/deploy.yml — notify on failure
- name: Notify on failure
if: failure()
env:
PUSHNOTIFI_WEBHOOK_TOKEN: ${{ secrets.PUSHNOTIFI_WEBHOOK_TOKEN }}
run: |
curl -fsS -X POST \
"https://api.pushnotifi.me/api/v1/webhooks/incoming/$PUSHNOTIFI_WEBHOOK_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"title\":\"Deploy failed\",\"message\":\"${GITHUB_REPOSITORY} run ${GITHUB_RUN_ID}\"}"
```
```bash
# scripts/notify.sh — generic CLI helper
set -euo pipefail
: "${PUSHNOTIFI_WEBHOOK_TOKEN:?must be set}"
title="${1:?title required}"
message="${2:?message required}"
curl -fsS -X POST \
"https://api.pushnotifi.me/api/v1/webhooks/incoming/${PUSHNOTIFI_WEBHOOK_TOKEN}" \
-H "Content-Type: application/json" \
-d "$(jq -nc --arg t "$title" --arg m "$message" '{title:$t, message:$m}')"
```
Invariants:
- The token is never echoed, logged, or interpolated into a URL stored in a log file. `set -x` should not be on.
- `set -euo pipefail` is required — silent shell failure defeats the recipe's purpose.
## When *not* to send a notification
Refuse to scaffold an alert when:
- The error is recoverable in the same call (the agent should retry, not page).
- The handler is a high-frequency hot path (>1/s) without a counter+window wrapper. Recipe 4 is mandatory in that case.
- The user is asking for end-user product notifications. PushNotifi is operational; recommend a different channel.pushnotifi-scaffold
Generate PushNotifi.me notification calls in Node, Python, Go, shell/curl, Next.js, or Express. Use when the user asks to "send a push", "notify", "alert", or "wire up PushNotifi". Encodes the canonical SDK contract, default-application semantics, idempotency, and typed error handling.
# PushNotifi scaffold
## When to use
- The user asks to add a notification, alert, or push to existing code.
- The user wants the PushNotifi SDK or REST API integrated into a new project.
- An agent needs to inject an alert into a try/catch, cron job, or webhook handler.
Do **not** use this skill for:
- End-user product notifications inside a consumer app UI (use a transactional-email or in-app channel instead — PushNotifi is operational).
- Bulk fan-out marketing pushes.
## Canonical contract
Auth: header `X-API-Key: <PUSHNOTIFI_USER_KEY>`.
Base URL: `https://api.pushnotifi.me` (override with `PUSHNOTIFI_API_BASE_URL`).
Send endpoint: `POST /api/v1/message`.
Request body fields (mirrors the dashboard / OpenAPI schema):
| Field | Required | Notes |
| ----------------- | -------- | ----- |
| `type` | yes | `"user"` or `"group"` |
| `send_to_key` | yes | `u…` (user) or `g…` (group), 32 chars |
| `message` | yes | Plain text body |
| `title` | no | Short identifier |
| `priority` | no | `-2 | -1 | 0 | 1 | 2`; default `0` |
| `application_key` | no | Omit to use account default application |
| `idempotency_key` | recommended | Stable string per logical event; required for retries to be safe |
| `url`, `url_title`| no | Deep-link tap target |
| `ttl` | no | Seconds before drop |
| `attachment_base64`, `attachment_type` | no | Image attachment |
Successful response: `{ "message": "<human-readable status>" }`.
Failures: throw `PushNotifiMeError` (Node SDK) with `status` (HTTP code) and `body` (parsed JSON when available).
Default-application semantics: if neither the constructor `applicationKey` nor the per-message `application_key` is set, the API uses the account's default application. Do not invent values.
## Node (preferred — `pushnotifime` SDK)
Install: `npm install pushnotifime` (Node 18+ for global `fetch`).
```ts
import { PushNotifiMe, PushNotifiMeError } from "pushnotifime";
const userKey = process.env.PUSHNOTIFI_USER_KEY;
if (!userKey) throw new Error("PUSHNOTIFI_USER_KEY is not set");
const pn = new PushNotifiMe({ userKey });
export async function alert(title: string, message: string, idempotencyKey: string) {
try {
return await pn.send({
type: "group",
send_to_key: process.env.PUSHNOTIFI_GROUP_KEY!,
title,
message,
priority: 1,
idempotency_key: idempotencyKey,
});
} catch (err) {
if (err instanceof PushNotifiMeError) {
console.error("pushnotifi failed", err.status, err.body);
}
throw err;
}
}
```
Other SDK methods you can scaffold without re-implementing:
- `pn.listApplications()` → `GET /api/v1/applications`
- `pn.getApplication(applicationKey)` → `GET /api/v1/applications/:application_key`
- `pn.listGroups()` → `GET /api/v1/groups`
- `pn.listMessages()` → `GET /api/v1/messages` (history)
## Python (REST, no SDK)
```python
import os, json, urllib.request, urllib.error
API = os.environ.get("PUSHNOTIFI_API_BASE_URL", "https://api.pushnotifi.me")
KEY = os.environ["PUSHNOTIFI_USER_KEY"]
GROUP = os.environ["PUSHNOTIFI_GROUP_KEY"]
def alert(title: str, message: str, idempotency_key: str) -> dict:
body = json.dumps({
"type": "group",
"send_to_key": GROUP,
"title": title,
"message": message,
"priority": 1,
"idempotency_key": idempotency_key,
}).encode("utf-8")
req = urllib.request.Request(
f"{API}/api/v1/message",
data=body,
headers={"Content-Type": "application/json", "X-API-Key": KEY},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=10) as resp:
return json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as e:
raise RuntimeError(f"pushnotifi {e.code}: {e.read().decode('utf-8', 'ignore')}") from e
```
## Go (REST, stdlib)
```go
package pn
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
type sendBody struct {
Type string `json:"type"`
SendToKey string `json:"send_to_key"`
Title string `json:"title,omitempty"`
Message string `json:"message"`
Priority int `json:"priority,omitempty"`
IdempotencyKey string `json:"idempotency_key,omitempty"`
}
func Alert(ctx context.Context, title, message, idemKey string) error {
base := os.Getenv("PUSHNOTIFI_API_BASE_URL")
if base == "" {
base = "https://api.pushnotifi.me"
}
key := os.Getenv("PUSHNOTIFI_USER_KEY")
group := os.Getenv("PUSHNOTIFI_GROUP_KEY")
if key == "" || group == "" {
return fmt.Errorf("PUSHNOTIFI_USER_KEY and PUSHNOTIFI_GROUP_KEY must be set")
}
body, err := json.Marshal(sendBody{
Type: "group", SendToKey: group, Title: title, Message: message,
Priority: 1, IdempotencyKey: idemKey,
})
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, "POST", base+"/api/v1/message", bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-API-Key", key)
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
return fmt.Errorf("pushnotifi %d", resp.StatusCode)
}
return nil
}
```
## Shell / CI (curl)
Two send paths exist. Prefer the API-key path inside trusted environments; use the inbound-webhook-token path for ad-hoc shell scripts where you do not want to plumb the API key.
API key:
```bash
curl -fsS -X POST "https://api.pushnotifi.me/api/v1/message" \
-H "X-API-Key: $PUSHNOTIFI_USER_KEY" \
-H "Content-Type: application/json" \
-d "{\"type\":\"group\",\"send_to_key\":\"$PUSHNOTIFI_GROUP_KEY\",\"title\":\"$1\",\"message\":\"$2\",\"priority\":1}"
```
Inbound webhook token (per-user secret, fetched from the dashboard):
```bash
curl -fsS -X POST "https://api.pushnotifi.me/api/v1/webhooks/incoming/$PUSHNOTIFI_WEBHOOK_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"title\":\"$1\",\"message\":\"$2\"}"
```
## Express middleware (error boundary)
```ts
import express from "express";
import { PushNotifiMe } from "pushnotifime";
const pn = new PushNotifiMe(process.env.PUSHNOTIFI_USER_KEY!);
export function pushnotifiErrorAlerts(): express.ErrorRequestHandler {
return async (err, req, _res, next) => {
try {
await pn.send({
type: "group",
send_to_key: process.env.PUSHNOTIFI_GROUP_KEY!,
title: `Unhandled error: ${req.method} ${req.path}`,
message: (err as Error).message,
priority: 1,
idempotency_key: `req:${req.headers["x-request-id"] ?? Date.now()}`,
});
} catch (alertErr) {
console.error("pushnotifi alert failed", alertErr);
}
next(err);
};
}
```
Mount **after** all routes: `app.use(pushnotifiErrorAlerts());`.
## Invariants the generated code must satisfy
1. Every `send()` call has a stable `idempotency_key` if it is reachable from a retry path (cron, webhook, queue worker).
2. No code path swallows a `PushNotifiMeError` silently; either re-throw, log with status, or both.
3. Keys are read from env vars listed in the `pushnotifi-secrets` rule. Never literals.
4. Do not introduce a new dependency for HTTP — use the SDK in Node, the language stdlib elsewhere.
5. Do not wrap a function that already alerts; the agent must check before adding a wrapper (idempotent edits).When writing inbound webhook handlers, wrap the work in try/catch, send a PushNotifi alert on failure, and use a stable idempotency key.
When writing inbound webhook handlers, wrap the work in try/catch, send a PushNotifi alert on failure, and use a stable idempotency key.
webhook-resilience:
- Every webhook handler must catch errors at the handler boundary. Do not let an unhandled rejection or thrown error reach the platform default error page; that path produces no alert.
- On failure, send a PushNotifi notification using the `pushnotifime` SDK (Node) or `POST /api/v1/message` (other languages). Include:
- `title`: short, identifying source (e.g. `Stripe webhook failure`)
- `message`: the error message and the relevant external id (event id, order id), no secrets
- `priority`: `1` for must-investigate, `0` for informational
- `idempotency_key`: a stable string derived from the webhook event id (e.g. `stripe:evt_…`). Do **not** derive it from `Date.now()` or a random value — that defeats deduplication on retries.
- Reply to the webhook source with the response status the source expects (typically `200` once accepted, `4xx` for permanent rejection, `5xx` for transient). Do not return `200` on internal failure unless you have already enqueued the work for retry; otherwise the source will not retry and the failure is lost.
- Verify the source signature *before* doing any work. On signature mismatch, send a PushNotifi alert with `priority: 1` and respond `401`. Signature failures are a security signal, not a normal-path event.
- Do not log raw request bodies. Webhook payloads frequently contain PII or tokens.
- For high-volume webhooks, throttle alerts: alert on the first failure of a given `idempotency_key` and suppress repeats for at least 5 minutes. The PushNotifi API enforces idempotency on the server, but client-side throttling avoids the round trip.
- Read keys from environment variables only; see the `pushnotifi-secrets` rule.
Skeleton (Node, Next.js route handler):
```ts
import { NextRequest, NextResponse } from "next/server";
import { PushNotifiMe } from "pushnotifime";
const pn = new PushNotifiMe(process.env.PUSHNOTIFI_USER_KEY!);
export async function POST(req: NextRequest) {
const sig = req.headers.get("x-source-signature");
let event: { id: string; type: string };
try {
event = await verifyAndParse(req, sig);
} catch (err) {
await pn.send({
type: "group",
send_to_key: process.env.PUSHNOTIFI_GROUP_KEY!,
title: "Webhook signature mismatch",
message: (err as Error).message,
priority: 1,
idempotency_key: `sig-fail:${sig ?? "missing"}`,
});
return new NextResponse("invalid signature", { status: 401 });
}
try {
await processEvent(event);
return new NextResponse("ok", { status: 200 });
} catch (err) {
await pn.send({
type: "group",
send_to_key: process.env.PUSHNOTIFI_GROUP_KEY!,
title: "Webhook processing failed",
message: `${event.type} ${event.id}: ${(err as Error).message}`,
priority: 1,
idempotency_key: `wh:${event.id}`,
});
return new NextResponse("retry", { status: 500 });
}
}
```init
Bootstrap PushNotifi in the current project — install the pushnotifime SDK, write .env.example with the required keys, and print the dashboard URL for the user to fetch their values.
# /pushnotifi init
Bootstrap PushNotifi in the current project. Idempotent — safe to run more than once.
## Steps (execute in order)
1. **Detect package manager.** Check, in order: `pnpm-lock.yaml` → use `pnpm`. `yarn.lock` → use `yarn`. `package-lock.json` → use `npm`. Otherwise default to `npm`. Do not change the lockfile format.
2. **Verify Node version.** Run `node --version`. If the major version is < 18, stop and tell the user: "PushNotifi SDK requires Node 18+ for global fetch. Upgrade Node before continuing."
3. **Install the SDK.** Run the install with the detected package manager:
- `npm install pushnotifime`
- `pnpm add pushnotifime`
- `yarn add pushnotifime`
4. **Write `.env.example`.** Append (do not overwrite — read the existing file first and only add missing keys) the following lines:
```env
# PushNotifi.me — get these from https://pushnotifi.me/dashboard
PUSHNOTIFI_USER_KEY=
PUSHNOTIFI_GROUP_KEY=
# Optional — omit to use the account default application
# PUSHNOTIFI_APPLICATION_KEY=
# Optional — per-user inbound webhook token (shell/CI shortcut)
# PUSHNOTIFI_WEBHOOK_TOKEN=
```
5. **Update `.gitignore`.** If `.env` is not already ignored, append `.env` and `.env.*` plus a `!.env.example` exception. Do not duplicate existing patterns.
6. **Print next-step instructions to the user**, exactly:
> PushNotifi installed.
>
> 1. Open https://pushnotifi.me/dashboard and copy your API key into `PUSHNOTIFI_USER_KEY`.
> 2. Create or pick a group and copy its `g…` send-to key into `PUSHNOTIFI_GROUP_KEY`.
> 3. Run `/pushnotifi test` to send a test notification to your phone.
## Failure modes the command must handle
- **No `package.json` in cwd:** stop and tell the user "/pushnotifi init must be run at the root of a Node project (no package.json found)." Do not create one.
- **Network failure during install:** report the package manager's exit code and stderr verbatim; do not retry silently.
- **`.env.example` write permission denied:** report the path and the OS error; do not attempt sudo.
## What this command does NOT do
- Never reads or writes `.env` (only `.env.example`).
- Never sends a real notification (that is `/pushnotifi test`).
- Never logs or echoes any environment variable values.test
Send a test PushNotifi notification using the keys in the local environment. The first-run smoke test — must work end-to-end before any other plugin behavior is trusted.
# /pushnotifi test
Send a single test notification through the PushNotifi API to confirm credentials, network, and device delivery are all working.
## Steps (execute in order)
1. **Read environment.** Load `PUSHNOTIFI_USER_KEY` and `PUSHNOTIFI_GROUP_KEY` from `.env` (via `dotenv`-style loader if the project uses one) or from the shell environment. If either is missing or empty, stop and tell the user:
> Missing PUSHNOTIFI_USER_KEY or PUSHNOTIFI_GROUP_KEY. Run `/pushnotifi init` first, then fill them in `.env`.
2. **Send the notification.** Use the `pushnotifime` SDK if it is already installed in the project; otherwise fall back to a single `curl` invocation. Body:
```json
{
"type": "group",
"send_to_key": "<PUSHNOTIFI_GROUP_KEY>",
"title": "Test from Cursor",
"message": "PushNotifi plugin is working.",
"priority": 0,
"idempotency_key": "cursor-plugin-test:<unix-timestamp>"
}
```
`idempotency_key` uses the current Unix timestamp truncated to the second so re-runs of `/pushnotifi test` within the same second are deduplicated; re-runs across seconds always deliver a new push (which is what the user wants — they pressed the button twice).
3. **Report the result.**
- On HTTP 2xx: print "Test notification sent. Check your PushNotifi mobile app." plus the API's response `message` field.
- On HTTP 4xx: print the status code, the API's error message, and the most likely cause:
- `401` → "API key is invalid or revoked."
- `404` → "Group key was not found in your account."
- `400` → "Bad request body — check that `send_to_key` is a 32-char `g…` key."
- On network failure: print the underlying error and "Could not reach https://api.pushnotifi.me. Check connectivity."
## What this command does NOT do
- Does not send to a user `send_to_key` (`u…`). If the user wants that, they invoke the API directly. Group is the right default for "is this working".
- Does not retry on failure. The user pressed a button; show them the result.
- Does not redact or modify the API's response message — the user needs to see exactly what the server returned.
- Does not echo `PUSHNOTIFI_USER_KEY` to the terminal at any point, even on error.
## Reference invocation (curl fallback)
```bash
curl -fsS -X POST "${PUSHNOTIFI_API_BASE_URL:-https://api.pushnotifi.me}/api/v1/message" \
-H "X-API-Key: $PUSHNOTIFI_USER_KEY" \
-H "Content-Type: application/json" \
-d "{\"type\":\"group\",\"send_to_key\":\"$PUSHNOTIFI_GROUP_KEY\",\"title\":\"Test from Cursor\",\"message\":\"PushNotifi plugin is working.\",\"priority\":0,\"idempotency_key\":\"cursor-plugin-test:$(date +%s)\"}"
```Forbid hardcoding PushNotifi API keys and webhook tokens; require environment variables; never log secret values.
Forbid hardcoding PushNotifi API keys and webhook tokens; require environment variables; never log secret values.
pushnotifi-secrets:
- Never inline a PushNotifi `X-API-Key`, user key, group key, or inbound-webhook token as a string literal in source. Read from environment variables.
- Required env var names (use these exact names so other tooling and recipes line up):
- `PUSHNOTIFI_USER_KEY` — account API key sent as `X-API-Key`
- `PUSHNOTIFI_GROUP_KEY` — default destination `send_to_key` (group), 32-char `g…`
- `PUSHNOTIFI_APPLICATION_KEY` — optional; omit to use the account default application
- `PUSHNOTIFI_WEBHOOK_TOKEN` — optional; per-user inbound-webhook token from the dashboard
- `PUSHNOTIFI_API_BASE_URL` — optional; default `https://api.pushnotifi.me`
- Validate at process start: if `PUSHNOTIFI_USER_KEY` is missing or empty, fail fast with a clear error. Do not partially-initialize a client.
- Never write a key value to a log line, error message, telemetry payload, or test fixture. When the key must appear in an error context, redact to the last 4 characters.
- Add `.env` and `.env.*` (except `.env.example`) to `.gitignore` if the project does not already ignore them.
- When generating example files, write `.env.example` with placeholder values, never `.env`.
- Inbound webhook tokens are per-user secrets, not shared identifiers. Treat them with the same care as API keys.