MoveFast uses Auth.js (NextAuth v5) with two sign-in methods out of the box: Google OAuth and email magic links.
Environment Variables
AUTH_SECRET=your-generated-secret
AUTH_TRUST_HOST=true
NEXTAUTH_URL=http://localhost:3000
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secretGenerate AUTH_SECRET with:
openssl rand -base64 32For production, update NEXTAUTH_URL to your deployed domain.
Google OAuth Setup
- Go to Google Cloud Console
- Create a new project (or use an existing one)
- Navigate to APIs & Services > Credentials
- Create an OAuth 2.0 Client ID (Web application type)
- Add authorized redirect URIs:
http://localhost:3000/api/auth/callback/google
https://yourdomain.com/api/auth/callback/google- Copy the Client ID and Client Secret into your
.env
Magic Link Sign-In
Magic links let users sign in without a password. Here is how the flow works:
- User enters their email on the sign-in page
- Server generates a random token (32 bytes, 15-minute expiry)
- Token is stored in the database
- An email is sent with a clickable link containing the token
- User clicks the link, which verifies the token and creates a session
Magic links require a working email provider. Make sure your email setup is complete before testing this flow.
Platform Access Modes
MoveFast supports two access modes, configured in src/config/platform.config.ts:
export const PLATFORM_ACCESS_MODE = PlatformAccessMode.WAITLIST_WITH_INVITES;WAITLIST_WITH_INVITES (default)
- New users cannot sign up freely
- They must be invited by an admin (via the invite system)
- A waitlist form is shown on the landing page for collecting emails
- Existing users (already in the database) can always log in
This is great for pre-launch. Collect emails, then invite users in batches.
OPEN
- Anyone can sign up without restrictions
- No waitlist form is shown
- Standard signup experience
Switch to OPEN when you are ready for public launch:
export const PLATFORM_ACCESS_MODE = PlatformAccessMode.OPEN;Role-Based Access Control
Every user has a roles array. The built-in roles are:
- USER: Default role assigned to all new users
- ADMIN: Can access the admin dashboard and manage users
Protecting Pages (Server Components)
Use checkAuth() in page components:
import { checkAuth } from "@/lib/auth/page-auth";
import { AccessLevel } from "@/lib/types/auth.enums";
import { UserRole } from "@/lib/types/auth.enums";
// Require any authenticated user
const user = await checkAuth({ access: AccessLevel.AUTHENTICATED });
// Require admin role
const user = await checkAuth({ access: { roles: [UserRole.ADMIN] } });If the user does not meet the access requirement, they are automatically redirected.
Protecting API Routes
Use withAuth() in route handlers:
import { withAuth } from "@/lib/auth/api-auth";
import { AccessLevel } from "@/lib/types/auth.enums";
export const GET = withAuth(AccessLevel.AUTHENTICATED, async ({ req, user }) => {
// user is guaranteed to be authenticated here
return Response.json({ userId: user.id });
});Access levels:
AccessLevel.PUBLIC- no auth required,usercan be nullAccessLevel.AUTHENTICATED- must be logged in{ roles: [UserRole.ADMIN] }- must have the ADMIN role- Custom predicate function:
(user) => user.planId !== null
Admin Impersonation
Admins can "impersonate" any non-admin user to see the app exactly as that user sees it. This is useful for debugging user-reported issues.
How it works:
- Admin clicks "Impersonate" on a user in the admin dashboard
- A signed, HttpOnly cookie is set (HMAC-SHA256, 4-hour expiry)
- All subsequent requests resolve to the impersonated user
- A banner appears at the top showing who is being impersonated
- Admin clicks "Stop" to end impersonation
Security constraints:
- Only users with the ADMIN role can impersonate
- Cannot impersonate other admins
- Cannot impersonate inactive users
- Cookie is cryptographically signed to prevent tampering
- Impersonation auto-expires after 4 hours
- Signing out clears impersonation
Adding a New OAuth Provider
Auth.js supports many providers. To add one (e.g., GitHub):
- Install the provider if needed (most are built into Auth.js)
- Open
src/lib/auth.ts - Add the provider to the
providersarray:
import GitHub from "next-auth/providers/github";
providers: [
Google({ ... }),
GitHub({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}),
Credentials({ ... }),
],- Add the env variables to
src/constants/env.tsand your.env - The
signIncallback handles new user creation automatically
Sign-In Page
The sign-in page lives at /signin and shows:
- Google sign-in button
- Magic link email form
- Context-aware messaging based on platform access mode
There is also a SignInModal component that can be triggered from anywhere in the app using useAppContext().setIsLoginDialogOpen(true).
