# Nip19 Routing

> Implement or populate the root-level NIP-19 router (/:nip19) that handles npub, nprofile, note, nevent, and naddr identifiers. Covers decoding, secure filter construction, and type-specific rendering for profiles, notes, events, and addressable events.

- Skill: `soapbox-pub/nip19-routing` (Agent Skill)
- Install (CLI): `npx skillmds@latest add soapbox-pub/nip19-routing`
- Raw SKILL.md: https://api.skillmd.com/api/skills/soapbox-pub/nip19-routing/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: soapbox-pub (https://skillmd.com/u/soapbox-pub)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/soapbox-pub/nip19-routing

---


# NIP-19 Identifier Routing

NIP-19 defines the bech32-encoded identifiers used throughout Nostr (`npub1...`, `note1...`, `naddr1...`, etc.). This project routes all of them through a single root-level page at `/:nip19`, implemented by `src/pages/NIP19Page.tsx`.

Use this skill when the user wants to populate the `NIP19Page` sections with real views, add a new identifier type, or build links that point into the Nostr routing system.

## Identifier Reference

| Prefix       | Payload                                                          | Use when…                                                    |
|--------------|------------------------------------------------------------------|--------------------------------------------------------------|
| `npub1`      | 32-byte public key                                               | Simple user reference                                        |
| `nprofile1`  | Public key + optional relay hints + petname                      | User reference with relay context                            |
| `note1`      | 32-byte event ID (kind:1 text notes only, per NIP-10)            | Referencing a short text note/thread                         |
| `nevent1`    | Event ID + optional relay hints + author pubkey + kind           | Any event kind, or notes where you need relay/author context |
| `naddr1`     | `kind` + `pubkey` + `identifier` (`d` tag) + optional relay hints | Addressable events (kind 30000-39999): articles, products    |
| `nsec1`      | Private key                                                      | **Never display or route** — treat as a 404                  |
| `nrelay1`    | Relay URL                                                        | Deprecated                                                   |

### `note1` vs `nevent1`

- `note1` carries only an event ID, and is canonically tied to kind:1 text notes.
- `nevent1` can reference **any** kind and can carry relay hints + author pubkey. Prefer `nevent1` for non-kind-1 events or when you want to ship relay hints with a link.

### `npub1` vs `nprofile1`

- `npub1` is just a pubkey.
- `nprofile1` adds relay hints and a petname. Prefer it for shareable profile links where discoverability matters.

## Routing Rules

1. **All NIP-19 identifiers are handled at the URL root**: `/:nip19` in `AppRouter.tsx`. Never nest them under paths like `/note/:id` or `/profile/:npub`.
2. **Invalid, vacant, or unsupported identifiers** (including `nsec1` and `nrelay1`) render the 404 page. The `NIP19Page` boilerplate already handles this.
3. **Addressable event URLs must include the author**. `naddr1` already encodes `pubkey` + `kind` + `identifier`, which is exactly what a secure query filter needs. If you ever design an alternative URL, use the shape `/:npub/:dtag`, never `/:dtag` alone — otherwise anyone can publish a conflicting event with the same `d` tag.

## Decoding and Filtering

Nostr relay filters only accept hex strings. Always decode the NIP-19 identifier before building a filter.

```ts
import { nip19 } from 'nostr-tools';

const decoded = nip19.decode(value); // throws on invalid input

switch (decoded.type) {
  case 'npub': {
    const pubkey = decoded.data; // hex string
    return nostr.query([{ kinds: [0], authors: [pubkey], limit: 1 }]);
  }

  case 'nprofile': {
    const { pubkey /*, relays */ } = decoded.data;
    return nostr.query([{ kinds: [0], authors: [pubkey], limit: 1 }]);
  }

  case 'note': {
    const id = decoded.data;
    return nostr.query([{ ids: [id], kinds: [1], limit: 1 }]);
  }

  case 'nevent': {
    const { id /*, relays, author, kind */ } = decoded.data;
    return nostr.query([{ ids: [id], limit: 1 }]);
  }

  case 'naddr': {
    const { kind, pubkey, identifier } = decoded.data;
    return nostr.query([{
      kinds: [kind],
      authors: [pubkey],        // critical: prevents d-tag spoofing
      '#d': [identifier],
      limit: 1,
    }]);
  }

  default:
    // nsec, nrelay, unknown → 404
    throw new Error('Unsupported Nostr identifier');
}
```

### Common mistakes

```ts
// ❌ Passing bech32 into a filter
nostr.query([{ ids: [naddr] }]);

// ❌ Addressable lookup without the author — anyone can spoof the d-tag
nostr.query([{ kinds: [30023], '#d': [slug] }]);

// ✅ Decode first, then include author
const { kind, pubkey, identifier } = nip19.decode(naddr).data;
nostr.query([{ kinds: [kind], authors: [pubkey], '#d': [identifier] }]);
```

## Populating `NIP19Page`

`src/pages/NIP19Page.tsx` already:

- Decodes `params.nip19` with `nip19.decode`.
- Branches on `decoded.type` with a section for each supported identifier.
- Redirects invalid / unsupported identifiers to the 404 page.
- Provides a responsive container wrapper.

To turn it into a real router, replace each placeholder section with a concrete component:

| `decoded.type`        | Typical view                                                  |
|-----------------------|---------------------------------------------------------------|
| `npub` / `nprofile`   | Profile page: header from kind 0, feed of the user's events   |
| `note`                | Single kind:1 text note with thread + replies                 |
| `nevent`              | Generic event renderer; branch on `kind` for specialized UIs  |
| `naddr`               | Addressable-event view (article, product, community, etc.)    |

Inside each branch, pass the decoded payload (not the raw bech32 string) to a child component. That keeps filter construction colocated with the fetching hook and removes any chance of a re-decode mismatch.

## Linking to NIP-19 Routes

When building links elsewhere in the app:

```tsx
import { nip19 } from 'nostr-tools';
import { Link } from 'react-router-dom';

// To a profile
<Link to={`/${nip19.npubEncode(pubkey)}`}>Profile</Link>

// To an addressable event (article, product, …)
<Link to={`/${nip19.naddrEncode({ kind, pubkey, identifier, relays })}`}>
  Open
</Link>

// To a specific event of any kind, with relay hints
<Link to={`/${nip19.neventEncode({ id, relays, author, kind })}`}>Open</Link>
```

Always encode with the **most specific** identifier you have context for (`nprofile` > `npub`, `nevent` > `note`, `naddr` for addressable). The extra metadata makes links more robust across relays.

## Security Recap

- Decode **before** querying.
- For addressable events, always include `authors: [pubkey]` in the filter — the `d` tag alone is not a trust boundary.
- Treat `nsec1` and any unknown/invalid identifier as 404. Never render, log, or echo a decoded `nsec`.

