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
Both forms read the project's sign-in methods and render what is enabled: Google, Apple, and GitHub buttons, a passkey option, and the email and password fields. Turning a method on or off in the dashboard updates the forms without a redeploy.
SignInForm
A sign-in form with every enabled method:
"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 sign-up form with every enabled method. When passkeys are on, users can create a passkey instead of a password:
"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 until the session and config are loaded
isAuthenticated: boolean; // True if a user is signed in
config: AuthConfig | null; // Enabled sign-in methods
redirectError: string | null; // ploy_auth_error code after a failed social sign-in
signUp: (
email: string,
password: string,
metadata?: Record<string, unknown>,
) => Promise<PloyUser>;
signIn: (email: string, password: string) => Promise<PloyUser>;
requestPasswordReset: (
email: string,
options?: { redirectTo?: string },
) => Promise<void>;
resetPassword: (token: string, password: string) => Promise<void>;
sendMagicLink: (
email: string,
options?: { redirectTo?: string; metadata?: Record<string, unknown> },
) => Promise<void>;
changePassword: (input: {
currentPassword?: string; // required when user.hasPassword
newPassword: string;
}) => Promise<PloyUser>;
resendVerification: (
email: string,
options?: { redirectTo?: string },
) => Promise<void>;
signInWithProvider: (
provider: "google" | "apple" | "github",
options?: { redirectTo?: string },
) => void;
signInWithPasskey: () => Promise<PloyUser>;
signUpWithPasskey: (
email: string,
metadata?: Record<string, unknown>,
) => Promise<PloyUser>;
addPasskey: (name?: string) => Promise<void>;
listPasskeys: () => Promise<PloyPasskey[]>;
removePasskey: (id: string) => Promise<void>;
signOut: () => Promise<void>;
}The session lives in an HttpOnly cookie, so there is no token to store or refresh in the browser.
Failed calls reject with a PloyAuthError whose code matches the error codes below. See Emails, sign-in links and passwords for the email and password methods.
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
The ploy_session cookie is sent with every same-origin request. Verify it on the server with the Next.js helpers:
import { auth } from "@meetploy/nextjs/auth";
export async function GET() {
const { user } = await auth();
if (!user) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
return Response.json({ message: "Protected data", userId: user.id });
}Client components call it with a plain fetch("/api/protected"); no Authorization header is needed. In Server Components use currentUser(), or protect() to redirect signed-out visitors to sign-in.
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>;
}Error Handling
Common error responses from auth endpoints:
| Status | code | Description |
|---|---|---|
| 400 | invalid_email | Email validation failed |
| 400 | weak_password | Password shorter than 8 characters |
| 401 | invalid_credentials | Wrong email or password |
| 401 | unauthorized | Missing, expired, or revoked session |
| 400 | invalid_token | Reset, verification, or sign-in link is invalid, expired, or used |
| 400 | name_required | The project requires a name for new accounts |
| 403 | email_not_verified | The project requires a verified email; SignInForm offers to resend it |
| 403 | password_disabled | Email and password sign-in is turned off |
| 403 | signups_disabled | Sign-ups are turned off |
| 403 | magic_link_disabled | Sign-in links are turned off |
| 403 | email_unavailable | Auth emails are off until email is enabled for the organization |
| 409 | user_exists | Email already registered |
| 429 | rate_limited | Too many attempts; see Retry-After |
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 - Endpoints, settings, and security
- Social sign-in - Google, Apple, and GitHub setup
- Passkeys - Passwordless sign-in
- Emails, sign-in links and passwords - Verification links, sign-in links, password reset and change, and the local inbox
- Users and data - Tables, password hashing, and user IDs
- Database - Add a database to your app
- Routes - Define type-safe API routes
How is this guide?
Last updated on