Skip to content

@pubflow/nextjs

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

📚 Documentation

Installation

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

✨ Features

  • Server-Side Rendering (SSR) - Full SSR support with App Router
  • API Route Helpers - Simplified API route creation
  • Bridge Payments - Payment processing with Stripe, PayPal, etc.
  • Authentication - Complete auth flow with Flowless
  • Cookie-Based Sessions - Secure httpOnly cookies
  • Middleware - Route protection and authentication
  • TypeScript - Full type safety
  • App Router & Pages Router - Support for both routing systems

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'],
};

Bridge Payments

The Next.js package includes full support for Bridge Payments with both server-side and client-side integration.

Server-Side Payment Processing (API Route)

typescript
// app/api/create-payment/route.ts
import { withPubflow } from '@pubflow/nextjs';
import { BridgePaymentClient } from '@pubflow/nextjs';

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

  try {
    const body = await req.json();

    // Initialize Bridge Payments on server
    const payments = new BridgePaymentClient({
      baseUrl: process.env.BRIDGE_URL!,
      getSessionId: async () => {
        // Session is available from cookies
        return req.cookies.get('pubflow_session_id')?.value || null;
      },
    });

    // Create payment intent
    const intent = await payments.createPaymentIntent({
      total_cents: body.amount,
      currency: 'USD',
      description: body.description,
      provider_id: 'stripe',
    });

    return Response.json({ intent });
  } catch (error: any) {
    return Response.json({ error: error.message }, { status: 500 });
  }
});

Client-Side Payment Integration

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

import { BridgePaymentClient } from '@pubflow/nextjs';
import { useState } from 'react';
import { loadStripe } from '@stripe/stripe-js';
import { CardElement, Elements, useStripe, useElements } from '@stripe/react-stripe-js';

const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_KEY!);

function CheckoutForm() {
  const stripe = useStripe();
  const elements = useElements();
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!stripe || !elements) return;

    setLoading(true);
    setError(null);

    try {
      // Initialize Bridge Payments client
      const payments = new BridgePaymentClient({
        baseUrl: process.env.NEXT_PUBLIC_BRIDGE_URL!,
        getSessionId: async () => {
          // Get session from cookies (client-side)
          const cookies = document.cookie.split(';');
          const sessionCookie = cookies.find(c => c.trim().startsWith('pubflow_session_id='));
          return sessionCookie?.split('=')[1] || null;
        },
      });

      // Create payment intent
      const intent = await payments.createPaymentIntent({
        total_cents: 5000, // $50.00
        currency: 'USD',
        description: 'Product Purchase',
        provider_id: 'stripe',
      });

      // Create token from card
      const cardElement = elements.getElement(CardElement);
      const { token, error: tokenError } = await stripe.createToken(cardElement!);

      if (tokenError) {
        setError(tokenError.message || 'Failed to create token');
        return;
      }

      // Create payment method
      await payments.createPaymentMethod({
        type: 'card',
        card_token: token!.id,
        is_default: true,
      });

      alert('Payment successful!');
    } catch (err: any) {
      setError(err.message || 'Payment failed');
    } finally {
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <CardElement />
      {error && <div style={{ color: 'red' }}>{error}</div>}
      <button type="submit" disabled={!stripe || loading}>
        {loading ? 'Processing...' : 'Pay $50.00'}
      </button>
    </form>
  );
}

export default function CheckoutPage() {
  return (
    <Elements stripe={stripePromise}>
      <CheckoutForm />
    </Elements>
  );
}

Server Component with Payment Data

typescript
// app/subscriptions/page.tsx
import { withPubflowAuthSSR } from '@pubflow/nextjs';
import { BridgePaymentClient } from '@pubflow/nextjs';
import { cookies } from 'next/headers';

export default withPubflowAuthSSR(
  async function SubscriptionsPage({ user }) {
    // Initialize Bridge Payments on server
    const payments = new BridgePaymentClient({
      baseUrl: process.env.BRIDGE_URL!,
      getSessionId: async () => {
        return cookies().get('pubflow_session_id')?.value || null;
      },
    });

    // Fetch subscriptions on server
    const subscriptions = await payments.listSubscriptions();

    return (
      <div>
        <h1>Your Subscriptions</h1>
        <ul>
          {subscriptions.data.map((sub) => (
            <li key={sub.id}>
              {sub.plan_id} - {sub.status}
            </li>
          ))}
        </ul>
      </div>
    );
  },
  { redirectTo: '/login' }
);

Payment Management API Routes

typescript
// app/api/payment-methods/route.ts
import { withPubflow } from '@pubflow/nextjs';
import { BridgePaymentClient } from '@pubflow/nextjs';

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

  const payments = new BridgePaymentClient({
    baseUrl: process.env.BRIDGE_URL!,
    getSessionId: async () => req.cookies.get('pubflow_session_id')?.value || null,
  });

  const methods = await payments.listPaymentMethods();
  return Response.json(methods);
});

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

  const body = await req.json();
  const payments = new BridgePaymentClient({
    baseUrl: process.env.BRIDGE_URL!,
    getSessionId: async () => req.cookies.get('pubflow_session_id')?.value || null,
  });

  const method = await payments.createPaymentMethod(body);
  return Response.json(method, { status: 201 });
});
typescript
// app/api/payment-methods/[id]/route.ts
import { withPubflow } from '@pubflow/nextjs';
import { BridgePaymentClient } from '@pubflow/nextjs';

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

  const payments = new BridgePaymentClient({
    baseUrl: process.env.BRIDGE_URL!,
    getSessionId: async () => req.cookies.get('pubflow_session_id')?.value || null,
  });

  await payments.deletePaymentMethod(params.id);
  return Response.json({ success: true });
});

Available Payment Features

All payment features from @pubflow/core are available:

  • Payment Intents - Create and manage payment intents
  • Payment Methods - Save and manage cards, bank accounts
  • Subscriptions - Recurring billing and subscription management
  • Customers - Customer profile management
  • Addresses - Billing and shipping addresses
  • Organizations - Multi-tenant organization support
  • Payment History - View past payments and transactions
  • Guest Checkout - Accept payments without registration
  • Server-Side Processing - Secure payment processing on the server
  • Client-Side Integration - Seamless client-side payment flows

See the Bridge Payments documentation for complete details.

Environment Variables

bash
# .env.local
NEXT_PUBLIC_BACKEND_URL=https://your-backend.com
NEXT_PUBLIC_BRIDGE_URL=https://your-bridge-payments.com
NEXT_PUBLIC_STRIPE_KEY=pk_test_...

# Server-only (not exposed to client)
BRIDGE_URL=https://your-bridge-payments.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