Get startedNext.js
Quickstart Example
Guiding you through the process of using Erebus.
Quickstart Example - Publish and Subscribe to a channel with next.js using the React SDK (Unstable)
To start using Erebus you only need three things:
- An API endpoint that creates tokens for your clients using your API keys
- A client file where you declare your channels and their types
- A page that subscribes to the channel
First create an API endpoint that issues a token for the client using your API keys.
import { ErebusService, Access } from "@erebus-sh/sdk/service";
import { createRouteHandler } from "@erebus-sh/sdk/server/next";
import { cookies } from "next/headers";
export const { POST } = createRouteHandler({
authorize: async (channel, ctx) => {
// Get user ID from cookies or headers
const ckis = await cookies();
const userId = ckis.get("x-User-Id")?.value;
if (!userId) {
throw new Error("Missing user id");
}
// Create a new service instance
const service = new ErebusService({
// Replace with your own secret_api_key
secret_api_key: "dv-er-4o7j90qw39p96bra19fa94prupp6vdcg9axrd3hg4hqy68c1",
});
// Prepare a session for the user id
const session = await service.prepareSession({
userId,
});
// Allow one single channel for the user
session.join(channel);
// Allow one single topic or multiple topics for the user up to 64 topics
// Give it Read or Write or ReadWrite access
session.allow("rm_123", Access.ReadWrite);
// Return the session instance
return session;
},
fireWebhook: async (webHookMessage) => {
// Add your own logic to handle the webhook message
},
});
"use client";
import { createChannel } from "@erebus-sh/sdk/react";
import { z } from "zod";
// Create typed schemas for the channels you want to use
export const schema = {
chat: z.object({
message: z.string(),
sentAt: z.number(),
}),
};
// Create a channel hook
export const useChannel = createChannel(schema);
import "./globals.css";
import { ErebusProvider } from "@erebus-sh/sdk/react";
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
{/*
Wrap your entire app in ErebusProvider
You can must pass authBaseUrl, but you can leave wsBaseUrl out
If you leave wsBaseUrl out the defaults will be used wss://gateway.erebus.sh/ (recommended)
*/}
<ErebusProvider authBaseUrl="http://localhost:3000/">
<body
className="antialiased"
>
{children}
</body>
</ErebusProvider>
</html>
);
}
import { TopicProvider } from "@erebus-sh/sdk/react";
export default function ChatLayout({
children,
}: {
children: React.ReactNode;
}) {
{/*
Wrap the path where you want to use Erebus in TopicProvider
Think of it as a room inside a channel
You can set the topic to whatever fits your app
If the user is not allowed to read the channel they cannot subscribe to its topics
*/}
return <TopicProvider topic="room123">{children}</TopicProvider>;
}
"use client";
import { useChannel } from "@/erebus/client";
export default function ChatPage() {
{/**
Use the useChannel hook to access publish, presence, messages, isError, and error
The hook is fully typed and auto completes based on the schema you defined
*/}
const { publish, presence, messages, isError, error } = useChannel("chat");
return (
...
);
}
Full code example: examples/chat-app/
Deployment
Vercel Deployment
-
Configure environment variables
vercel env add SECRET_API_KEY vercel env add EREBUS_AUTH_URL
-
Update vercel.json
vercel.json { "functions": { "app/api/erebus/[...all]/route.ts": { "maxDuration": 30 } } }
-
Deploy
vercel deploy --prod
Environment Variables for Production
Variable | Value | Notes |
---|---|---|
SECRET_API_KEY | Your production API key | Get from Erebus dashboard |
EREBUS_AUTH_URL | https://your-app.vercel.app | Your deployed URL |
EREBUS_WS_URL | Leave empty | Uses default wss://gateway.erebus.sh |
Troubleshooting
Common Next.js Issues
1. Server Action Errors
Error:
Server Actions must be async functions
Solution: Ensure your API route handlers are async:
export const { POST } = createRouteHandler({
authorize: async (channel, ctx) => { // Must be async
// Your logic
}
});
2. Client-Side Hydration Issues
Error:
Hydration failed because the initial UI does not match
Solution: Use proper React patterns for real-time data:
import { useEffect, useState } from 'react';
export default function ChatPage() {
const [messages, setMessages] = useState<MessageBody[]>([]);
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return <div>Loading...</div>;
// Your Erebus hooks here
}
3. API Route Connection Issues
Error:
Failed to fetch grant token
Solution: Check your API route configuration:
// Verify the route is exported correctly
export const { POST } = createRouteHandler({
authorize: async (channel, ctx) => {
console.log("Auth request for channel:", channel);
// Your authorization logic
}
});
4. CORS Issues in Development
Error:
Access to fetch blocked by CORS policy
Solution: Update your Next.js configuration:
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
source: '/api/erebus/:path*',
headers: [
{ key: 'Access-Control-Allow-Origin', value: '*' },
{ key: 'Access-Control-Allow-Methods', value: 'POST' },
{ key: 'Access-Control-Allow-Headers', value: 'Content-Type' },
],
},
];
},
};
module.exports = nextConfig;
Performance Optimization
App Router vs Pages Router
App Router (Recommended):
- Server components for auth logic
- Streaming for real-time updates
- Better performance with React 18
Pages Router (Legacy):
- API routes for auth endpoints
- Client-side rendering for real-time features
- Simpler setup for existing apps
Production Optimizations
-
Enable compression
next.config.js const nextConfig = { compress: true, poweredByHeader: false, };
-
Optimize bundle splitting
// Lazy load Erebus client const ErebusClient = dynamic(() => import('@erebus-sh/sdk/client'), { ssr: false });
-
Implement connection pooling
// Singleton client instance let globalClient: ErebusPubSubClient | null = null; export function getErebusClient() { if (!globalClient) { globalClient = ErebusClient.createClient({ client: ErebusClientState.PubSub, authBaseUrl: process.env.EREBUS_AUTH_URL! }); } return globalClient; }
Advanced Patterns
Server-Side Events with React
// Server component for initial data
async function ChatRoom({ roomId }: { roomId: string }) {
const initialMessages = await getMessagesFromDB(roomId);
return (
<div>
<MessageList initialMessages={initialMessages} roomId={roomId} />
<MessageInput roomId={roomId} />
</div>
);
}
// Client component for real-time updates
'use client';
function MessageList({ initialMessages, roomId }) {
const { messages, publish } = useChannel("chat");
useEffect(() => {
// Subscribe when component mounts
client.subscribe(roomId, (msg) => {
setMessages(prev => [...prev, msg]);
});
}, [roomId]);
return (
<div>
{messages.map(msg => (
<div key={msg.id}>{msg.payload}</div>
))}
</div>
);
}
React Suspense Integration
import { Suspense } from 'react';
function RealtimeData({ topic }: { topic: string }) {
const { messages, isConnected } = useChannel("data");
if (!isConnected) {
throw new Promise(resolve => {
// Wait for connection
const checkConnection = () => {
if (client.isConnected) resolve(void 0);
else setTimeout(checkConnection, 100);
};
checkConnection();
});
}
return <div>{/* Your component */}</div>;
}
export default function Page() {
return (
<Suspense fallback={<div>Connecting...</div>}>
<RealtimeData topic="live-data" />
</Suspense>
);
}