Next.js Integration Guide
Complete guide for integrating Loyalteez with Next.js applications, including deployment to Cloudflare Pages, Vercel, and other platforms.
🎯 What You'll Build: A Next.js SaaS application with crypto rewards for user actions (newsletter signup, profile completion, subscription upgrades).
Live Demo: saas-demo.loyalteez.app
Source Code: GitHub Repository
Quick Start
Time: 15 minutes
Difficulty: ⭐⭐⭐ Easy
1. Install Dependencies
npm install next react react-dom
2. Configure Environment
Create .env.local:
NEXT_PUBLIC_BRAND_ID=0xYourBrandWalletAddress
3. Track Events
// In your component
const res = await fetch('https://api.loyalteez.app/loyalteez-api/manual-event', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
brandId: process.env.NEXT_PUBLIC_BRAND_ID,
eventType: 'newsletter_subscribe',
userEmail: email,
userIdentifier: email,
domain: 'yourdomain.com',
sourceUrl: 'https://yourdomain.com',
}),
});
Integration Patterns
Pattern 1: Client-Side Direct API (Recommended) ⭐
Best for: Next.js on Cloudflare Pages, Vercel, or any static hosting
Why: Simple, reliable, no server-side complexity
Example:
'use client';
import { useState } from 'react';
export default function NewsletterForm() {
const [email, setEmail] = useState('');
const [status, setStatus] = useState<'idle' | 'loading' | 'success'>('idle');
const handleSubscribe = async (e: React.FormEvent) => {
e.preventDefault();
setStatus('loading');
try {
const res = await fetch('https://api.loyalteez.app/loyalteez-api/manual-event', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
brandId: process.env.NEXT_PUBLIC_BRAND_ID?.toLowerCase(),
eventType: 'newsletter_subscribe',
userEmail: email,
userIdentifier: email,
domain: window.location.hostname,
sourceUrl: window.location.href,
}),
});
const data = await res.json();
if (res.ok && data.success) {
setStatus('success');
}
} catch (err) {
console.error('Subscription failed:', err);
}
};
return (
<form onSubmit={handleSubscribe}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<button disabled={status === 'loading'}>
{status === 'loading' ? 'Subscribing...' : 'Subscribe'}
</button>
</form>
);
}
Advantages:
- ✅ Works on all hosting platforms
- ✅ No server-side code needed
- ✅ Simple error handling
- ✅ Brand ID is public anyway (wallet address)
Pattern 2: Server-Side API Route (Optional)
Best for: When you need to hide Brand ID or add server-side validation
⚠️ Important: Edge Runtime API routes don't work well on Cloudflare Pages. Use Node.js runtime instead.
Example:
// app/api/track-event/route.ts
import { NextResponse } from 'next/server';
export const runtime = 'nodejs'; // NOT 'edge' for Cloudflare Pages
export async function POST(request: Request) {
const { eventType, userEmail, metadata } = await request.json();
const res = await fetch('https://api.loyalteez.app/loyalteez-api/manual-event', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
brandId: process.env.BRAND_ID, // Server-side only
eventType,
userEmail,
userIdentifier: userEmail,
domain: 'yourdomain.com',
sourceUrl: 'https://yourdomain.com',
metadata,
}),
});
const data = await res.json();
return NextResponse.json(data);
}
Client-side call:
const res = await fetch('/api/track-event', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
eventType: 'newsletter_subscribe',
userEmail: email,
}),
});
Cloudflare Pages Deployment
Configuration
1. Set Environment Variables:
In Cloudflare Pages Dashboard → Settings → Environment Variables:
NEXT_PUBLIC_BRAND_ID=0xYourBrandWalletAddress
2. Configure Build:
- Build command:
npx @cloudflare/next-on-pages@1 - Build output directory:
.vercel/output/static - Root directory: (leave empty)
3. Add wrangler.toml:
name = "your-app-name"
compatibility_date = "2024-04-05"
compatibility_flags = ["nodejs_compat"]
pages_build_output_dir = ".vercel/output/static"
4. Update next.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
// No special config needed for Cloudflare Pages
};
module.exports = nextConfig;
Important Notes
- ❌ Don't use Edge Runtime for API routes on Cloudflare Pages
- ✅ Use client-side direct API calls instead (Pattern 1)
- ✅ Brand ID must be lowercase (
brandId.toLowerCase()) - ✅ Domain must be whitelisted in Partner Portal → Settings → Domain Configuration
Error Handling Best Practices
1. Timeout Handling
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 seconds
try {
const res = await fetch('https://api.loyalteez.app/loyalteez-api/manual-event', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: controller.signal,
});
// ... handle response
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') {
// Handle timeout
}
} finally {
clearTimeout(timeoutId);
}
2. CORS Error Handling
try {
const res = await fetch('https://api.loyalteez.app/loyalteez-api/manual-event', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || `HTTP ${res.status}`);
}
} catch (err) {
if (err instanceof TypeError && err.message.includes('fetch')) {
// Network/CORS error
console.error('Network error - check CORS configuration');
}
}
3. Response Validation
const contentType = res.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
const text = await res.text();
throw new Error(`Invalid response: ${text}`);
}
const data = await res.json();
if (!data.success) {
throw new Error(data.error || 'Request failed');
}
Brand ID Format
Important: Brand ID must be a valid Ethereum address and lowercase.
// ✅ Correct
const brandId = process.env.NEXT_PUBLIC_BRAND_ID?.toLowerCase();
// Example: "0x5242b6db88a72752ac5a54cfe6a7db8244d743c9"
// ❌ Wrong
const brandId = process.env.NEXT_PUBLIC_BRAND_ID; // May have uppercase
Validation:
if (!brandId || !brandId.startsWith('0x') || brandId.length !== 42) {
throw new Error('Invalid Brand ID format');
}
Domain Configuration
Required: Add your domain to Partner Portal before deploying.
- Log into Partner Portal
- Navigate to Settings → Domain Configuration
- Add your domain:
yourdomain.com - Save configuration
Why: The API validates that requests come from authorized domains for security.
Complete Example: Newsletter Signup
'use client';
import { useState } from 'react';
export default function NewsletterForm() {
const [email, setEmail] = useState('');
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
const [reward, setReward] = useState<number | null>(null);
const [errorMessage, setErrorMessage] = useState<string>('');
const handleSubscribe = async (e: React.FormEvent) => {
e.preventDefault();
setStatus('loading');
setErrorMessage('');
try {
const brandId = process.env.NEXT_PUBLIC_BRAND_ID?.toLowerCase();
if (!brandId || !brandId.startsWith('0x') || brandId.length !== 42) {
throw new Error('Invalid Brand ID configuration');
}
const payload = {
brandId,
eventType: 'newsletter_subscribe',
userEmail: email,
userIdentifier: email,
domain: window.location.hostname,
sourceUrl: window.location.href,
metadata: {
source: 'newsletter_form',
timestamp: new Date().toISOString(),
},
};
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000);
const res = await fetch('https://api.loyalteez.app/loyalteez-api/manual-event', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: controller.signal,
});
clearTimeout(timeoutId);
const contentType = res.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
throw new Error(`Server error: ${res.status}`);
}
const data = await res.json();
if (res.ok && data.success) {
setStatus('success');
setReward(data.ltzDistributed || data.rewardAmount || 25);
} else {
setStatus('error');
setErrorMessage(data.error || 'Subscription failed');
}
} catch (err) {
setStatus('error');
if (err instanceof DOMException && err.name === 'AbortError') {
setErrorMessage('Request timed out. Please try again.');
} else if (err instanceof TypeError) {
setErrorMessage('Network error. Please check your connection.');
} else {
setErrorMessage(err instanceof Error ? err.message : 'An error occurred');
}
}
};
return (
<form onSubmit={handleSubscribe}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
disabled={status === 'success'}
/>
<button disabled={status === 'loading' || status === 'success'}>
{status === 'loading' ? 'Subscribing...' : status === 'success' ? 'Subscribed!' : 'Subscribe'}
</button>
{status === 'success' && reward && (
<p>✅ You earned {reward} LTZ points!</p>
)}
{status === 'error' && (
<p className="error">{errorMessage}</p>
)}
</form>
);
}
Common Event Types
| Event Type | Default Reward | Use Case |
|---|---|---|
newsletter_subscribe | 25 LTZ | Newsletter signup |
account_creation | 100 LTZ | User registration |
profile_completed | 50 LTZ | Profile completion |
subscription_upgrade | 200 LTZ | Paid plan upgrade |
form_submit | 10 LTZ | Generic form submission |
complete_survey | 75 LTZ | Survey completion |
Troubleshooting
CORS Errors
Error: Access to fetch has been blocked by CORS policy
Solution:
- Ensure your domain is added in Partner Portal → Settings → Domain Configuration
- Contact support to whitelist your domain in the API CORS configuration
Connection Reset
Error: ERR_CONNECTION_RESET or Failed to fetch
Solutions:
- Verify Brand ID format (must be lowercase Ethereum address)
- Check domain is configured in Partner Portal
- Add timeout handling (see Error Handling section)
- Verify network connectivity
Invalid Event Data
Error: Invalid event data or Missing required field
Solution:
- Ensure
brandId,eventType, anduserEmailare provided - Verify
brandIdis lowercase:brandId.toLowerCase() - Check
domainandsourceUrlare valid URLs
Related Documentation
Next Steps
- ✅ Set up environment variables
- ✅ Add domain to Partner Portal
- ✅ Implement event tracking
- ✅ Test with real users
- ✅ Deploy to production
Need Help? Join our Discord or email [email protected]