HelloJohn / docs
Sdks

Next.js SDK

Add authentication to Next.js App Router and Pages Router apps with @hellojohn/nextjs — middleware, server components, and client hooks.

Next.js SDK

@hellojohn/nextjs provides first-class support for Next.js, including App Router Server Components, middleware-based route protection, and server actions.

Installation

npm install @hellojohn/nextjs

Environment variables

# .env.local
NEXT_PUBLIC_HJ_PUBLISHABLE_KEY=pk_live_...
HJ_SECRET_KEY=sk_live_...
NEXT_PUBLIC_HJ_API_URL=https://auth.yourdomain.com  # self-hosted only

Middleware (route protection)

Create middleware.ts at the root of your project:

// middleware.ts
import { hellojohnMiddleware } from '@hellojohn/nextjs/server';

export default hellojohnMiddleware({
  publicRoutes: ['/', '/sign-in(.*)', '/sign-up(.*)', '/about'],
  afterSignInUrl: '/dashboard',
  afterSignOutUrl: '/',
});

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|.*\\..*|api/public).*)',
  ],
};

Any route not in publicRoutes redirects unauthenticated visitors to the sign-in page.

App Router — Server Components

Read auth state in Server Components and Server Actions:

// app/dashboard/page.tsx
import { auth, currentUser } from '@hellojohn/nextjs/server';
import { redirect } from 'next/navigation';

export default async function DashboardPage() {
  const { userId, tenantId } = auth();

  if (!userId) redirect('/sign-in');

  const user = await currentUser();

  return (
    <div>
      <h1>Welcome, {user?.displayName}</h1>
      <p>Tenant: {tenantId}</p>
    </div>
  );
}

Server action with auth

// app/actions.ts
'use server';

import { auth } from '@hellojohn/nextjs/server';
import { createHelloJohnAdminClient } from '@hellojohn/node';

export async function updateProfile(formData: FormData) {
  const { userId } = auth();
  if (!userId) throw new Error('Unauthorized');

  const hj = createHelloJohnAdminClient({ secretKey: process.env.HJ_SECRET_KEY! });
  await hj.users.update(userId, {
    displayName: formData.get('displayName') as string,
  });
}

Route handler with auth

// app/api/me/route.ts
import { auth } from '@hellojohn/nextjs/server';
import { NextResponse } from 'next/server';

export async function GET() {
  const { userId, tenantId, role } = auth();

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

  return NextResponse.json({ userId, tenantId, role });
}

App Router — Client Components

// app/dashboard/profile.tsx
'use client';

import { useUser, useSession } from '@hellojohn/nextjs';

export function Profile() {
  const { user, isLoaded } = useUser();
  const { session, getToken } = useSession();

  if (!isLoaded) return <div>Loading...</div>;

  return (
    <div>
      <p>{user?.email}</p>
      <button onClick={async () => {
        const token = await getToken();
        console.log('Token:', token);
      }}>
        Get token
      </button>
    </div>
  );
}

Layout setup (App Router)

// app/layout.tsx
import { HelloJohnProvider } from '@hellojohn/nextjs';

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

Sign-in page

// app/sign-in/[[...sign-in]]/page.tsx
import { SignIn } from '@hellojohn/nextjs';

export default function SignInPage() {
  return (
    <div className="flex items-center justify-center min-h-screen">
      <SignIn
        afterSignInUrl="/dashboard"
        signUpUrl="/sign-up"
      />
    </div>
  );
}
// app/sign-up/[[...sign-up]]/page.tsx
import { SignUp } from '@hellojohn/nextjs';

export default function SignUpPage() {
  return (
    <div className="flex items-center justify-center min-h-screen">
      <SignUp
        afterSignUpUrl="/onboarding"
        signInUrl="/sign-in"
      />
    </div>
  );
}

Pages Router

// pages/_app.tsx
import { HelloJohnProvider } from '@hellojohn/nextjs';
import type { AppProps } from 'next/app';

export default function App({ Component, pageProps }: AppProps) {
  return (
    <HelloJohnProvider {...pageProps}>
      <Component {...pageProps} />
    </HelloJohnProvider>
  );
}
// pages/api/me.ts
import { withAuth } from '@hellojohn/nextjs/api';
import type { NextApiRequest, NextApiResponse } from 'next';

export default withAuth(function handler(req: NextApiRequest, res: NextApiResponse) {
  res.json({ user: req.auth.user });
});
// pages/dashboard.tsx
import { GetServerSideProps } from 'next';
import { getAuth } from '@hellojohn/nextjs/ssr';

export const getServerSideProps: GetServerSideProps = async (ctx) => {
  const { userId } = getAuth(ctx.req);

  if (!userId) {
    return { redirect: { destination: '/sign-in', permanent: false } };
  }

  return { props: {} };
};

Passing the token to your API

When calling your own API routes from client components:

'use client';
import { useSession } from '@hellojohn/nextjs';

export function DataFetcher() {
  const { getToken } = useSession();

  const fetchData = async () => {
    const token = await getToken();
    const res = await fetch('/api/data', {
      headers: { Authorization: `Bearer ${token}` },
    });
    return res.json();
  };
}

And verify it in the API route:

// app/api/data/route.ts
import { auth } from '@hellojohn/nextjs/server';
import { NextResponse } from 'next/server';

export async function GET() {
  const { userId } = auth();
  if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });

  // Fetch user-specific data
  const data = await db.query('SELECT * FROM items WHERE user_id = $1', [userId]);
  return NextResponse.json({ data });
}

On this page