Skip to content

jeefxM/forumzero

Repository files navigation

forumzero

forumzero

An anonymous forum where every post lives on Arkiv. Members prove they belong without revealing who they are.

Arkiv ETHNS Builder Challenge Theme: Privacy Next.js 16 License MIT

Built for the Arkiv ETHNS Builder Challenge. Theme: Privacy. Confidential data patterns on a public, tamper-proof layer.

forumzero is a community forum for Network School members. It looks like any other forum. Two things make it different:

  • You're anonymous to the room. Sign in once with Discord, mint a Semaphore identity in your browser, and from then on every post, reply, and vote is signed by a zero-knowledge proof. The forum knows you're an NS member. It never learns which one.
  • Every post is yours, not ours. Threads, comments, polls, and votes are first-class Arkiv entities. If this server vanished tomorrow, every conversation would still be readable on chain. You'd just need someone to re-host the UI.

Features

  • Anonymous sign-in with Discord OAuth + Semaphore ZK proofs
  • Encrypted identity backup on Arkiv with PBKDF2 + AES-GCM, restorable on any device with your passphrase
  • Threads with categories: food, gym, crypto, build, trips, housing, marina hotel, cohorts, ai, general
  • Polls attachable to discussions, one vote per member, anonymous
  • Live on-chain provenance — every action surfaces its Arkiv transaction hash
  • Mobile-first UI with brutalist Tektur display + orange accents
  • Identity reset from the profile page, with on-chain tombstone receipts

Architecture

Six entity types on Arkiv

All entities share a PROJECT_ATTRIBUTE so reads and writes stay scoped to this app.

Entity Stores TTL
group-snapshot Semaphore commitments + Discord subject hashes. Append-only, versioned. 1 year
identity-backup Encrypted Semaphore identity. Versioned. Tombstoned on delete. 1 year
thread Title, body, tag, author handle, optional poll_id. 30 days
comment Body, parent type, parent ID. 30 days
poll Question, options, closes-at. 30 days
vote Poll ID, option index, voter nullifier. 30 days

Relationships

Implemented through shared attribute keys (the Arkiv pattern):

  • thread.poll_id points at a poll
  • comment.parent_id points at a thread or poll
  • vote.poll_id points at a poll
  • group-snapshot.version chains snapshots in append-only order

Auth flow

Discord OAuth          → server cookie (subject_id)
       ↓
Mint Semaphore identity in browser
       ↓
Encrypt with passphrase → PUT /api/backup        → identity-backup entity
       ↓
Add commitment          → POST /api/group/join   → group-snapshot entity
       ↓
Generate ZK proof of membership
       ↓
POST /api/verify-zeropass                        → forum session cookie
       ↓
You're in. The forum can't tell which member you are.

On a second device, the browser fetches GET /api/backup, prompts for the passphrase, decrypts the identity, and proves membership against the same group root. No re-verifying Discord.

Note

Three independent privacy properties are preserved:

Data Public on Arkiv Server sees Linkable to you
Membership commitment yes no no
Encrypted identity backup yes (ciphertext) no no
Threads / comments / votes yes no no

Why the forum signs, not the user

Every entity in forumzero is signed by a single server wallet. On the explorer you'll see $creator = 0x1135D4546611D7CcCC7C4E0315072Ef2E61b9483 on every thread, comment, poll, vote, group snapshot, and backup. That's deliberate.

If each user signed their own posts from their own wallet, the chain would group all their writes under one address. Anyone reading the explorer could deanonymize authors through timing, frequency, or style correlation. Anonymous and wallet-signed are mutually exclusive.

So we trade chain-native ownership for cryptographic ownership at the protocol layer:

What Who owns it How it's enforced
Your Semaphore identity (trapdoor + nullifier) You Lives only in your browser's localStorage. Server can't read it.
Your encrypted backup You PBKDF2 + AES-GCM in the browser. Server stores ciphertext only. Without your passphrase, it's noise.
Your right to post under a given handle You Every new post requires a fresh ZK proof produced with your private nullifier. Server can't fabricate one.
Your votes You Same nullifier signs everything. One member, one vote, no impersonation.

$owner is never exercised either. Every entity is append-only. Group snapshots get new versions on join or leave. Backups get new versions on update. Threads and comments are write-once. The chain is the source of truth and nobody, including the server, modifies it after the fact.

So when the brief says "users own their data, instead of their data being owned by the platform," in forumzero that means: the forum can't write as you, can't read your identity, can't decrypt your backup, and can't delete what you wrote. It can render your posts. That's it.

Tech stack

  • Next.js 16 (App Router, RSC + client components)
  • Arkiv SDK (@arkiv-network/sdk) for entity CRUD on the Braga testnet
  • Semaphore Protocol (@semaphore-protocol/core) for ZK membership proofs
  • Tailwind v4 for styling
  • Discord OAuth for the one-time NS membership check

Getting started

Prerequisites

  • Node.js 20+
  • A Discord application with OAuth2 set up (guide)
  • An Arkiv Braga testnet wallet (you can generate one with node scripts/gen-wallet.mjs)

Install

npm install

Configure environment

Create a .env file at the project root:

# Arkiv
ARKIV_PRIVATE_KEY=0x...           # signs all writes
ARKIV_POLL_PROJECT=arkiv_test     # value used for PROJECT_ATTRIBUTE
ARKIV_GROUP_ID=arkiv_test-ns      # group namespace inside the project

# Discord
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
DISCORD_REDIRECT_URI=http://localhost:3002/api/auth/discord/callback
NS_GUILD_ID=                      # NS Discord guild ID
NS_MEMBER_ROLE_ID=                # optional, blank accepts any guild member

# Session
FORUM_SESSION_SECRET=             # any 32+ char random string

Important

Add the DISCORD_REDIRECT_URI to your Discord app's allowed redirect URIs under OAuth2 → Redirects, or callbacks will fail.

Fund the Arkiv wallet

Hit the Braga faucet for the address derived from your private key:

https://braga.hoodi.arkiv.network/faucet/

Verify it can write:

node scripts/arkiv-ping.mjs

Expected output:

✓ Wrote entity in ~1000ms
  txHash:    0x...
  explorer:  https://explorer.braga.hoodi.arkiv.network/tx/0x...

Run

npm run dev

Open http://localhost:3002 and hit /auth to sign in.

Tip

The first sign-in walks you through Discord verification, passphrase setup, group join, and proof generation. After that, every action you take posts a real Arkiv transaction with a link you can click straight to the explorer.

Useful scripts

Script Purpose
npm run dev Start the dev server on port 3002
npm run build Production build
npm run start Start the production server
node scripts/gen-wallet.mjs Generate a fresh Arkiv private key + address
node scripts/arkiv-ping.mjs Write a test entity to verify env + wallet are working
node scripts/arkiv-read.mjs Read the latest block, balance, and nonce status

Project layout

src/
├── app/
│   ├── api/
│   │   ├── auth/discord/        Discord OAuth start + callback
│   │   ├── group/               GET snapshot, POST join, POST leave
│   │   ├── backup/              GET, PUT, DELETE encrypted backup
│   │   ├── identity/delete/     Full account wipe
│   │   ├── threads/             GET list, POST create, GET [id]
│   │   ├── comments/            GET list, POST create
│   │   ├── poll/                GET list, POST create, POST cast
│   │   └── verify-zeropass/     Verify ZK proof, set session
│   ├── auth/                    Discord + passphrase flow
│   ├── profile/                 Handle, sign out, delete identity
│   ├── rules/                   Community rules
│   └── page.tsx                 Forum home
├── components/forum/            UI (top bar, composer, toasts, detail)
└── lib/
    ├── arkiv-client.ts          Shared viem clients + PROJECT_ATTRIBUTE
    ├── group-store.ts           group-snapshot CRUD
    ├── backup-store.ts          identity-backup CRUD
    ├── thread-store.ts          thread CRUD
    ├── comment-store.ts         comment CRUD
    ├── poll-store.ts            poll + vote CRUD
    ├── discord.ts               OAuth helpers
    ├── discord-session.ts       Discord-verified cookie
    ├── session.ts               Forum session cookie
    └── zeropass-identity.ts     Semaphore identity in localStorage

The walkthrough covers: Discord verify, passphrase setup, post a thread, attach a poll, second member votes, then open the Arkiv explorer from the in-app transaction link.

Team

Name GitHub Wallet
Jeefx https://github.com/jeefxm 0x93fA0E828Ab8b72EEEE42747DE3f9C66D1B43a5c

Roadmap

  • Cross-device QR pairing as an alternative to passphrase backup
  • Federated groups: other communities running their own forumzero on the same primitives
  • Rich-text and image posts (entity payload as bytes)

Caution

Lose your passphrase and your anonymous identity is gone forever. We can't recover it. Nobody can. Save it somewhere off-device.

About

forumzero: anonymous, NS-only forum gated by ZK proofs. Every thread, comment, poll, and vote lives on Arkiv.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors