Core Setup

Authentication

Set up Google OAuth, magic link sign-in, platform access modes, role-based access control, and admin impersonation.

MoveFast uses Auth.js (NextAuth v5) with two sign-in methods out of the box: Google OAuth and email magic links.

Environment Variables

env
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-secret

Generate AUTH_SECRET with:

bash
openssl rand -base64 32

For production, update NEXTAUTH_URL to your deployed domain.

Google OAuth Setup

  1. Go to Google Cloud Console
  2. Create a new project (or use an existing one)
  3. Navigate to APIs & Services > Credentials
  4. Create an OAuth 2.0 Client ID (Web application type)
  5. Add authorized redirect URIs:
text
http://localhost:3000/api/auth/callback/google
https://yourdomain.com/api/auth/callback/google
  1. 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:

  1. User enters their email on the sign-in page
  2. Server generates a random token (32 bytes, 15-minute expiry)
  3. Token is stored in the database
  4. An email is sent with a clickable link containing the token
  5. 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:

typescript
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:

typescript
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:

typescript
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:

typescript
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, user can be null
  • AccessLevel.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:

  1. Admin clicks "Impersonate" on a user in the admin dashboard
  2. A signed, HttpOnly cookie is set (HMAC-SHA256, 4-hour expiry)
  3. All subsequent requests resolve to the impersonated user
  4. A banner appears at the top showing who is being impersonated
  5. 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):

  1. Install the provider if needed (most are built into Auth.js)
  2. Open src/lib/auth.ts
  3. Add the provider to the providers array:
typescript
import GitHub from "next-auth/providers/github";
 
providers: [
    Google({ ... }),
    GitHub({
        clientId: process.env.GITHUB_CLIENT_ID!,
        clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    }),
    Credentials({ ... }),
],
  1. Add the env variables to src/constants/env.ts and your .env
  2. The signIn callback 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).