A Web3-powered community and wallet application built on Polkadot Asset Hub
- Introduction
- Product Architecture
- Feature Highlights
- Prerequisites
- Installation Guide
- Configuration
- Database Setup
- Running the Application
- API Reference
- Testing
- Deployment
- Troubleshooting
- Security Considerations
Relay is an Web3 application that combines community management with cryptocurrency wallet functionality. Built on Next.js 16 and Polkadot Asset Hub, it enables users to:
- Create and manage crypto wallets on Polkadot Asset Hub
- Join and create communities with shared activities
- Send and receive tokens on Polkadot Asset Hub
- Create community tokens (Coming soon...)
- Authenticate securely using wallet signatures (no passwords required)
| Layer | Technology |
|---|---|
| Frontend | Next.js 16, React 19, TailwindCSS 4 |
| Backend | Next.js API Routes (Edge-compatible) |
| Database | Supabase (PostgreSQL with RLS), only for community and activity storage |
| Blockchain | Polkadot Asset Hub via Polkadot API |
| Authentication | Wallet-based JWT (sr25519 signatures) |
| Testing | Vitest, Testing Library |
┌─────────────────────────────────────────────────────────────────────────────┐ │ Web2 + Web3 SERVICES │ ├─────────────────────────────────────────────────────────────────────────────┤ │ ┌───────────────────────────┐ ┌───────────────────────────────┐ │ │ │ Supabase Database │ │ Polkadot Asset Hub │ │ │ │ (PostgreSQL + RLS) │ │ (Blockchain) │ │ │ │ │ │ │ │ │ │ - users │ │ - Native DOT transfers │ │ │ │ - communities │ │ - Asset Hub tokens │ │ │ │ - activities │ │ │ │ │ │ - comments │ │ │ │ │ │ - friends │ │ │ │ │ │ - transactions │ │ │ │ │ │ - community_tokens │ │ │ │ │ │ - known_assets │ │ │ │ │ └───────────────────────────┘ └───────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────────┘
### Directory Structure
relay-new/ ├── app/ # Next.js App Router │ ├── api/ # API Routes │ │ ├── auth/ # Authentication endpoints │ │ │ ├── nonce/ # Generate auth nonce │ │ │ └── verify/ # Verify signature & issue JWT │ │ ├── activity/ # Activity management │ │ ├── community/ # Community CRUD operations │ │ ├── friends/ # Friend list management │ │ └── user/ # User profile operations │ ├── dashboard/ # Main application pages │ │ ├── community/ # Community views │ │ ├── settings/ # User settings │ │ └── wallet/ # Wallet operations │ ├── db/ # Database utilities │ │ └── supabase.ts # Supabase client & operations │ ├── types/ # TypeScript type definitions │ │ ├── frontend_type.ts # Domain types │ │ └── constants.ts # App constants │ └── utils/ # Utility functions │ └── auth.ts # Authentication utilities ├── components/ # Reusable UI components ├── lib/ # Shared libraries ├── public/ # Static assets ├── tests/ # Test suites ├── supabase-schema.sql # Main database schema ├── supabase-community-tokens.sql # Token extension schema └── supabase-migration-*.sql # Migration scripts
### Data Flow
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ Frontend │────▶│ Next.js │────▶│ Supabase │ │ Components │ │ API Routes │ │ Database │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ │ │ │ ▼ ▼ ▼ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ supabase.ts │ │ auth.ts │ │ RLS Policies│ │ (queries) │ │ (server) │ │ (security) │ └──────────────┘ └──────────────┘ └──────────────┘
---
## Feature Highlights
### 1. Web3 Wallet Authentication (Passwordless)
Relay uses a cryptographic signature-based authentication system. Users prove ownership of their wallet by signing a message with their private key, eliminating the need for passwords.
**How it works:**
┌─────────────────────────────────────────────────────────────────────────────┐ │ AUTHENTICATION FLOW │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ 1. User wants to authenticate │ │ └─▶ Call authenticateWithWallet() from auth.ts │ │ │ │ 2. Get wallet address from stored mnemonic │ │ └─▶ Uses @polkadot/keyring with sr25519 curve │ │ │ │ 3. Request nonce from server │ │ └─▶ POST /api/auth/nonce { walletAddress } │ │ └─▶ Server stores nonce in auth_nonces table (5 min expiry) │ │ └─▶ Returns { message, nonce } │ │ │ │ 4. Sign the message with wallet │ │ └─▶ Uses keypair.sign(message) with sr25519 │ │ └─▶ Creates cryptographic proof of wallet ownership │ │ │ │ 5. Verify signature and get JWT │ │ └─▶ POST /api/auth/verify { walletAddress, signature, nonce } │ │ └─▶ Server verifies using @polkadot/util-crypto signatureVerify │ │ └─▶ Server creates/updates user in users table │ │ └─▶ Server generates JWT with wallet_address claim (24h expiry) │ │ └─▶ Returns { token } │ │ │ │ 6. Set JWT in Supabase client │ │ └─▶ All subsequent requests include the JWT │ │ │ │ 7. RLS policies check JWT for wallet_address │ │ └─▶ auth.jwt() ->> 'wallet_address' in SQL policies │ │ └─▶ Users can only access their own data │ │ │ └─────────────────────────────────────────────────────────────────────────────┘
**Security Features:**
- **No password storage**: Authentication is based on cryptographic signatures
- **One-time nonces**: Prevents replay attacks
- **Short expiry**: Nonces expire in 5 minutes
- **JWT tokens**: Secure, stateless session management
- **RLS enforcement**: Database-level access control
### 2. Polkadot Asset Hub Integration
Relay integrates directly with Polkadot Asset Hub for blockchain operations:
- **Native DOT transfers**: Send and receive DOT tokens
- **Asset Hub tokens**: Support for USDt, USDC, and other fungible assets
- **Community tokens**: Create custom tokens for communities
- **QR code payments**: Scan to send/receive tokens
### 3. Community Management
Create and manage communities with:
- **Custom activity types**: Define allowed activities (meetings, events, etc.)
- **Member management**: Join/leave communities
- **Community tokens**: Issue fungible tokens on Polkadot Asset Hub
- **Activity scheduling**: Create and manage events
- **Real-time updates** (needs fine-tune): Live activity and comment subscriptions
### 4. Social Features
- **Friends list**: Manage contacts with wallet addresses
- **Activity comments**: Discuss activities with community members
- **Like system**: Engage with activities and comments
- **Transaction history**: Track all wallet transactions
### 5. Polkadot Bazaar
Browse known tokens on Polkadot Asset Hub:
- Stablecoins (USDt, USDC)
- Utility tokens
- Bridged assets
---
## Prerequisites
Before installation, ensure you have:
| Requirement | Version | Purpose |
|------------|---------|---------|
| Node.js | 18.x or higher | JavaScript runtime |
| npm | 9.x or higher | Package manager |
| Git | 2.x or higher | Version control |
| Supabase Account | Free tier OK | Database & auth |
### Supabase Account
1. Go to [supabase.com](https://supabase.com)
2. Sign up or log in
3. Create a new project (free tier is sufficient)
---
## Installation Guide
### Step 1: Clone the Repository
```bash
git clone https://github.com/MontaQ-Labs/relay-new.git
cd relay-new
npm installThis will:
- Install all npm packages
- Run
papi generate(postinstall script) to generate Polkadot API descriptors
# Check that dependencies are installed correctly
npm run lint- Log in to Supabase Dashboard
- Click "New Project"
- Fill in project details:
- Name:
relay(or your preferred name) - Database Password: Generate a strong password (save this!)
- Region: Choose closest to your users
- Name:
- Click "Create new project"
- Wait 2-3 minutes for provisioning
Once your project is ready:
- Go to Settings → API
- Note down these values:
| Key | Description | Location |
|---|---|---|
| Project URL | Supabase API endpoint | "Project URL" section |
| anon/public key | Client-side key (safe to expose) | "Project API keys" → anon public |
| service_role key | Server-side key (SECRET) | "Project API keys" → service_role |
| JWT Secret | For signing tokens | "JWT Settings" → JWT Secret |
Create a .env.local file in the project root:
touch .env.localAdd the following variables:
# =============================================================================
# SUPABASE CONFIGURATION
# =============================================================================
# Public keys (safe for client-side)
NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
# Private keys (server-side only - NEVER expose these!)
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
SUPABASE_JWT_SECRET=your-jwt-secret-from-supabase-dashboardKeys should look like:
| Variable | Format |
|---|---|
| URL | https://abcdefghij.supabase.co (~30 chars) |
| anon key | eyJhbGciOiJI... (~200+ chars, starts with eyJ) |
| service_role key | eyJhbGciOiJI... (~200+ chars, starts with eyJ) |
| JWT Secret | Random string (~40+ chars) |
- Go to your Supabase Dashboard
- Click SQL Editor in the sidebar
- Click "New query"
- Open
supabase-schema.sqlfrom the project - Copy the entire contents and paste into the SQL Editor
- Click "Run" (or Cmd/Ctrl + Enter)
Expected output:
✅ Relay database schema created successfully!
Tables created:
- users
- auth_nonces
- friends
- transactions
- communities
- community_members
- activities
- activity_attendees
- comments
RLS policies enabled on all tables.
To enable community tokens on Polkadot Asset Hub:
- Open
supabase-community-tokens.sql - Run in SQL Editor
Expected output:
✅ Community tokens schema extension created successfully!
Table created:
- community_tokens
Run this SQL to enable the Polkadot Bazaar feature:
-- ============================================================================
-- KNOWN ASSETS TABLE
-- ============================================================================
CREATE TABLE IF NOT EXISTS known_assets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
asset_id INTEGER UNIQUE NOT NULL,
ticker TEXT NOT NULL,
decimals INTEGER NOT NULL DEFAULT 10,
symbol TEXT NOT NULL,
category TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_known_assets_id ON known_assets(asset_id);
CREATE INDEX IF NOT EXISTS idx_known_assets_category ON known_assets(category);
ALTER TABLE known_assets ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Anyone can read known assets" ON known_assets
FOR SELECT USING (true);
-- Insert initial known assets
INSERT INTO known_assets (asset_id, ticker, decimals, symbol, category) VALUES
(1984, 'USDt', 6, 'https://assets.coingecko.com/coins/images/325/small/Tether.png', 'stablecoin'),
(1337, 'USDC', 6, 'https://assets.coingecko.com/coins/images/6319/small/usdc.png', 'stablecoin'),
(30, 'DED', 10, 'https://raw.githubusercontent.com/nicpick/logos/main/DED-LOGO-EYE.png', 'meme'),
(18, 'DOTA', 4, 'https://raw.githubusercontent.com/nicpick/logos/main/dota.png', 'meme'),
(23, 'PINK', 10, 'https://raw.githubusercontent.com/nicpick/logos/main/pink.png', 'meme'),
(31337, 'WUD', 10, 'https://raw.githubusercontent.com/nicpick/logos/main/WUD.png', 'meme'),
(17, 'WIFD', 10, 'https://raw.githubusercontent.com/nicpick/logos/main/wifd.png', 'meme'),
(42069, 'STINK', 10, 'https://raw.githubusercontent.com/nicpick/logos/main/stink.png', 'meme'),
(1107, 'TSN', 18, 'https://raw.githubusercontent.com/nicpick/logos/main/tsn.png', 'utility'),
(50000111, 'DON', 10, 'https://raw.githubusercontent.com/nicpick/logos/main/don.png', 'utility'),
(21, 'vDOT', 10, 'https://raw.githubusercontent.com/nicpick/logos/main/vdot.png', 'bridged'),
(8, 'RMRK', 10, 'https://assets.coingecko.com/coins/images/15320/small/RMRK.png', 'bridged')
ON CONFLICT (asset_id) DO UPDATE SET
ticker = EXCLUDED.ticker,
decimals = EXCLUDED.decimals,
symbol = EXCLUDED.symbol,
category = EXCLUDED.category,
updated_at = NOW();- Go to Table Editor in the sidebar
- Verify these tables exist:
| Table | Purpose |
|---|---|
users |
User profiles |
auth_nonces |
Authentication nonces |
friends |
User friend lists |
transactions |
Transaction records |
communities |
Community data |
community_members |
Community membership |
activities |
Community activities |
activity_attendees |
Activity participation |
comments |
Activity comments |
community_tokens |
Community token configs |
known_assets |
Polkadot Asset Hub tokens |
npm run devOpen http://localhost:3000 in your browser.
# Build the application
npm run build
# Start production server
npm start| Script | Command | Description |
|---|---|---|
dev |
npm run dev |
Start development server |
build |
npm run build |
Build for production |
start |
npm start |
Start production server |
lint |
npm run lint |
Run ESLint |
test |
npm test |
Run all tests |
test:watch |
npm run test:watch |
Run tests in watch mode |
test:ui |
npm run test:ui |
Run tests with UI |
test:coverage |
npm run test:coverage |
Run tests with coverage |
Generate an authentication nonce for wallet signature.
Request:
{
"walletAddress": "1A2B3C4D..."
}Response:
{
"message": "Sign this message to authenticate with Relay...",
"nonce": "abc123..."
}Verify wallet signature and receive JWT token.
Request:
{
"walletAddress": "1A2B3C4D...",
"signature": "0x...",
"nonce": "abc123..."
}Response:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}| Method | Endpoint | Description |
|---|---|---|
| GET | /api/community |
List all communities |
| POST | /api/community/create |
Create new community |
| GET | /api/community/search?q=term |
Search communities |
| POST | /api/community/join |
Join a community |
| POST | /api/community/leave |
Leave a community |
| GET | /api/community/members |
Get community members |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/activity/join |
Join an activity |
| POST | /api/activity/leave |
Leave an activity |
| POST | /api/activity/like |
Like an activity |
| POST | /api/activity/comment |
Add a comment |
| GET | /api/activity/comments |
Get activity comments |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/user/profile |
Get user profile |
| GET | /api/user/nicknames |
Get nicknames for addresses |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/friends |
List all friends |
| POST | /api/friends |
Add a friend |
| PUT | /api/friends/[walletAddress] |
Update friend |
| DELETE | /api/friends/[walletAddress] |
Remove friend |
npm test# Unit tests
npm run test:unit
# API tests
npm run test:api
# Component tests
npm run test:componentsnpm run test:watchnpm run test:coveragenpm run test:ui- Push your code to GitHub
- Go to Vercel
- Import your repository
- Configure environment variables:
NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEYSUPABASE_SERVICE_ROLE_KEYSUPABASE_JWT_SECRET
- Deploy
Ensure all environment variables are set in your deployment platform:
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_ROLE_KEY=eyJ...
SUPABASE_JWT_SECRET=your-secret- Cause: Calling auth without a wallet
- Fix: Ensure user has created/imported a wallet first
- Cause: Nonce expired (>5 min) or already used
- Fix: Request a new nonce and try again
- Cause: Message was modified or wrong key used
- Fix: Ensure the exact message from server is signed
- Cause: RLS policy blocking access
- Fix: Check that JWT contains correct
wallet_address
- Cause: Token older than 24 hours
- Fix: Call
refreshAuth()to get new token
-
Check browser console for detailed error messages
-
Verify environment variables:
echo $NEXT_PUBLIC_SUPABASE_URL
-
Check Supabase logs:
- Go to Dashboard → Logs → Postgres
-
Test RLS policies in SQL Editor:
SELECT auth.jwt();
-
View table data with RLS disabled:
- Use Table Editor with gear icon
If you need to start fresh:
- Go to SQL Editor
- Uncomment the DROP statements at the top of
supabase-schema.sql:DROP TABLE IF EXISTS auth_nonces CASCADE; DROP TABLE IF EXISTS comments CASCADE; DROP TABLE IF EXISTS activity_attendees CASCADE; DROP TABLE IF EXISTS activities CASCADE; DROP TABLE IF EXISTS community_members CASCADE; DROP TABLE IF EXISTS communities CASCADE; DROP TABLE IF EXISTS transactions CASCADE; DROP TABLE IF EXISTS friends CASCADE; DROP TABLE IF EXISTS users CASCADE;
- Run the drops
- Re-run the full schema
-
Never expose service role key
- It bypasses all RLS policies
- Only use in server-side code
-
Keep JWT Secret secure
- Anyone with it can forge tokens
- Rotate if compromised
-
Nonces are one-time use
- Prevents replay attacks
- Automatically cleaned up
-
Signatures prove ownership
- No password needed
- Private key never leaves device
-
RLS enforces access control
- Database-level security
- Even if client is compromised
| Variable | Exposure | Notes |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Public | Safe to expose |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Public | Safe to expose |
SUPABASE_SERVICE_ROLE_KEY |
Secret | Server-only |
SUPABASE_JWT_SECRET |
Secret | Server-only |
- Fork the repository
- Create a feature branch
- Make your changes
- Run tests:
npm test - Run lint:
npm run lint - Submit a pull request
This project is open source. See the LICENSE file for details.
For questions or issues:
- Open a GitHub issue
- Check the Supabase documentation
- Check the Polkadot documentation