Skip to content

@pubflow/nextjs

Next.js integration for Flowfull Clients with SSR support, API route helpers, middleware, and cookie-based session management.

Installation

bash
npm install @pubflow/core @pubflow/nextjs swr

Quick Start

1. Setup Provider (App Router)

typescript
// app/providers.tsx
'use client';

import { PubflowProvider } from '@pubflow/nextjs';

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <PubflowProvider url={process.env.NEXT_PUBLIC_BACKEND_URL!}>
      {children}
    </PubflowProvider>
  );
}
typescript
// app/layout.tsx
import { Providers } from './providers';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

2. Client Component

typescript
// app/dashboard/page.tsx
'use client';

import { useAuth, useBridgeQuery } from '@pubflow/nextjs';

export default function DashboardPage() {
  const { user, logout } = useAuth();
  const { data, error, isLoading } = useBridgeQuery('/api/users');

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <h1>Welcome, {user?.name}!</h1>
      <button onClick={logout}>Logout</button>
      <ul>
        {data.map((user: any) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

3. Server Component with SSR

typescript
// app/profile/page.tsx
import { withPubflowAuthSSR } from '@pubflow/nextjs';

export default withPubflowAuthSSR(
  async function ProfilePage({ user }) {
    // This runs on the server
    // user is automatically available
    return (
      <div>
        <h1>Profile</h1>
        <p>Name: {user.name}</p>
        <p>Email: {user.email}</p>
      </div>
    );
  },
  {
    redirectTo: '/login', // Redirect if not authenticated
  }
);

4. API Route with Authentication

typescript
// app/api/users/route.ts
import { withPubflow } from '@pubflow/nextjs';

export const GET = withPubflow(async (req, { user, api }) => {
  // user is automatically available if authenticated
  if (!user) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // api is the authenticated API client
  const users = await api.get('/api/users');

  return Response.json({ users });
});

export const POST = withPubflow(async (req, { user, api }) => {
  if (!user) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const body = await req.json();
  const newUser = await api.post('/api/users', body);

  return Response.json(newUser);
});

Provider

PubflowProvider

Same API as React, but with cookie-based session management.

typescript
'use client';

import { PubflowProvider } from '@pubflow/nextjs';

<PubflowProvider
  url={process.env.NEXT_PUBLIC_BACKEND_URL!}
  instanceId="default"
  onAuthError={(error) => console.error('Auth error:', error)}
  onSessionExpired={() => {
    window.location.href = '/login';
  }}
>
  <App />
</PubflowProvider>

Hooks

All hooks from @pubflow/react are available, plus Next.js-specific hooks:

useAuth()

Same as React, but with cookie-based storage.

typescript
'use client';

import { useAuth } from '@pubflow/nextjs';

export function LoginForm() {
  const { login, isLoading, error } = useAuth();

  const handleLogin = async (e: React.FormEvent) => {
    e.preventDefault();
    await login({ email: 'user@example.com', password: 'password123' });
  };

  return <form onSubmit={handleLogin}>...</form>;
}

useServerAuth()

Access server-side authentication state in client components.

typescript
'use client';

import { useServerAuth } from '@pubflow/nextjs';

export function ServerAuthExample() {
  const { user, isAuthenticated, isLoading } = useServerAuth();

  if (isLoading) return <div>Loading...</div>;
  if (!isAuthenticated) return <div>Not authenticated</div>;

  return <div>Welcome, {user?.name}!</div>;
}

useBridgeQuery(), useBridgeMutation(), useBridgeCrud(), useBridgeApi()

Same as React. See @pubflow/react documentation for details.

useSearchQueryBuilder()

Build search queries for data tables.

typescript
'use client';

import { useSearchQueryBuilder } from '@pubflow/nextjs';

export function UserSearch() {
  const { query, setFilter, setSort, setPage } = useSearchQueryBuilder();

  return (
    <div>
      <input onChange={(e) => setFilter('name', e.target.value)} />
      <button onClick={() => setSort('created_at', 'desc')}>Sort by Date</button>
      <button onClick={() => setPage(2)}>Page 2</button>
      <p>Query: {query}</p>
    </div>
  );
}

SSR Utilities

withPubflowSSR()

Wrap server components to access Pubflow context.

typescript
import { withPubflowSSR } from '@pubflow/nextjs';

export default withPubflowSSR(async function HomePage({ api, storage }) {
  // api is the API client
  // storage is the server storage adapter

  const data = await api.get('/api/public-data');

  return <div>{JSON.stringify(data)}</div>;
});

withPubflowAuthSSR()

Wrap server components that require authentication.

typescript
import { withPubflowAuthSSR } from '@pubflow/nextjs';

export default withPubflowAuthSSR(
  async function ProtectedPage({ user, api }) {
    // user is guaranteed to be available
    // Automatically redirects if not authenticated

    const userData = await api.get(`/api/users/${user.id}`);

    return (
      <div>
        <h1>Welcome, {user.name}!</h1>
        <pre>{JSON.stringify(userData, null, 2)}</pre>
      </div>
    );
  },
  {
    redirectTo: '/login', // Where to redirect if not authenticated
  }
);

Options

typescript
interface AuthSSROptions {
  redirectTo?: string; // Redirect URL if not authenticated
  onUnauthorized?: (req: Request) => Response; // Custom unauthorized handler
}

API Route Helpers

withPubflow()

Wrap API routes to access Pubflow context.

typescript
import { withPubflow } from '@pubflow/nextjs';

export const GET = withPubflow(async (req, { user, api, storage }) => {
  // user is available if authenticated (null otherwise)
  // api is the authenticated API client
  // storage is the server storage adapter

  if (!user) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const data = await api.get('/api/data');
  return Response.json(data);
});

Context Object

The context object passed to wrapped functions contains:

typescript
interface PubflowContext {
  user: User | null;           // Current user (null if not authenticated)
  api: ApiClient;              // Authenticated API client
  storage: ServerStorageAdapter; // Server storage adapter
  sessionId: string | null;    // Current session ID
}

Components

All components from @pubflow/react are available:

  • BridgeView - Display data from API
  • BridgeTable - Data table with sorting/filtering
  • BridgeForm - Form with validation
  • BridgeList - List with infinite scroll
  • OfflineIndicator - Connection status
  • AdvancedFilter - Advanced filtering

See @pubflow/react documentation for component details.

Storage

ServerStorageAdapter

Cookie-based storage for server-side rendering.

typescript
import { ServerStorageAdapter } from '@pubflow/nextjs';
import { cookies } from 'next/headers';

const storage = new ServerStorageAdapter(cookies());

// Session is stored in httpOnly cookie
await storage.setItem('pubflow_session_id', 'ses_...');
typescript
{
  httpOnly: true,      // Not accessible to JavaScript
  secure: true,        // HTTPS only in production
  sameSite: 'lax',     // CSRF protection
  maxAge: 30 * 24 * 60 * 60, // 30 days
}

ClientStorageAdapter

Browser localStorage for client-side rendering.

typescript
import { ClientStorageAdapter } from '@pubflow/nextjs';

const storage = new ClientStorageAdapter();

// Same as React's LocalStorageAdapter
await storage.setItem('pubflow_session_id', 'ses_...');

Examples

Protected Dashboard (SSR)

typescript
// app/dashboard/page.tsx
import { withPubflowAuthSSR } from '@pubflow/nextjs';

export default withPubflowAuthSSR(
  async function DashboardPage({ user, api }) {
    // Fetch data on the server
    const stats = await api.get('/api/stats');
    const recentActivity = await api.get('/api/activity/recent');

    return (
      <div>
        <h1>Dashboard</h1>
        <p>Welcome back, {user.name}!</p>

        <div>
          <h2>Stats</h2>
          <pre>{JSON.stringify(stats, null, 2)}</pre>
        </div>

        <div>
          <h2>Recent Activity</h2>
          <ul>
            {recentActivity.map((item: any) => (
              <li key={item.id}>{item.description}</li>
            ))}
          </ul>
        </div>
      </div>
    );
  },
  { redirectTo: '/login' }
);

Login Page

typescript
// app/login/page.tsx
'use client';

import { useAuth } from '@pubflow/nextjs';
import { useState } from 'react';
import { useRouter } from 'next/navigation';

export default function LoginPage() {
  const { login, isLoading, error } = useAuth();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const router = useRouter();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      await login({ email: email.toLowerCase(), password });
      router.push('/dashboard');
    } catch (err) {
      console.error('Login failed:', err);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <h1>Login</h1>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value.toLowerCase())}
        placeholder="Email"
        required
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
        required
      />
      {error && <p style={{ color: 'red' }}>{error.message}</p>}
      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Logging in...' : 'Login'}
      </button>
    </form>
  );
}

API Route with CRUD Operations

typescript
// app/api/users/route.ts
import { withPubflow } from '@pubflow/nextjs';

export const GET = withPubflow(async (req, { user, api }) => {
  if (!user) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const users = await api.get('/api/users');
  return Response.json(users);
});

export const POST = withPubflow(async (req, { user, api }) => {
  if (!user) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const body = await req.json();
  const newUser = await api.post('/api/users', body);
  return Response.json(newUser, { status: 201 });
});
typescript
// app/api/users/[id]/route.ts
import { withPubflow } from '@pubflow/nextjs';

export const GET = withPubflow(async (req, { user, api }, { params }) => {
  if (!user) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const userId = params.id;
  const userData = await api.get(`/api/users/${userId}`);
  return Response.json(userData);
});

export const PUT = withPubflow(async (req, { user, api }, { params }) => {
  if (!user) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const userId = params.id;
  const body = await req.json();
  const updatedUser = await api.put(`/api/users/${userId}`, body);
  return Response.json(updatedUser);
});

export const DELETE = withPubflow(async (req, { user, api }, { params }) => {
  if (!user) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const userId = params.id;
  await api.delete(`/api/users/${userId}`);
  return Response.json({ success: true });
});

Client Component with Data Fetching

typescript
// app/users/page.tsx
'use client';

import { useBridgeQuery, useBridgeMutation } from '@pubflow/nextjs';
import { useState } from 'react';

export default function UsersPage() {
  const { data: users, error, isLoading, mutate } = useBridgeQuery('/api/users');
  const { trigger: createUser, isMutating } = useBridgeMutation('/api/users', 'POST');
  const [name, setName] = useState('');

  const handleCreate = async (e: React.FormEvent) => {
    e.preventDefault();
    await createUser({ name, email: `${name.toLowerCase()}@example.com` });
    setName('');
    mutate(); // Refresh the list
  };

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <h1>Users</h1>

      <form onSubmit={handleCreate}>
        <input
          value={name}
          onChange={(e) => setName(e.target.value)}
          placeholder="Name"
        />
        <button type="submit" disabled={isMutating}>
          {isMutating ? 'Creating...' : 'Create User'}
        </button>
      </form>

      <ul>
        {users.map((user: any) => (
          <li key={user.id}>
            {user.name} - {user.email}
          </li>
        ))}
      </ul>
    </div>
  );
}

Middleware for Authentication

typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const sessionId = request.cookies.get('pubflow_session_id')?.value;

  // Protect routes
  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    if (!sessionId) {
      return NextResponse.redirect(new URL('/login', request.url));
    }
  }

  // Redirect to dashboard if already logged in
  if (request.nextUrl.pathname === '/login') {
    if (sessionId) {
      return NextResponse.redirect(new URL('/dashboard', request.url));
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/login'],
};

Environment Variables

bash
# .env.local
NEXT_PUBLIC_BACKEND_URL=https://your-backend.com

TypeScript Support

Full TypeScript support with type inference:

typescript
import { useBridgeQuery } from '@pubflow/nextjs';

interface User {
  id: string;
  name: string;
  email: string;
}

export default function UsersPage() {
  const { data } = useBridgeQuery<User[]>('/api/users');
  // data is typed as User[] | undefined
}

Best Practices

1. Use Server Components When Possible

Server components are faster and more SEO-friendly:

typescript
// ✅ Good - Server component
import { withPubflowAuthSSR } from '@pubflow/nextjs';

export default withPubflowAuthSSR(async function Page({ user, api }) {
  const data = await api.get('/api/data');
  return <div>{data.title}</div>;
});

// ❌ Avoid - Client component for static data
'use client';
import { useBridgeQuery } from '@pubflow/nextjs';

export default function Page() {
  const { data } = useBridgeQuery('/api/data');
  return <div>{data?.title}</div>;
}

2. Use API Routes for Backend Communication

Don't call your Flowfull backend directly from client components:

typescript
// ✅ Good - API route
// app/api/users/route.ts
export const GET = withPubflow(async (req, { api }) => {
  const users = await api.get('/api/users');
  return Response.json(users);
});

// app/users/page.tsx
'use client';
const { data } = useBridgeQuery('/api/users'); // Calls Next.js API route

// ❌ Avoid - Direct backend call from client
const { data } = useBridgeQuery('https://backend.com/api/users');

3. Handle Authentication Errors

typescript
<PubflowProvider
  url={process.env.NEXT_PUBLIC_BACKEND_URL!}
  onSessionExpired={() => {
    window.location.href = '/login';
  }}
  onAuthError={(error) => {
    console.error('Auth error:', error);
    if (error.message.includes('401')) {
      window.location.href = '/login';
    }
  }}
>

Next Steps