24 KiB
Master Prompt: Building a Decentralized Forum App with @opchan/react
Objective
Build a complete, production-ready decentralized forum application using the @opchan/react library. The app should support anonymous and wallet-based authentication, real-time content updates, and a modern, cyberpunk-inspired UI.
Core Requirements
1. Authentication System
Implement a dual-mode authentication system:
Anonymous Mode
- Users can browse and interact immediately without wallet
- Generate browser-based session (UUID) with ed25519 keypair
- Allow posting, commenting, and voting
- Support optional call sign for identity
- Session persists in IndexedDB
Wallet Mode
- Support Ethereum wallet connection (MetaMask, WalletConnect, Coinbase)
- Use wagmi v2.x for wallet management
- Implement ENS verification for premium features
- Support browser key delegation to reduce signature prompts
Implementation Pattern:
const { currentUser, connect, startAnonymous, verificationStatus } = useAuth();
// Offer both options
<button onClick={connect}>Connect Wallet</button>
<button onClick={startAnonymous}>Continue Anonymously</button>
// Check verification level
if (verificationStatus === EVerificationStatus.ANONYMOUS) {
// Show call sign setup
} else if (verificationStatus === EVerificationStatus.ENS_VERIFIED) {
// Show cell creation
}
2. Content Management
Implement a hierarchical content system:
- Cells - Discussion boards (require ENS to create)
- Posts - Threads within cells (any authenticated user)
- Comments - Replies to posts (any authenticated user)
- Votes - Upvote/downvote (any authenticated user)
Implementation Pattern:
const { createPost, createComment, vote, posts, comments } = useContent();
// Create content
await createPost({ cellId, title, content });
await createComment({ postId, content });
await vote({ targetId: postId, isUpvote: true });
// Display content with grouped data
const { postsByCell, commentsByPost } = useContent();
const cellPosts = postsByCell[cellId] || [];
const postComments = commentsByPost[postId] || [];
3. Permission System
Implement role-based access control:
| Action | Anonymous | Wallet | ENS Verified |
|---|---|---|---|
| View | ✅ | ✅ | ✅ |
| Vote | ✅ | ✅ | ✅ |
| Comment | ✅ | ✅ | ✅ |
| Post | ✅ | ✅ | ✅ |
| Create Cell | ❌ | ❌ | ✅ |
| Moderate | ❌ | ❌ | ✅ (own cells) |
Implementation Pattern:
const { canPost, canComment, canCreateCell, canModerate, check } = usePermissions();
// Show/hide UI based on permissions
{canCreateCell && <CreateCellButton />}
// Detailed permission check
const { allowed, reason } = check('canPost');
{!allowed && <p>{reason}</p>}
// Conditional features
{canModerate(cellId) && <ModerationTools />}
4. User Identity Display
Implement flexible identity rendering for both wallet and anonymous users:
Implementation Pattern:
const { displayName, callSign, ensName, ensAvatar } = useUserDisplay(address);
// Render user identity
<div>
{ensAvatar && <img src={ensAvatar} />}
<span>{displayName}</span>
{/* Verification badges */}
{callSign && <Badge><Hash /> Call Sign</Badge>}
{ensName && <Badge><Crown /> ENS</Badge>}
{isAnonymous(address) && <Badge><UserX /> Anonymous</Badge>}
</div>
// Detect anonymous users
function isAnonymous(address: string): boolean {
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(address);
}
5. Onboarding Flow
Create a 3-step wizard for wallet users, instant access for anonymous:
Wallet Wizard Steps:
- Connect Wallet - Choose wallet connector
- Verify Ownership - Check for ENS (optional, can skip)
- Delegate Key - Generate browser keys for better UX
Anonymous Flow:
- Click "Continue Anonymously" → Wizard closes immediately
- Show call sign setup in header dropdown
- Allow interaction immediately
Implementation Pattern:
// In wallet wizard
const handleStepComplete = (step) => {
if (step === 1 && verificationStatus === EVerificationStatus.ANONYMOUS) {
// Close wizard immediately for anonymous users
onComplete();
return;
}
// Continue to next step for wallet users
setCurrentStep(step + 1);
};
6. Real-Time Updates
Implement optimistic UI updates with network sync:
Implementation Pattern:
const { pending, lastSync } = useContent();
const isPending = pending.isPending(post.id);
// Show pending state
{isPending && <Loader2 className="animate-spin" />}
// Listen to pending changes
useEffect(() => {
return pending.onChange(() => {
// Update UI when pending state changes
});
}, []);
// Network status
{!network.isConnected && <button onClick={refresh}>Reconnect</button>}
7. Call Sign System
Allow all users (wallet and anonymous) to set call signs:
Implementation Patterns:
// Anonymous users: Show inline prompt
{currentUser?.verificationStatus === EVerificationStatus.ANONYMOUS &&
!currentUser.callSign && (
<InlineCallSignInput />
)}
// All users: Header dropdown option
<DropdownMenuItem onClick={() => setCallSignDialogOpen(true)}>
{currentUser?.callSign ? 'Update' : 'Set'} Call Sign
</DropdownMenuItem>
// Update profile
await updateProfile({ callSign: 'my_username' });
// Display shows call sign when available
{callSign || 'Anonymous User'}
8. Moderation System
Implement cell-based moderation (no global censorship):
Implementation Pattern:
const { moderate } = useContent();
const { canModerate } = usePermissions();
if (canModerate(cellId)) {
// Moderate content
await moderate.post(cellId, postId, 'Spam');
await moderate.comment(cellId, commentId, 'Off-topic');
await moderate.user(cellId, userAddress, 'Harassment');
// Unmoderate
await moderate.unpost(cellId, postId);
}
// Hide moderated content by default
const visiblePosts = posts.filter(p => !p.moderated);
// Admin toggle to show moderated
const [showModerated, setShowModerated] = useUIState('showModerated', false);
const displayPosts = showModerated ? posts : visiblePosts;
9. Relevance Scoring UI
Display relevance scores to help users find quality content:
Implementation Pattern:
// Sort by relevance
const sortedPosts = [...posts].sort((a, b) =>
(b.relevanceScore || 0) - (a.relevanceScore || 0)
);
// Show relevance details
{post.relevanceDetails && (
<div>
<span>Score: {post.relevanceDetails.finalScore.toFixed(1)}</span>
<span>Upvotes: {post.relevanceDetails.upvotes}</span>
<span>Verified: {post.relevanceDetails.verifiedUpvotes}</span>
</div>
)}
10. Network Status Indicator
Show Waku network connection state:
Implementation Pattern:
const { isConnected, statusMessage, isHydrated } = useNetwork();
// Health indicator
<div className={isConnected ? 'text-green-500' : 'text-red-500'}>
<WakuHealthDot />
<span>{statusMessage}</span>
</div>
// Wait for hydration before showing content
{isHydrated ? <Content /> : <LoadingSkeleton />}
Critical Implementation Details
State Preservation for Anonymous Users
Problem: Wallet sync effects may clear anonymous users Solution: Check verification status before clearing:
// In useAuth hook
useEffect(() => {
if (!wallet.isConnected && currentUser &&
currentUser.verificationStatus !== EVerificationStatus.ANONYMOUS) {
// Only clear non-anonymous users
clearUser();
}
}, [wallet.isConnected, currentUser]);
Verification Status Preservation
Problem: Profile updates may reset verification status Solution: Force preservation when updating:
const updateProfile = async (updates) => {
const res = await client.userIdentityService.updateProfile(address, updates);
const updated = {
...currentUser,
...res.identity,
verificationStatus: currentUser.verificationStatus, // Preserve!
};
setOpchanState(prev => ({ ...prev, session: { ...prev.session, currentUser: updated } }));
};
Verification Status Mapping
Problem: Status mapper doesn't handle ANONYMOUS Solution: Add ANONYMOUS case:
private mapVerificationStatus(status: string): EVerificationStatus {
switch (status) {
case EVerificationStatus.ANONYMOUS:
return EVerificationStatus.ANONYMOUS; // Add this!
case EVerificationStatus.WALLET_CONNECTED:
return EVerificationStatus.WALLET_CONNECTED;
// ... other cases
default:
return EVerificationStatus.WALLET_UNCONNECTED;
}
}
Message Verification
Problem: Network peers must accept anonymous messages Solution: Make delegationProof optional in verification:
async verify(message: OpchanMessage): Promise<boolean> {
// Verify message signature (always required)
if (!verifyMessageSignature(message)) return false;
// If has delegationProof, verify it (wallet user)
if (message.delegationProof) {
return verifyDelegationProof(message.delegationProof, message.author);
}
// Anonymous message - verify session ID format
return isValidSessionId(message.author);
}
UI/UX Guidelines
Design System
- Colors: Dark theme with cyan/blue accents
- Typography: Monospace fonts for technical aesthetic
- Components: Use shadcn/ui patterns
- Spacing: Consistent spacing with Tailwind scale
- Icons: Lucide React icons
Key UI Components to Build
-
Header Component
- Logo/branding
- Navigation (Home, Cells, Profile, Bookmarks)
- Auth button (Connect/User dropdown)
- Network status indicator
- Call sign setup for anonymous users
-
Wallet Wizard
- Step indicator (1/2/3)
- Wallet connection (Step 1)
- ENS verification (Step 2)
- Key delegation (Step 3)
- Anonymous bypass option
-
Cell List
- Grid/list view of cells
- Active member count
- Recent activity indicator
- "Create Cell" button (ENS only)
-
Post List
- Post cards with vote buttons
- Author display with badges
- Relevance score indicator
- Create post form (if has permission)
- Call sign prompt (anonymous without call sign)
-
Post Detail
- Full post content (markdown)
- Vote buttons
- Comment thread
- Comment form
- Moderation tools (if cell admin)
-
Author Display
- Handle wallet addresses (0x...)
- Handle anonymous (UUID pattern)
- Show call sign when available
- Show ENS when available
- Badge system (ENS, Call Sign, Anonymous)
-
Profile Page
- User information
- Call sign management
- Display preferences
- Security section (wallet: delegation status, anonymous: session info)
-
Inline Call Sign Input
- For anonymous users without call sign
- Contextual placement (where interaction blocked)
- Validation (3-20 chars, alphanumeric)
- Immediate feedback
Data Patterns
Content Grouping
// Group posts by cell
const { postsByCell } = useContent();
const cellPosts = postsByCell[cellId] || [];
// Group comments by post
const { commentsByPost } = useContent();
const postComments = commentsByPost[postId] || [];
// User verification status cache
const { userVerificationStatus } = useContent();
const isVerified = userVerificationStatus[authorAddress]?.isVerified;
Bookmarks
const { bookmarks, togglePostBookmark, toggleCommentBookmark } = useContent();
// Check if bookmarked
const isBookmarked = bookmarks.some(b => b.targetId === post.id && b.type === 'post');
// Toggle bookmark
await togglePostBookmark(post, cellId);
await toggleCommentBookmark(comment, postId);
Sorting
// Sort by relevance (default)
const sorted = [...posts].sort((a, b) =>
(b.relevanceScore || 0) - (a.relevanceScore || 0)
);
// Sort by time
const recent = [...posts].sort((a, b) => b.timestamp - a.timestamp);
// Sort by votes
const popular = [...posts].sort((a, b) =>
(b.upvotes?.length || 0) - (a.upvotes?.length || 0)
);
Critical Success Factors
✅ Must Have
- Immediate Engagement - Anonymous users can interact without barriers
- Clear Permission Feedback - Users know what they can/can't do and why
- Identity Flexibility - Support wallet addresses, ENS names, call signs, and anonymous
- Optimistic UI - Instant feedback with background sync
- Network Resilience - Handle disconnections gracefully
- State Preservation - Don't clear anonymous users on wallet events
- Verification Preservation - Maintain verification status through profile updates
⚠️ Common Pitfalls to Avoid
- ❌ Clearing anonymous sessions - Check verification status before clearing users
- ❌ Hardcoding wallet-only flows - Always support anonymous mode
- ❌ Forgetting to add ANONYMOUS to enums - Update all status mappers
- ❌ Blocking anonymous users - They should interact freely (except cell creation)
- ❌ Not showing call sign option - Make it prominent for anonymous users
- ❌ Complex onboarding - Keep anonymous flow instant (no wizard steps)
- ❌ Assuming addresses are 0x format - Support both wallet addresses and UUIDs
Complete Feature Checklist
Authentication & Identity
- Anonymous session creation
- Wallet connection (Ethereum)
- ENS verification
- Key delegation (wallet users)
- Call sign setup (all users)
- Display preferences
- Session persistence
- Disconnect/exit flows
Content Features
- Cell creation (ENS only)
- Post creation
- Comment creation (nested threading)
- Upvote/downvote
- Bookmark management
- Markdown support
- Content sorting (relevance, new, top)
- Author attribution
Moderation
- Moderate posts
- Moderate comments
- Moderate users
- Admin-only access
- Show/hide moderated toggle
- Moderation reasons
UI Components
- Header with auth state
- Wallet wizard (3 steps)
- Cell list with stats
- Post list/feed
- Post detail with comments
- Comment threads
- Vote buttons
- Author display with badges
- Network status indicator
- Loading states
- Error handling
- Call sign input (inline + dialog)
- Profile page
Developer Experience
- TypeScript strict mode
- Proper error handling
- Loading states
- Optimistic updates
- Hot reload support
- Build optimization
Technical Stack
Required Dependencies
{
"dependencies": {
"@opchan/react": "^1.1.0",
"@opchan/core": "^1.0.0",
"react": "^18.3.0",
"react-dom": "^18.3.0",
"react-router-dom": "^6.20.0",
"@tanstack/react-query": "^5.0.0",
"wagmi": "^2.0.0",
"viem": "^2.0.0",
"buffer": "^6.0.3"
},
"devDependencies": {
"@types/react": "^18.3.0",
"typescript": "^5.3.0",
"vite": "^5.0.0",
"tailwindcss": "^3.4.0"
}
}
Recommended UI Libraries
- shadcn/ui - Accessible component primitives
- Radix UI - Unstyled accessible components
- Tailwind CSS - Utility-first styling
- Lucide React - Icon library
- date-fns - Date formatting
- react-hook-form - Form handling
- zod - Schema validation
Example App Structure
src/
├── main.tsx # Entry point with OpChanProvider
├── App.tsx # Router setup
├── hooks/
│ └── index.ts # Re-export @opchan/react hooks
├── components/
│ ├── ui/ # shadcn/ui components
│ │ ├── wallet-wizard.tsx # 3-step onboarding
│ │ ├── wallet-connection-step.tsx
│ │ ├── verification-step.tsx
│ │ ├── delegation-step.tsx
│ │ ├── author-display.tsx # User identity display
│ │ ├── inline-callsign-input.tsx
│ │ ├── call-sign-setup-dialog.tsx
│ │ └── ... # Other UI primitives
│ ├── Header.tsx # Navigation + auth
│ ├── PostList.tsx # Post feed
│ ├── PostDetail.tsx # Single post view
│ ├── PostCard.tsx # Post preview
│ ├── CommentCard.tsx # Comment display
│ ├── CellList.tsx # Cell grid
│ └── CreateCellDialog.tsx # Cell creation modal
├── pages/
│ ├── Dashboard.tsx # Landing page
│ ├── Index.tsx # Cell list page
│ ├── CellPage.tsx # Cell detail
│ ├── PostPage.tsx # Post detail
│ ├── ProfilePage.tsx # User profile
│ ├── BookmarksPage.tsx # Saved content
│ └── NotFound.tsx # 404 page
└── utils/
└── sorting.ts # Content sorting utilities
Code Examples
Complete Anonymous Flow Implementation
// 1. Wallet Wizard with Anonymous Option
export function WalletWizard() {
const { startAnonymous, verificationStatus } = useAuth();
const handleAnonymous = async () => {
const sessionId = await startAnonymous();
if (sessionId) {
onComplete(); // Close wizard immediately
}
};
return (
<Dialog>
<DialogContent>
{/* Wallet connection UI */}
<Button onClick={connectWallet}>Connect Wallet</Button>
{/* Anonymous option */}
<div className="separator">Or</div>
<Button variant="outline" onClick={handleAnonymous}>
Continue Anonymously
</Button>
<p className="text-xs">You can post, comment, and vote (but not create cells)</p>
</DialogContent>
</Dialog>
);
}
// 2. Header with Call Sign Access
export function Header() {
const { currentUser } = useAuth();
const [callSignOpen, setCallSignOpen] = useState(false);
return (
<header>
{currentUser && (
<DropdownMenu>
<DropdownMenuTrigger>{currentUser.displayName}</DropdownMenuTrigger>
<DropdownMenuContent>
{currentUser.verificationStatus === EVerificationStatus.ANONYMOUS && (
<DropdownMenuItem onClick={() => setCallSignOpen(true)}>
{currentUser.callSign ? 'Update' : 'Set'} Call Sign
</DropdownMenuItem>
)}
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>
{currentUser.verificationStatus === EVerificationStatus.ANONYMOUS
? 'Exit Anonymous'
: 'Disconnect'}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
<CallSignSetupDialog open={callSignOpen} onOpenChange={setCallSignOpen} />
</header>
);
}
// 3. Author Display with Anonymous Support
export function AuthorDisplay({ address }: { address: string }) {
const { displayName, callSign, ensName } = useUserDisplay(address);
const isAnonymous = /^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(address);
return (
<div className="flex items-center gap-2">
<span>{displayName}</span>
{isAnonymous ? (
callSign ? (
<Badge className="bg-green-900/20"><Hash /> Call Sign</Badge>
) : (
<Badge className="bg-neutral-800/50"><UserX /> Anonymous</Badge>
)
) : (
ensName && <Badge className="bg-green-900/20"><Crown /> ENS</Badge>
)}
</div>
);
}
// 4. Inline Call Sign Input
export function InlineCallSignInput() {
const { currentUser, updateProfile } = useAuth();
const [callSign, setCallSign] = useState('');
// Only show for anonymous users without call sign
if (currentUser?.verificationStatus !== EVerificationStatus.ANONYMOUS ||
currentUser?.callSign) {
return null;
}
const handleSubmit = async () => {
const success = await updateProfile({ callSign });
if (success) {
toast({ title: 'Call Sign Set!' });
}
};
return (
<div className="p-4 border rounded bg-muted">
<p className="text-sm mb-3">Set a call sign to personalize your identity</p>
<div className="flex gap-2">
<Input
placeholder="your_call_sign"
value={callSign}
onChange={(e) => setCallSign(e.target.value)}
maxLength={20}
/>
<Button onClick={handleSubmit}>Set Call Sign</Button>
</div>
</div>
);
}
// 5. Permission-Based Content Forms
export function PostDetail({ postId }: { postId: string }) {
const { permissions, currentUser } = useForum();
const { createComment } = useContent();
return (
<div>
{/* Post content */}
{/* Call sign suggestion for anonymous users */}
{currentUser?.verificationStatus === EVerificationStatus.ANONYMOUS &&
!currentUser.callSign &&
permissions.canComment && (
<InlineCallSignInput />
)}
{/* Comment form */}
{permissions.canComment && (
<CommentForm onSubmit={(content) => createComment({ postId, content })} />
)}
{/* Blocked state */}
{!permissions.canComment && (
<div>
<p>Connect your wallet to comment</p>
<Button>Connect Wallet</Button>
</div>
)}
</div>
);
}
Testing Your Implementation
Manual Test Checklist
Anonymous Flow:
- Can start anonymous session
- Can post without call sign
- Can comment without call sign
- Can vote without call sign
- Can set call sign from header
- Call sign appears in author display
- Session persists after call sign update
- Cannot create cells
- Can exit anonymous session
Wallet Flow:
- Can connect wallet
- ENS resolves automatically
- Can delegate browser keys
- Can create cells (if ENS verified)
- Can post without repeated signatures
- Can moderate own cells
- Can update profile
- Can disconnect wallet
Edge Cases:
- Anonymous user sets call sign → remains anonymous
- Wallet disconnects → doesn't clear anonymous users
- Profile update → preserves verification status
- Network disconnect → shows reconnect option
- Content pending → shows sync status
Performance Optimization
- Memoization - Use React.memo for expensive components
- Virtual Lists - For long comment threads
- Lazy Loading - Code-split routes
- Image Optimization - Lazy load ENS avatars
- Bundle Size - Tree-shake unused components
Deployment
Build Commands
# Build all packages
npm run build
# Build for production
cd app
npm run build
# Output: app/dist/
Environment Variables
# .env.production
VITE_REOWN_SECRET=your_production_reown_id
Static Hosting
Deploy app/dist/ to:
- Vercel
- Netlify
- GitHub Pages
- IPFS
Configure SPA routing:
{
"routes": [
{ "handle": "filesystem" },
{ "src": "/(.*)", "dest": "/index.html" }
]
}
Summary
Building with @opchan/react requires understanding:
- Dual authentication model - Anonymous and wallet modes
- Permission-based UI - Show features based on capabilities
- Key delegation - Browser keys reduce wallet friction
- Local-first architecture - IndexedDB with network sync
- Real-time updates - React state tied to network messages
- Identity flexibility - Handle addresses, ENS, call signs, and sessions
- Verification preservation - Critical for anonymous users
- Message verification - Optional delegation proofs for protocol v2
Follow these patterns and you'll build a robust, user-friendly decentralized forum that works for everyone - from crypto natives to complete newcomers.
For complete examples, see the reference implementation in /app