INTEGRATION GUIDE - nself-org/nchat GitHub Wiki
Version: Ι³Chat v0.9.1 Date: 2026-02-03
This guide explains how to integrate Ι³Plugins with the Next.js frontend. Each plugin requires:
- Environment Variables - Frontend configuration
- API Routes - Proxy endpoints to plugin services
- Service Layer - TypeScript services wrapping API calls
- React Hooks - Custom hooks for components
- Components - UI components using the hooks
Component
β (uses)
React Hook
β (calls)
Service Layer
β (hits)
API Route (/app/api/*)
β (proxies to)
Plugin Service (Docker container)
File: /.env.local
# Realtime Plugin
NEXT_PUBLIC_REALTIME_URL=http://realtime.localhost:3101
NEXT_PUBLIC_REALTIME_WS_URL=ws://realtime.localhost:3101File: /src/app/api/realtime/route.ts
import { NextRequest, NextResponse } from 'next/server'
const REALTIME_URL = process.env.NEXT_PUBLIC_REALTIME_URL || 'http://realtime.localhost:3101'
export async function GET(request: NextRequest) {
try {
const response = await fetch(`${REALTIME_URL}/health`)
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
return NextResponse.json({ error: 'Realtime service unavailable' }, { status: 503 })
}
}File: /src/app/api/realtime/presence/route.ts
import { NextRequest, NextResponse } from 'next/server'
const REALTIME_URL = process.env.NEXT_PUBLIC_REALTIME_URL
export async function GET(request: NextRequest) {
const channelId = request.nextUrl.searchParams.get('channelId')
try {
const response = await fetch(`${REALTIME_URL}/presence/${channelId}`)
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
return NextResponse.json({ error: 'Failed to fetch presence' }, { status: 500 })
}
}
export async function POST(request: NextRequest) {
const body = await request.json()
const { channelId, userId, status } = body
try {
const response = await fetch(`${REALTIME_URL}/presence/${channelId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, status }),
})
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
return NextResponse.json({ error: 'Failed to update presence' }, { status: 500 })
}
}File: /src/services/realtime/realtime-client.ts
Update to use plugin:
import { io, Socket } from 'socket.io-client'
const REALTIME_WS_URL = process.env.NEXT_PUBLIC_REALTIME_WS_URL || 'ws://realtime.localhost:3101'
export class RealtimeClient {
private socket: Socket | null = null
connect(userId: string, token: string) {
this.socket = io(REALTIME_WS_URL, {
auth: { userId, token },
transports: ['websocket'],
reconnection: true,
reconnectionDelay: 1000,
reconnectionAttempts: 5,
})
this.socket.on('connect', () => {
console.log('[Realtime] Connected to plugin')
})
this.socket.on('disconnect', () => {
console.log('[Realtime] Disconnected from plugin')
})
return this.socket
}
disconnect() {
if (this.socket) {
this.socket.disconnect()
this.socket = null
}
}
joinChannel(channelId: string) {
this.socket?.emit('channel:join', { channelId })
}
leaveChannel(channelId: string) {
this.socket?.emit('channel:leave', { channelId })
}
sendTyping(channelId: string, isTyping: boolean) {
this.socket?.emit('typing', { channelId, isTyping })
}
updatePresence(status: 'online' | 'away' | 'dnd' | 'offline') {
this.socket?.emit('presence:update', { status })
}
}
export const realtimeClient = new RealtimeClient()File: /src/hooks/use-realtime.ts
import { useEffect, useRef } from 'react'
import { realtimeClient } from '@/services/realtime/realtime-client'
import { useAuth } from '@/contexts/auth-context'
export function useRealtime() {
const { user, session } = useAuth()
const isConnected = useRef(false)
useEffect(() => {
if (user && session?.accessToken && !isConnected.current) {
realtimeClient.connect(user.id, session.accessToken)
isConnected.current = true
}
return () => {
if (isConnected.current) {
realtimeClient.disconnect()
isConnected.current = false
}
}
}, [user, session])
return {
joinChannel: realtimeClient.joinChannel.bind(realtimeClient),
leaveChannel: realtimeClient.leaveChannel.bind(realtimeClient),
sendTyping: realtimeClient.sendTyping.bind(realtimeClient),
updatePresence: realtimeClient.updatePresence.bind(realtimeClient),
}
}NEXT_PUBLIC_NOTIFICATIONS_URL=http://notifications.localhost:3102File: /src/app/api/notifications/send/route.ts
import { NextRequest, NextResponse } from 'next/server'
const NOTIFICATIONS_URL = process.env.NEXT_PUBLIC_NOTIFICATIONS_URL
export async function POST(request: NextRequest) {
const body = await request.json()
try {
const response = await fetch(`${NOTIFICATIONS_URL}/send`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
return NextResponse.json({ error: 'Failed to send notification' }, { status: 500 })
}
}File: /src/services/notifications/notification.service.ts
Update to use plugin:
export class NotificationService {
private baseUrl =
process.env.NEXT_PUBLIC_NOTIFICATIONS_URL || 'http://notifications.localhost:3102'
async sendNotification(notification: NotificationPayload) {
const response = await fetch('/api/notifications/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(notification),
})
return response.json()
}
async subscribeToEmailDigest(userId: string, frequency: 'daily' | 'weekly') {
const response = await fetch('/api/notifications/preferences', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, emailDigest: { enabled: true, frequency } }),
})
return response.json()
}
}NEXT_PUBLIC_JOBS_URL=http://jobs.localhost:3105
NEXT_PUBLIC_BULLMQ_DASHBOARD_URL=http://queues.localhost:4200File: /src/app/api/jobs/schedule/route.ts
import { NextRequest, NextResponse } from 'next/server'
const JOBS_URL = process.env.NEXT_PUBLIC_JOBS_URL
export async function POST(request: NextRequest) {
const body = await request.json()
const { type, payload, runAt } = body
try {
const response = await fetch(`${JOBS_URL}/schedule`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type, payload, runAt }),
})
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
return NextResponse.json({ error: 'Failed to schedule job' }, { status: 500 })
}
}File: /src/services/jobs/queue.service.ts
Update to use plugin:
export class JobQueueService {
private baseUrl = process.env.NEXT_PUBLIC_JOBS_URL || 'http://jobs.localhost:3105'
async scheduleJob(job: JobPayload) {
const response = await fetch('/api/jobs/schedule', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(job),
})
return response.json()
}
async getJobStatus(jobId: string) {
const response = await fetch(`/api/jobs/${jobId}`)
return response.json()
}
async cancelJob(jobId: string) {
const response = await fetch(`/api/jobs/${jobId}`, { method: 'DELETE' })
return response.json()
}
}NEXT_PUBLIC_FILE_PROCESSING_URL=http://files.localhost:3104File: /src/app/api/files/process/route.ts
import { NextRequest, NextResponse } from 'next/server'
const FILE_PROCESSING_URL = process.env.NEXT_PUBLIC_FILE_PROCESSING_URL
export async function POST(request: NextRequest) {
const formData = await request.formData()
try {
const response = await fetch(`${FILE_PROCESSING_URL}/process`, {
method: 'POST',
body: formData,
})
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
return NextResponse.json({ error: 'Failed to process file' }, { status: 500 })
}
}File: /src/services/files/upload.service.ts
Update to use plugin:
export class FileUploadService {
private baseUrl = process.env.NEXT_PUBLIC_FILE_PROCESSING_URL || 'http://files.localhost:3104'
async uploadAndProcess(file: File, options?: ProcessingOptions) {
const formData = new FormData()
formData.append('file', file)
if (options) {
formData.append('options', JSON.stringify(options))
}
const response = await fetch('/api/files/process', {
method: 'POST',
body: formData,
})
return response.json()
}
async generateThumbnail(fileId: string) {
const response = await fetch(`/api/files/${fileId}/thumbnail`, {
method: 'POST',
})
return response.json()
}
}NEXT_PUBLIC_IDME_ENABLED=true
IDME_CLIENT_ID=your_client_id
IDME_CLIENT_SECRET=your_client_secretFile: /src/app/api/auth/oauth/idme/route.ts
import { NextRequest, NextResponse } from 'next/server'
const IDME_CLIENT_ID = process.env.IDME_CLIENT_ID
const REDIRECT_URI = `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/oauth/callback`
export async function GET(request: NextRequest) {
const state = crypto.randomUUID()
const authUrl = new URL('https://api.id.me/oauth/authorize')
authUrl.searchParams.set('client_id', IDME_CLIENT_ID!)
authUrl.searchParams.set('redirect_uri', REDIRECT_URI)
authUrl.searchParams.set('response_type', 'code')
authUrl.searchParams.set('scope', 'openid profile email')
authUrl.searchParams.set('state', state)
return NextResponse.redirect(authUrl.toString())
}File: /src/config/auth.config.ts
Update to include ID.me:
export const authProviders = [
// ... existing providers
{
id: 'idme',
name: 'ID.me',
enabled: process.env.NEXT_PUBLIC_IDME_ENABLED === 'true',
icon: 'Shield',
href: '/api/auth/oauth/idme',
description: 'Military, first responders, students, teachers',
},
]File: /src/lib/__tests__/plugin-health.test.ts
describe('Plugin Health Checks', () => {
it('should check realtime plugin health', async () => {
const response = await fetch('/api/realtime')
expect(response.ok).toBe(true)
const data = await response.json()
expect(data.status).toBe('healthy')
})
it('should check notifications plugin health', async () => {
const response = await fetch('/api/notifications/health')
expect(response.ok).toBe(true)
})
it('should check jobs plugin health', async () => {
const response = await fetch('/api/jobs/health')
expect(response.ok).toBe(true)
})
it('should check file-processing plugin health', async () => {
const response = await fetch('/api/files/health')
expect(response.ok).toBe(true)
})
})Add to /.env.local:
# Plugin URLs (Development)
NEXT_PUBLIC_REALTIME_URL=http://realtime.localhost:3101
NEXT_PUBLIC_REALTIME_WS_URL=ws://realtime.localhost:3101
NEXT_PUBLIC_NOTIFICATIONS_URL=http://notifications.localhost:3102
NEXT_PUBLIC_JOBS_URL=http://jobs.localhost:3105
NEXT_PUBLIC_FILE_PROCESSING_URL=http://files.localhost:3104
NEXT_PUBLIC_BULLMQ_DASHBOARD_URL=http://queues.localhost:4200
# Plugin Feature Flags
NEXT_PUBLIC_REALTIME_ENABLED=true
NEXT_PUBLIC_NOTIFICATIONS_ENABLED=true
NEXT_PUBLIC_JOBS_ENABLED=true
NEXT_PUBLIC_FILE_PROCESSING_ENABLED=true
NEXT_PUBLIC_IDME_ENABLED=false
# Plugin Secrets (from vault)
IDME_CLIENT_ID=${IDME_CLIENT_ID}
IDME_CLIENT_SECRET=${IDME_CLIENT_SECRET}- Environment variables added
- API routes created
- RealtimeClient updated
- Hooks wired up
- WebSocket connection tested
- Presence working
- Typing indicators working
- Environment variables added
- API routes created
- NotificationService updated
- Email notifications tested
- Push notifications configured
- Preferences working
- Environment variables added
- API routes created
- JobQueueService updated
- Scheduled jobs tested
- BullMQ dashboard accessible
- Background tasks running
- Environment variables added
- API routes created
- FileUploadService updated
- Image processing tested
- Video thumbnails working
- Storage integration working
- Environment variables added
- OAuth routes created
- Auth config updated
- Login button working
- Callback handling tested
- User profile synced
Use environment-specific URLs:
# Production
NEXT_PUBLIC_REALTIME_URL=https://realtime.nchat.io
NEXT_PUBLIC_REALTIME_WS_URL=wss://realtime.nchat.io
NEXT_PUBLIC_NOTIFICATIONS_URL=https://notifications.nchat.io
NEXT_PUBLIC_JOBS_URL=https://jobs.nchat.io
NEXT_PUBLIC_FILE_PROCESSING_URL=https://files.nchat.io- CORS: Configure plugin CORS to allow frontend domain
- Authentication: Pass JWT tokens to plugins
- Rate Limiting: Apply rate limits to public endpoints
- HTTPS Only: Production must use HTTPS
- Secrets: Use environment-specific secrets
- β
Copy environment variables to
.env.local - β Create API routes for each plugin
- β Update service layer to use plugins
- β Test health endpoints
- β Run integration tests
- β Update components to use integrated services
- β Document completion in PROGRESS.md
β All plugin environment variables configured β All API routes created and tested β All service layers updated β All health checks passing β Integration tests passing β Components using plugin services β Documentation complete