Build Nostr protocol clients with React 18, TailwindCSS, Vite, shadcn/ui, and Nostrify. Complete stack for social media applications with authentication, posts, profiles, and real-time updates.
Build decentralized social media applications on the Nostr protocol using React 18.x, TailwindCSS 3.x, Vite, shadcn/ui, and Nostrify.
```
/src/
/components/ # UI components
/ui/ # shadcn/ui components (48+ available)
/auth/ # Authentication components (LoginArea, LoginDialog)
/hooks/ # Custom hooks
/pages/ # Page components for React Router
/lib/ # Utility functions
/contexts/ # React context providers (AppContext)
/test/ # Testing utilities
/public/ # Static assets
App.tsx # Main app with provider setup
AppRouter.tsx # React Router configuration
NIP.md # Custom Nostr protocol definitions (if exists)
```
Before creating custom event kinds:
1. Use `nostr__read_nips_index` to check existing kind numbers
2. Choose kind number based on desired behavior:
- **Regular Events** (1 ≤ kind < 10000): Stored permanently by relays
- **Replaceable Events** (10000 ≤ kind < 20000): Only latest per pubkey+kind stored
- **Addressable Events** (30000 ≤ kind < 40000): Latest per pubkey+kind+d-tag stored
3. Document custom events in `NIP.md` at project root
4. Update `NIP.md` whenever custom event structures change
5. Use `nostr__read_protocol` for detailed event structure information
**Nostr Core:**
**Utilities:**
Create custom hooks combining `useNostr` and `useQuery`:
```typescript
import { useNostr } from '@nostrify/react';
import { useQuery } from '@tanstack/react-query';
function usePosts() {
const { nostr } = useNostr();
return useQuery({
queryKey: ['posts'],
queryFn: async (c) => {
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(1500)]);
const events = await nostr.query([{ kinds: [1], limit: 20 }], { signal });
return events;
},
});
}
```
**Event Validation:** For custom kinds or kinds with strict requirements, filter events through validator functions:
```typescript
function validateEvent(event: NostrEvent): boolean {
// Check required tags/fields per NIP spec
const requiredTag = event.tags.find(([name]) => name === 'required')?.[1];
return !!requiredTag;
}
// In queryFn:
return events.filter(validateEvent);
```
Use `useAuthor` to fetch and display user metadata:
```tsx
import { useAuthor } from '@/hooks/useAuthor';
import { genUserName } from '@/lib/genUserName';
import type { NostrEvent, NostrMetadata } from '@nostrify/nostrify';
function Post({ event }: { event: NostrEvent }) {
const author = useAuthor(event.pubkey);
const metadata: NostrMetadata | undefined = author.data?.metadata;
const displayName = metadata?.name ?? genUserName(event.pubkey);
const profileImage = metadata?.picture;
return <div>{/* Render with displayName, profileImage */}</div>;
}
```
**NostrMetadata fields:** `name`, `display_name`, `about`, `picture`, `banner`, `nip05`, `lud06`, `lud16`, `website`, `bot`
Use `useNostrPublish` with `useCurrentUser` to ensure authentication:
```tsx
import { useState } from 'react';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useNostrPublish } from '@/hooks/useNostrPublish';
export function PostForm() {
const [content, setContent] = useState('');
const { user } = useCurrentUser();
const { mutate: createEvent } = useNostrPublish();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
createEvent({ kind: 1, content });
};
if (!user) {
return <span>You must be logged in to post.</span>;
}
return (
<form onSubmit={handleSubmit}>
<textarea value={content} onChange={e => setContent(e.target.value)} />
<button type="submit">Post</button>
</form>
);
}
```
Use the `LoginArea` component for Nostr authentication:
```tsx
import { LoginArea } from '@/components/auth/LoginArea';
function Header() {
return (
<header>
<nav>{/* navigation items */}</nav>
<LoginArea className="max-w-60" />
</header>
);
}
```
**Behavior:**
**Identifier Prefixes:**
**Decoding for Filters:**
```typescript
import { nip19 } from 'nostr-tools';
// Decode NIP-19 identifier
const decoded = nip19.decode(value);
// For addressable events (naddr)
if (decoded.type === 'naddr') {
const naddr = decoded.data;
const events = await nostr.query([{
kinds: [naddr.kind],
authors: [naddr.pubkey],
'#d': [naddr.identifier],
}], { signal });
}
```
**URL Routing:**
**Security:** Always use `naddr` identifiers for addressable events as they include the author pubkey, preventing malicious override of events with same `d` tag.
48+ accessible components in `@/components/ui`:
**Layout:** Accordion, AspectRatio, Card, Collapsible, Drawer, ResizablePanel, ScrollArea, Separator, Sheet, Sidebar, Tabs
**Forms:** Button, Checkbox, Form, Input, InputOTP, Label, RadioGroup, Select, Slider, Switch, Textarea, Toggle, ToggleGroup
**Feedback:** Alert, AlertDialog, Badge, Dialog, HoverCard, Popover, Progress, Skeleton, Sonner, Toast, Tooltip
**Navigation:** Breadcrumb, Command, ContextMenu, DropdownMenu, Menubar, NavigationMenu, Pagination
**Data:** Calendar, Carousel, Chart, Table
**Pattern:** Components use `forwardRef`, `cn()` utility for className merging, built on Radix UI primitives
1. **Always validate custom event kinds** with validator functions
2. **Use NIP-19 identifiers in URLs** for secure, universal links
3. **Check authentication** with `useCurrentUser` before allowing event publishing
4. **Combine `useNostr` + `useQuery`** for data fetching with proper caching
5. **Set query timeouts** using `AbortSignal.any()` pattern (1500ms recommended)
6. **Document custom events** in `NIP.md` and keep it updated
7. **Use `cn()` utility** for className merging in components
8. **Leverage shadcn/ui components** for consistent, accessible UI
```tsx
import { useNostr } from '@nostrify/react';
import { useQuery } from '@tanstack/react-query';
import { useAuthor } from '@/hooks/useAuthor';
import { genUserName } from '@/lib/genUserName';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
import type { NostrEvent } from '@nostrify/nostrify';
function usePosts() {
const { nostr } = useNostr();
return useQuery({
queryKey: ['posts'],
queryFn: async (c) => {
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(1500)]);
return await nostr.query([{ kinds: [1], limit: 20 }], { signal });
},
});
}
function PostCard({ event }: { event: NostrEvent }) {
const author = useAuthor(event.pubkey);
const metadata = author.data?.metadata;
const displayName = metadata?.name ?? genUserName(event.pubkey);
return (
<Card>
<CardHeader>
<Avatar>
<AvatarImage src={metadata?.picture} />
<AvatarFallback>{displayName[0]}</AvatarFallback>
</Avatar>
<span>{displayName}</span>
</CardHeader>
<CardContent>{event.content}</CardContent>
</Card>
);
}
export function PostFeed() {
const { data: posts, isLoading } = usePosts();
if (isLoading) return <div>Loading...</div>;
return (
<div className="space-y-4">
{posts?.map(post => <PostCard key={post.id} event={post} />)}
</div>
);
}
```
Leave a review
No reviews yet. Be the first to review this skill!
# Download SKILL.md from killerskills.ai/api/skills/nostr-twitter-alternative-client/raw