بنقرة واحدة
volcano-nextjs
Detailed guidance for using the Volcano SDK correctly in Next.js environments
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Detailed guidance for using the Volcano SDK correctly in Next.js environments
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Install or upgrade the Volcano CLI from a plugin-shipped skills environment.
Detailed guidance for authentication flows built with the Volcano SDK
Detailed guidance for browser and function data access with the Volcano query builder
Reusable error handling patterns for Volcano SDK apps: centralized error dispatcher with action enum, useApiCall React hook for loading/error/data state, retry with exponential backoff, retry with toast notifications, and cross-domain error message catalog.
Detailed guidance for server-side function invocation and orchestration with Volcano
Canonical Volcano project shape and deploy contract: the volcano/functions/ model, migrations, volcano-config.yaml, env vars, shared-code conventions, and the build/deploy workflow.
| name | volcano-nextjs |
| description | Detailed guidance for using the Volcano SDK correctly in Next.js environments |
Implement Volcano SDK in Next.js with strict client/server separation and middleware-first auth checks. This skill is self-contained: env contract, shared client pattern, AuthContext template, middleware helpers, app/pages router examples, server actions, OAuth callback, and SSR safety are embedded.
getVolcano() helper used by all client components.createServerClient, withAuth, getTokenFromRequest) for server/middleware auth checks..env.local:
NEXT_PUBLIC_VOLCANO_API_URL=https://api.yourproject.volcano.dev
NEXT_PUBLIC_VOLCANO_ANON_KEY=ak-your-anon-key
NEXT_PUBLIC_VOLCANO_DATABASE_NAME=your-database
NEXT_PUBLIC_* vars are exposed to both client and server.NEXT_PUBLIC_ prefix.| Environment | SDK usage |
|---|---|
| Client Components | Full SDK — auth, queries, storage, realtime |
| Server Components | No direct SDK — call API routes, server actions, or middleware-derived data |
| Middleware | @volcano.dev/sdk/next/middleware helpers |
| API Routes / Route Handlers | Server client for token validation; full SDK with the bearer token for queries |
// lib/volcano.ts
import { VolcanoAuth } from '@volcano.dev/sdk';
let volcano: VolcanoAuth | null = null;
export function getVolcano(): VolcanoAuth {
if (!volcano) {
volcano = new VolcanoAuth({
apiUrl: process.env.NEXT_PUBLIC_VOLCANO_API_URL!,
anonKey: process.env.NEXT_PUBLIC_VOLCANO_ANON_KEY!,
});
volcano.database(process.env.NEXT_PUBLIC_VOLCANO_DATABASE_NAME!);
}
return volcano;
}
// context/AuthContext.tsx
'use client';
import { createContext, useContext, useEffect, useState, useCallback, ReactNode } from 'react';
import { getVolcano } from '@/lib/volcano';
import type { User } from '@volcano.dev/sdk';
interface AuthContextType {
user: User | null;
loading: boolean;
signIn: (email: string, password: string) => Promise<void>;
signUp: (email: string, password: string) => Promise<void>;
signOut: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const volcano = getVolcano();
// Restore session on mount
volcano.initialize().then(({ user }) => {
setUser(user);
setLoading(false);
});
// React to subsequent changes
const unsubscribe = volcano.auth.onAuthStateChange(setUser);
return () => unsubscribe();
}, []);
const signIn = useCallback(async (email: string, password: string) => {
const { error } = await getVolcano().auth.signIn({ email, password });
if (error) throw error;
}, []);
const signUp = useCallback(async (email: string, password: string) => {
const { error } = await getVolcano().auth.signUp({ email, password });
if (error) throw error;
}, []);
const signOut = useCallback(async () => {
await getVolcano().auth.signOut();
}, []);
return (
<AuthContext.Provider value={{ user, loading, signIn, signUp, signOut }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be used within an AuthProvider');
return ctx;
}
Wire in app/layout.tsx:
import { AuthProvider } from '@/context/AuthContext';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en"><body><AuthProvider>{children}</AuthProvider></body></html>
);
}
'use client';
import { useAuth } from '@/context/AuthContext';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { getVolcano } from '@/lib/volcano';
export default function DashboardPage() {
const { user, loading } = useAuth();
const router = useRouter();
const [posts, setPosts] = useState<any[]>([]);
useEffect(() => {
if (!loading && !user) router.push('/login');
}, [user, loading, router]);
useEffect(() => {
if (!user) return;
getVolcano()
.from('posts')
.select('*')
.order('created_at', { ascending: false })
.limit(10)
.then(({ data }) => setPosts(data ?? []));
}, [user]);
if (loading) return <div>Loading...</div>;
if (!user) return null;
return (
<div>
<h1>Welcome, {user.email}</h1>
<ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>
</div>
);
}
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { createServerClient, withAuth } from '@volcano.dev/sdk/next/middleware';
export async function middleware(request: NextRequest) {
const client = createServerClient({
anonKey: process.env.NEXT_PUBLIC_VOLCANO_ANON_KEY!,
apiUrl: process.env.NEXT_PUBLIC_VOLCANO_API_URL,
});
const user = await withAuth(request, client);
if (request.nextUrl.pathname.startsWith('/dashboard') && !user) {
return NextResponse.redirect(new URL('/login', request.url));
}
if (request.nextUrl.pathname === '/login' && user) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/login'],
};
import {
createServerClient,
withAuth,
getTokenFromRequest,
isBrowser,
isServer,
} from '@volcano.dev/sdk/next/middleware';
const client = createServerClient({ anonKey, apiUrl }); // apiUrl optional
const user = await withAuth(request, client); // null when invalid/missing
const token = getTokenFromRequest(request); // Authorization header or cookie
const { user, error } = await client.getUser(token);
const { accessToken, refreshToken, error } = await client.refreshToken(oldRefreshToken);
if (isBrowser()) { /* ... */ }
if (isServer()) { /* ... */ }
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createServerClient, getTokenFromRequest } from '@volcano.dev/sdk/next/middleware';
import { VolcanoAuth } from '@volcano.dev/sdk';
export async function GET(request: NextRequest) {
const client = createServerClient({
anonKey: process.env.NEXT_PUBLIC_VOLCANO_ANON_KEY!,
apiUrl: process.env.NEXT_PUBLIC_VOLCANO_API_URL,
});
const token = getTokenFromRequest(request);
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { user, error } = await client.getUser(token);
if (error || !user) return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
// Use the SDK with the bearer token for queries / function calls
const volcano = new VolcanoAuth({
apiUrl: process.env.NEXT_PUBLIC_VOLCANO_API_URL!,
anonKey: process.env.NEXT_PUBLIC_VOLCANO_ANON_KEY!,
accessToken: token,
});
const { data, error: queryError } = await volcano.functions.invoke('get-user-posts');
if (queryError) return NextResponse.json({ error: queryError.message }, { status: 500 });
return NextResponse.json({ posts: data });
}
// pages/api/posts.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { VolcanoAuth } from '@volcano.dev/sdk';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) return res.status(401).json({ error: 'Unauthorized' });
const volcano = new VolcanoAuth({
apiUrl: process.env.NEXT_PUBLIC_VOLCANO_API_URL!,
anonKey: process.env.NEXT_PUBLIC_VOLCANO_ANON_KEY!,
accessToken: token,
});
volcano.database(process.env.NEXT_PUBLIC_VOLCANO_DATABASE_NAME!);
const { data, error } = await volcano
.from('posts')
.select('*')
.order('created_at', { ascending: false });
if (error) return res.status(500).json({ error: error.message });
return res.status(200).json({ posts: data });
}
The SDK stores tokens in localStorage by default. Server Actions, route handlers, and middleware run on the server and cannot read localStorage — they read cookies. Add a small client component that mirrors the access token to a cookie whenever auth state changes:
// components/CookieSync.tsx
'use client';
import { useEffect } from 'react';
import { getVolcano } from '@/lib/volcano';
export function CookieSync() {
useEffect(() => {
const volcano = getVolcano();
const sync = (token: string | null) => {
const base = 'Path=/; SameSite=Lax; Secure';
document.cookie = token
? `volcano_access_token=${token}; ${base}; Max-Age=3600`
: `volcano_access_token=; ${base}; Max-Age=0`;
};
sync(volcano.accessToken ?? null);
const unsub = volcano.auth.onAuthStateChange(() => {
sync(volcano.accessToken ?? null);
});
return () => unsub();
}, []);
return null;
}
Mount it once, alongside the AuthProvider:
// app/layout.tsx
import { AuthProvider } from '@/context/AuthContext';
import { CookieSync } from '@/components/CookieSync';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<AuthProvider>
<CookieSync />
{children}
</AuthProvider>
</body>
</html>
);
}
Security note: This cookie is JS-readable, which matches the SDK's localStorage exposure — the XSS surface area is identical. For higher-security postures, set an httpOnly cookie from an API route after sign-in and read it server-side via getTokenFromRequest. The client-mirrored pattern shown above is the simplest route to a working Server Action.
Server Components don't have localStorage. Three options:
// app/actions.ts
'use server';
import { cookies } from 'next/headers';
import { createServerClient } from '@volcano.dev/sdk/next/middleware';
import { VolcanoAuth } from '@volcano.dev/sdk';
export async function getPosts() {
const cookieStore = cookies();
const token = cookieStore.get('volcano_access_token')?.value;
if (!token) throw new Error('Not authenticated');
const client = createServerClient({
anonKey: process.env.NEXT_PUBLIC_VOLCANO_ANON_KEY!,
apiUrl: process.env.NEXT_PUBLIC_VOLCANO_API_URL,
});
const { user, error } = await client.getUser(token);
if (error || !user) throw new Error('Invalid session');
const volcano = new VolcanoAuth({
apiUrl: process.env.NEXT_PUBLIC_VOLCANO_API_URL!,
anonKey: process.env.NEXT_PUBLIC_VOLCANO_ANON_KEY!,
accessToken: token,
});
volcano.database(process.env.NEXT_PUBLIC_VOLCANO_DATABASE_NAME!);
const { data } = await volcano
.from('posts')
.select('id, title, created_at')
.order('created_at', { ascending: false })
.limit(10);
return data;
}
// app/posts/page.tsx
import { getPosts } from '@/app/actions';
export default async function PostsPage() {
const posts = await getPosts();
return <ul>{posts?.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}
'use client';
import { useEffect, useState } from 'react';
import { VolcanoRealtime } from '@volcano.dev/sdk/realtime';
import { getVolcano } from '@/lib/volcano';
import { useAuth } from '@/context/AuthContext';
export function LivePosts() {
const { user } = useAuth();
const [posts, setPosts] = useState<any[]>([]);
useEffect(() => {
if (!user) return;
const volcano = getVolcano();
volcano
.from('posts')
.select('*')
.order('created_at', { ascending: false })
.then(({ data }) => setPosts(data ?? []));
const realtime = new VolcanoRealtime({
apiUrl: process.env.NEXT_PUBLIC_VOLCANO_API_URL!,
anonKey: process.env.NEXT_PUBLIC_VOLCANO_ANON_KEY!,
accessToken: volcano.accessToken!,
});
realtime.connect().then(() => {
const channel = realtime.channel('posts', { type: 'postgres' });
channel.onPostgresChanges('INSERT', 'public', 'posts', (c) =>
setPosts((cur) => [c.record, ...cur]));
channel.onPostgresChanges('UPDATE', 'public', 'posts', (c) =>
setPosts((cur) => cur.map((p) => (p.id === c.record.id ? c.record : p))));
channel.onPostgresChanges('DELETE', 'public', 'posts', (c) =>
setPosts((cur) => cur.filter((p) => p.id !== c.old_record.id)));
channel.subscribe();
});
return () => { realtime.disconnect(); };
}, [user]);
return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}
OAuth initiation must be in a Client Component; the callback page reconciles the tokens.
// app/login/page.tsx
'use client';
import { getVolcano } from '@/lib/volcano';
export default function LoginPage() {
return (
<button onClick={() => getVolcano().auth.signInWithGoogle()}>
Sign in with Google
</button>
);
}
// app/auth/callback/page.tsx
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { getVolcano } from '@/lib/volcano';
export default function AuthCallbackPage() {
const router = useRouter();
useEffect(() => {
getVolcano().initialize().then(({ user }) => {
router.push(user ? '/dashboard' : '/login?error=auth_failed');
});
}, [router]);
return <div>Completing sign in...</div>;
}
volcano.auth.signInWith* (OAuth) only works in browser; calling it server-side throws.sk-...) in browser environments.isBrowser() / isServer() from middleware helpers when branching.getVolcano(). Don't new VolcanoAuth(...) in components.useAuth().loading === false.connect() with disconnect() for realtime in useEffect cleanup.NEXT_PUBLIC_ only for non-secrets; service keys never get this prefix.withAuth (or equivalent) before allowing protected routes.