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.

/app/api/erebus/[...all]/route.ts
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
 },
});
/erebus/client.ts
"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);
app/layout.tsx
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>
 );
}
app/chat/layout.tsx
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>;
}
app/chat/page.tsx
"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

  1. Configure environment variables

    vercel env add SECRET_API_KEY
    vercel env add EREBUS_AUTH_URL
  2. Update vercel.json

    vercel.json
    {
      "functions": {
        "app/api/erebus/[...all]/route.ts": {
          "maxDuration": 30
        }
      }
    }
  3. Deploy

    vercel deploy --prod

Environment Variables for Production

VariableValueNotes
SECRET_API_KEYYour production API keyGet from Erebus dashboard
EREBUS_AUTH_URLhttps://your-app.vercel.appYour deployed URL
EREBUS_WS_URLLeave emptyUses 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:

next.config.js
/** @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

  1. Enable compression

    next.config.js
    const nextConfig = {
      compress: true,
      poweredByHeader: false,
    };
  2. Optimize bundle splitting

    // Lazy load Erebus client
    const ErebusClient = dynamic(() => import('@erebus-sh/sdk/client'), {
      ssr: false
    });
  3. 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>
  );
}