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-react2. Configure Auth Binding
Add the auth configuration to your ploy.yaml:
kind: dynamic
build: pnpm build
out: dist
auth:
binding: AUTH_DB3. Add AuthProvider
Wrap your application with the AuthProvider:
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:
"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:
"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:
"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
"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
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" />.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:
| Status | Error | Description |
|---|---|---|
| 400 | Invalid email format | Email validation failed |
| 400 | Password must be at least 8 characters | Password too short |
| 401 | Invalid credentials | Wrong email or password |
| 401 | Invalid or expired token | Token verification failed |
| 409 | User already exists | Email 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
- Auth Feature Overview - Learn about auth endpoints and security
- Database - Add a database to your app
- Routes - Define type-safe API routes
How is this guide?
Last updated on