Ploy
Ploy
Ploy Start

Authentication

Add user authentication to your Next.js application with Ploy Auth.

Authentication

This guide shows how to integrate Ploy Auth into a Next.js application using the @meetploy/auth-react package.

Setup

1. Install Dependencies

pnpm add @meetploy/auth-react

2. Configure Auth Binding

Add the auth configuration to your ploy.yaml:

ploy.yaml
kind: dynamic
build: pnpm build
out: dist
auth:
  binding: AUTH_DB

3. Add AuthProvider

Wrap your application with the AuthProvider:

app/layout.tsx
import { AuthProvider } from "@meetploy/auth-react";

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

Pre-Built Components

SignInForm

A complete sign-in form with email and password fields:

app/login/page.tsx
"use client";

import { SignInForm } from "@meetploy/auth-react";
import { useRouter } from "next/navigation";

export default function LoginPage() {
	const router = useRouter();

	return (
		<div className="max-w-md mx-auto mt-20 p-6">
			<h1 className="text-2xl font-bold mb-6">Sign In</h1>
			<SignInForm
				onSuccess={() => router.push("/dashboard")}
				onError={(error) => console.error(error.message)}
			/>
			<p className="mt-4 text-center text-sm">
				Don't have an account?{" "}
				<a href="/signup" className="text-blue-500">
					Sign up
				</a>
			</p>
		</div>
	);
}

SignUpForm

A complete sign-up form with email, password, and confirm password fields:

app/signup/page.tsx
"use client";

import { SignUpForm } from "@meetploy/auth-react";
import { useRouter } from "next/navigation";

export default function SignUpPage() {
	const router = useRouter();

	return (
		<div className="max-w-md mx-auto mt-20 p-6">
			<h1 className="text-2xl font-bold mb-6">Create Account</h1>
			<SignUpForm
				onSuccess={() => router.push("/dashboard")}
				onError={(error) => console.error(error.message)}
			/>
			<p className="mt-4 text-center text-sm">
				Already have an account?{" "}
				<a href="/login" className="text-blue-500">
					Sign in
				</a>
			</p>
		</div>
	);
}

Custom Metadata Fields

Add extra fields to collect user metadata during signup:

<SignUpForm
	fields={["name", "company"]}
	onSuccess={(user) => console.log(user)}
/>

Component Props

Both components accept these props:

interface SignInFormProps {
	onSuccess?: (user: PloyUser) => void; // Called after successful auth
	onError?: (error: Error) => void; // Called on error
	redirectTo?: string; // Auto-redirect URL after success
	className?: string; // Custom CSS class
}

interface SignUpFormProps extends SignInFormProps {
	fields?: string[]; // Additional metadata fields to collect
}

useAuth Hook

Access auth state anywhere in your app:

components/user-menu.tsx
"use client";

import { useAuth } from "@meetploy/auth-react";

export function UserMenu() {
	const { user, isLoading, signOut } = useAuth();

	if (isLoading) {
		return <div>Loading...</div>;
	}

	if (!user) {
		return <a href="/login">Sign In</a>;
	}

	return (
		<div className="flex items-center gap-4">
			<span>{user.email}</span>
			<button onClick={signOut}>Sign Out</button>
		</div>
	);
}

Hook Return Value

interface AuthContextValue {
	user: PloyUser | null; // Current user or null
	isLoading: boolean; // True during initial load
	isAuthenticated: boolean; // True if user is signed in
	accessToken: string | null; // Current access token

	// Auth methods
	signIn: (email: string, password: string) => Promise<AuthResponse>;
	signUp: (
		email: string,
		password: string,
		metadata?: Record<string, string>,
	) => Promise<AuthResponse>;
	signOut: () => Promise<void>;
	refreshTokens: () => Promise<RefreshResponse>;
}

Protected Routes

Client-Side Protection

app/dashboard/page.tsx
"use client";

import { useAuth } from "@meetploy/auth-react";
import { useRouter } from "next/navigation";
import { useEffect } from "react";

export default function DashboardPage() {
	const { user, isLoading } = useAuth();
	const router = useRouter();

	useEffect(() => {
		if (!isLoading && !user) {
			router.push("/login");
		}
	}, [user, isLoading, router]);

	if (isLoading) {
		return <div>Loading...</div>;
	}

	if (!user) {
		return null;
	}

	return (
		<div>
			<h1>Welcome, {user.email}</h1>
			{user.metadata?.name && <p>Name: {user.metadata.name}</p>}
		</div>
	);
}

Protected API Routes

app/api/protected/route.ts
export async function GET(request: Request) {
	const authHeader = request.headers.get("Authorization");

	if (!authHeader?.startsWith("Bearer ")) {
		return Response.json({ error: "Unauthorized" }, { status: 401 });
	}

	const token = authHeader.replace("Bearer ", "");

	// Verify token using the PLOY_AUTH binding
	const user = await env.PLOY_AUTH.getUser(token);

	if (!user) {
		return Response.json({ error: "Invalid token" }, { status: 401 });
	}

	// User is authenticated
	return Response.json({
		message: "Protected data",
		userId: user.id,
	});
}

Making Authenticated Requests

Use the access token from useAuth for API calls:

"use client";

import { useAuth } from "@meetploy/auth-react";

export function DataFetcher() {
	const { accessToken } = useAuth();

	const fetchProtectedData = async () => {
		const response = await fetch("/api/protected", {
			headers: {
				Authorization: `Bearer ${accessToken}`,
			},
		});
		return response.json();
	};

	return <button onClick={fetchProtectedData}>Fetch Protected Data</button>;
}

Styling Components

The pre-built components use minimal inline styles. Override with your own CSS:

<SignInForm className="my-custom-form" />
globals.css
.my-custom-form input {
	@apply border-2 border-gray-300 rounded-lg px-4 py-2;
}

.my-custom-form button {
	@apply bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded;
}

Or build custom forms using the useAuth hook directly:

"use client";

import { useAuth } from "@meetploy/auth-react";
import { useState } from "react";

export function CustomSignInForm() {
	const { signIn } = useAuth();
	const [email, setEmail] = useState("");
	const [password, setPassword] = useState("");
	const [error, setError] = useState("");

	const handleSubmit = async (e: React.FormEvent) => {
		e.preventDefault();
		try {
			await signIn(email, password);
			// Handle success
		} catch (err) {
			setError(err instanceof Error ? err.message : "Sign in failed");
		}
	};

	return <form onSubmit={handleSubmit}>{/* Your custom UI */}</form>;
}

Token Storage

The AuthProvider automatically:

  • Stores tokens in localStorage
  • Refreshes tokens before expiration
  • Clears tokens on sign out
  • Restores session on page reload

For enhanced security, consider storing refresh tokens in httpOnly cookies using a custom backend endpoint.

Error Handling

Common error responses from auth endpoints:

StatusErrorDescription
400Invalid email formatEmail validation failed
400Password must be at least 8 charactersPassword too short
401Invalid credentialsWrong email or password
401Invalid or expired tokenToken verification failed
409User already existsEmail already registered

Handle errors in your components:

<SignInForm
	onError={(error) => {
		if (error.message === "Invalid credentials") {
			toast.error("Wrong email or password");
		} else {
			toast.error("Something went wrong");
		}
	}}
/>

Full Example

See the complete example at examples/nextjs-auth.

Next Steps

How is this guide?

Last updated on