Skip to content

Commit 76e05d8

Browse files
committed
feat: Implement Realtime Service with Pusher and SSE support
- Introduced a new Realtime module with support for Pusher and Server-Sent Events (SSE). - Created a RealtimeService class to manage provider selection and connection. - Added PusherProvider and SSEProvider classes for handling real-time events. - Implemented configuration utilities for detecting and creating realtime configurations. - Updated the existing SSE utilities to use the new server-realtime service. - Refactored the realtime store to utilize the new RealtimeService for managing connections and subscriptions. - Added necessary types and interfaces for the realtime functionality. - Updated package.json to include Pusher dependencies.
1 parent f106a64 commit 76e05d8

File tree

17 files changed

+1438
-100
lines changed

17 files changed

+1438
-100
lines changed

.env.example

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,32 @@ POSTGRES_URL="postgresql://username:password@host:5432/database"
169169
## ======== APPLICATION SETTINGS ========
170170
NODE_ENV="development"
171171

172+
## ======== REALTIME UPDATES CONFIGURATION ========
173+
174+
# Realtime provider selection (auto-detected if not specified)
175+
# Options: "sse" (Server-Sent Events) or "pusher" (Pusher)
176+
# Auto-detection: Uses Pusher for serverless deployments (Vercel/Netlify) if configured, otherwise SSE
177+
# NEXT_PUBLIC_REALTIME_PROVIDER="auto"
178+
179+
# Server-Sent Events configuration (default for non-serverless deployments)
180+
# NEXT_PUBLIC_SSE_ENDPOINT="/api/events"
181+
# NEXT_PUBLIC_SSE_RECONNECT_INTERVAL="3000"
182+
183+
# Pusher configuration (recommended for serverless deployments)
184+
# Get these from https://pusher.com/channels
185+
# PUSHER_APP_ID="your-app-id"
186+
# NEXT_PUBLIC_PUSHER_KEY="your-pusher-key"
187+
# PUSHER_SECRET="your-pusher-secret"
188+
# NEXT_PUBLIC_PUSHER_CLUSTER="us2"
189+
# PUSHER_USE_TLS="true"
190+
191+
# Example Pusher setup for production:
192+
# PUSHER_APP_ID="123456"
193+
# NEXT_PUBLIC_PUSHER_KEY="abcdef123456"
194+
# PUSHER_SECRET="your-secret-key"
195+
# NEXT_PUBLIC_PUSHER_CLUSTER="us2"
196+
# PUSHER_USE_TLS="true"
197+
172198
## ======== VERCEL-SPECIFIC CONFIGURATION ========
173199
# Vercel Postgres (automatically provided by Vercel)
174200
# The system automatically prefers POSTGRES_URL_NON_POOLING over POSTGRES_URL to avoid SASL authentication issues

packages/web/REALTIME.md

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
# Realtime Updates System
2+
3+
This document describes the optimized realtime update system that supports both Server-Sent Events (SSE) and Pusher for different deployment environments.
4+
5+
## Overview
6+
7+
The realtime system automatically chooses the best transport method based on your deployment environment:
8+
9+
- **SSE (Server-Sent Events)**: Default for traditional deployments (Docker, self-hosted)
10+
- **Pusher**: Automatically selected for serverless deployments (Vercel, Netlify) when configured
11+
12+
## Architecture
13+
14+
### Client-Side Components
15+
16+
- `RealtimeService`: Main service that manages provider selection and operation
17+
- `SSEProvider`: Handles Server-Sent Events connections
18+
- `PusherProvider`: Handles Pusher Channels connections
19+
- `useRealtime()`: React hook for easy integration
20+
21+
### Server-Side Components
22+
23+
- `ServerRealtimeService`: Broadcasts messages to both SSE and Pusher
24+
- Backward-compatible with existing `broadcastUpdate()` function
25+
26+
## Configuration
27+
28+
### Environment Variables
29+
30+
```bash
31+
# Optional: Force a specific provider (auto-detected if not set)
32+
NEXT_PUBLIC_REALTIME_PROVIDER="auto" # "sse", "pusher", or "auto"
33+
34+
# SSE Configuration (used by default for non-serverless)
35+
NEXT_PUBLIC_SSE_ENDPOINT="/api/events"
36+
NEXT_PUBLIC_SSE_RECONNECT_INTERVAL="3000"
37+
38+
# Pusher Configuration (required for Pusher support)
39+
PUSHER_APP_ID="your-app-id"
40+
NEXT_PUBLIC_PUSHER_KEY="your-pusher-key"
41+
PUSHER_SECRET="your-pusher-secret"
42+
NEXT_PUBLIC_PUSHER_CLUSTER="us2"
43+
PUSHER_USE_TLS="true"
44+
```
45+
46+
### Auto-Detection Logic
47+
48+
1. Check `NEXT_PUBLIC_REALTIME_PROVIDER` for explicit preference
49+
2. Detect if running on Vercel (`VERCEL=1`) or Netlify
50+
3. Check if Pusher is properly configured
51+
4. Use Pusher for serverless + configured, otherwise SSE
52+
53+
## Usage
54+
55+
### Basic React Hook
56+
57+
```typescript
58+
import { useRealtime } from '@/hooks/use-realtime';
59+
60+
function MyComponent() {
61+
const { connected, providerType, subscribe } = useRealtime();
62+
63+
useEffect(() => {
64+
const unsubscribe = subscribe('devlog-updated', (devlog) => {
65+
console.log('Devlog updated:', devlog);
66+
});
67+
68+
return unsubscribe;
69+
}, [subscribe]);
70+
71+
return (
72+
<div>
73+
Status: {connected ? 'Connected' : 'Disconnected'}
74+
({providerType})
75+
</div>
76+
);
77+
}
78+
```
79+
80+
### Event-Specific Hooks
81+
82+
```typescript
83+
import { useDevlogEvents } from '@/hooks/use-realtime';
84+
85+
function DevlogList() {
86+
const { onDevlogCreated, onDevlogUpdated } = useDevlogEvents();
87+
88+
useEffect(() => {
89+
const unsubscribeCreated = onDevlogCreated((devlog) => {
90+
// Handle new devlog
91+
});
92+
93+
const unsubscribeUpdated = onDevlogUpdated((devlog) => {
94+
// Handle updated devlog
95+
});
96+
97+
return () => {
98+
unsubscribeCreated();
99+
unsubscribeUpdated();
100+
};
101+
}, [onDevlogCreated, onDevlogUpdated]);
102+
}
103+
```
104+
105+
### Server-Side Broadcasting
106+
107+
```typescript
108+
import { serverRealtimeService } from '@/lib/api/server-realtime';
109+
110+
// In your API route or server action
111+
await serverRealtimeService.broadcastDevlogCreated(newDevlog);
112+
113+
// Or use the generic broadcast method
114+
await serverRealtimeService.broadcast('custom-event', data);
115+
```
116+
117+
### Status Components
118+
119+
```typescript
120+
import { RealtimeStatus, RealtimeDebugInfo } from '@/components/realtime/realtime-status';
121+
122+
// Minimal status indicator
123+
<RealtimeStatus />
124+
125+
// Detailed status (development only)
126+
<RealtimeDebugInfo />
127+
```
128+
129+
## Event Types
130+
131+
The system supports the following standard events:
132+
133+
- `project-created`
134+
- `project-updated`
135+
- `project-deleted`
136+
- `devlog-created`
137+
- `devlog-updated`
138+
- `devlog-deleted`
139+
- `devlog-note-created`
140+
- `devlog-note-updated`
141+
- `devlog-note-deleted`
142+
143+
## Deployment Scenarios
144+
145+
### Traditional Docker Deployment
146+
147+
Uses SSE by default. No additional configuration needed.
148+
149+
```bash
150+
# .env
151+
# No realtime configuration needed - SSE works out of the box
152+
```
153+
154+
### Vercel Deployment
155+
156+
1. Sign up for Pusher at https://pusher.com/channels
157+
2. Create a new app and get your credentials
158+
3. Add to Vercel environment variables:
159+
160+
```bash
161+
PUSHER_APP_ID="123456"
162+
NEXT_PUBLIC_PUSHER_KEY="abcdef123456"
163+
PUSHER_SECRET="your-secret-key"
164+
NEXT_PUBLIC_PUSHER_CLUSTER="us2"
165+
```
166+
167+
The system will automatically detect Vercel and use Pusher.
168+
169+
### Local Development
170+
171+
SSE is used by default. To test Pusher locally:
172+
173+
```bash
174+
# .env.local
175+
NEXT_PUBLIC_REALTIME_PROVIDER="pusher"
176+
PUSHER_APP_ID="123456"
177+
NEXT_PUBLIC_PUSHER_KEY="abcdef123456"
178+
PUSHER_SECRET="your-secret-key"
179+
NEXT_PUBLIC_PUSHER_CLUSTER="us2"
180+
```
181+
182+
## Migration from Previous System
183+
184+
The new system is backward compatible. Existing code using `useRealtimeStore` will continue to work without changes.
185+
186+
### Old API (still supported)
187+
```typescript
188+
const { connected, subscribe } = useRealtimeStore();
189+
```
190+
191+
### New API (recommended)
192+
```typescript
193+
const { connected, subscribe } = useRealtime();
194+
```
195+
196+
## Troubleshooting
197+
198+
### Debug Information
199+
200+
Enable debug mode in development:
201+
202+
```typescript
203+
import { logRealtimeConfig } from '@/lib/realtime';
204+
205+
// Log current configuration
206+
logRealtimeConfig();
207+
```
208+
209+
### Common Issues
210+
211+
1. **Pusher not connecting**: Check that all environment variables are set correctly
212+
2. **SSE disconnecting**: Verify your deployment supports long-running connections
213+
3. **No events received**: Ensure server-side code is using `serverRealtimeService.broadcast()`
214+
215+
### Testing
216+
217+
You can test the system by checking the browser console and network tab:
218+
219+
- SSE: Look for `/api/events` connection in Network tab
220+
- Pusher: Look for WebSocket connections to `ws-*.pusherapp.com`
221+
222+
## Performance Considerations
223+
224+
- **SSE**: Lower latency, uses one HTTP connection per client
225+
- **Pusher**: Higher latency but better for serverless, uses WebSockets
226+
- Both providers include automatic reconnection logic
227+
- Connection state is managed automatically
228+
229+
## Security
230+
231+
- Pusher keys marked as `NEXT_PUBLIC_*` are safe for client-side use
232+
- `PUSHER_SECRET` must be kept server-side only
233+
- SSE endpoints should validate client permissions as needed

packages/web/app/api/projects/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
createSuccessResponse,
88
SSEEventType,
99
} from '@/lib';
10+
import { RealtimeEventType } from '@/lib/realtime';
1011

1112
// Mark this route as dynamic to prevent static generation
1213
export const dynamic = 'force-dynamic';
@@ -48,7 +49,7 @@ export async function POST(request: NextRequest) {
4849

4950
return createSuccessResponse(createdProject, {
5051
status: 201,
51-
sseEventType: SSEEventType.PROJECT_CREATED,
52+
sseEventType: RealtimeEventType.PROJECT_CREATED,
5253
});
5354
} catch (error) {
5455
console.error('Error creating project:', error);
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* Realtime status component for debugging and monitoring
3+
*/
4+
5+
'use client';
6+
7+
import { useRealtime } from '@/hooks/use-realtime';
8+
import { Badge } from '@/components/ui/badge';
9+
import { cn } from '@/lib/utils';
10+
11+
interface RealtimeStatusProps {
12+
className?: string;
13+
showProvider?: boolean;
14+
variant?: 'minimal' | 'detailed';
15+
}
16+
17+
export function RealtimeStatus({
18+
className,
19+
showProvider = true,
20+
variant = 'minimal'
21+
}: RealtimeStatusProps) {
22+
const { connected, providerType } = useRealtime();
23+
24+
if (variant === 'minimal') {
25+
return (
26+
<div className={cn('flex items-center gap-2', className)}>
27+
<div
28+
className={cn(
29+
'h-2 w-2 rounded-full',
30+
connected ? 'bg-green-500' : 'bg-red-500'
31+
)}
32+
/>
33+
{showProvider && providerType && (
34+
<Badge variant="outline" className="text-xs">
35+
{providerType.toUpperCase()}
36+
</Badge>
37+
)}
38+
</div>
39+
);
40+
}
41+
42+
return (
43+
<div className={cn('flex items-center gap-2 text-sm', className)}>
44+
<div
45+
className={cn(
46+
'h-2 w-2 rounded-full',
47+
connected ? 'bg-green-500' : 'bg-red-500'
48+
)}
49+
/>
50+
<span className="text-muted-foreground">
51+
{connected ? 'Connected' : 'Disconnected'}
52+
</span>
53+
{showProvider && providerType && (
54+
<Badge variant="outline" className="text-xs">
55+
{providerType.toUpperCase()}
56+
</Badge>
57+
)}
58+
</div>
59+
);
60+
}
61+
62+
/**
63+
* Debug component that shows detailed realtime information
64+
*/
65+
export function RealtimeDebugInfo({ className }: { className?: string }) {
66+
const { connected, providerType } = useRealtime();
67+
68+
if (process.env.NODE_ENV !== 'development') {
69+
return null;
70+
}
71+
72+
return (
73+
<div className={cn('rounded-lg border p-4 space-y-2', className)}>
74+
<h3 className="text-sm font-medium">Realtime Debug Info</h3>
75+
<div className="space-y-1 text-xs text-muted-foreground">
76+
<div>Status: {connected ? '🟢 Connected' : '🔴 Disconnected'}</div>
77+
<div>Provider: {providerType || 'Unknown'}</div>
78+
<div>Environment: {process.env.NODE_ENV}</div>
79+
<div>
80+
Vercel: {process.env.NEXT_PUBLIC_VERCEL === '1' ? 'Yes' : 'No'}
81+
</div>
82+
<div>
83+
Pusher Configured: {
84+
process.env.NEXT_PUBLIC_PUSHER_KEY && process.env.NEXT_PUBLIC_PUSHER_CLUSTER
85+
? 'Yes'
86+
: 'No'
87+
}
88+
</div>
89+
</div>
90+
</div>
91+
);
92+
}

0 commit comments

Comments
 (0)