CursorPool
← 返回首页

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.

cursor.directory·4
MCP

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}"
  }
}
Skill

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.
Skill

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.

来源:https://github.com/thetawavetechnologies/pushnotifime