Skip to main content
Deltalytix uses Supabase Auth with multiple authentication providers to offer flexible sign-in options.

Supabase Auth Setup

Create Supabase Project

  1. Sign up at https://supabase.com
  2. Create a new project
  3. Note your project URL and API keys from Settings > API

Configure Environment Variables

.env
NEXT_PUBLIC_SUPABASE_URL="https://your-project.supabase.co"
NEXT_PUBLIC_SUPABASE_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
The NEXT_PUBLIC_SUPABASE_ANON_KEY is safe to expose in client-side code. It provides Row Level Security (RLS) protection.

Server Client Configuration

The authentication system uses server-side rendering with cookie-based sessions:
server/auth.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export async function createClient() {
  const cookieStore = await cookies()

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            )
          } catch {
            // Ignore errors from Server Components
          }
        },
      },
    }
  )
}

Discord OAuth

Create Discord Application

  1. Go to https://discord.com/developers/applications
  2. Click New Application
  3. Navigate to OAuth2 settings
  4. Add redirect URI:
    • Development: http://localhost:3000/api/auth/callback
    • Production: https://yourdomain.com/api/auth/callback
  5. Copy your Client ID and Client Secret

Configure Discord in Supabase

  1. In Supabase Dashboard, go to Authentication > Providers
  2. Enable Discord
  3. Enter your Discord Client ID and Client Secret
  4. Save changes

Environment Variables

.env
DISCORD_ID="123456789012345678"
DISCORD_SECRET="your_discord_secret_here"
REDIRECT_URL="http://localhost:3000/api/auth/callback"
Keep DISCORD_SECRET secure. Never commit to version control or expose in client-side code.

Implementation

Sign in with Discord:
server/auth.ts:74
export async function signInWithDiscord(next: string | null = null, locale?: string) {
  const supabase = await createClient()
  const websiteURL = await getWebsiteURL()
  const callbackParams = new URLSearchParams()
  if (next) callbackParams.set('next', next)
  if (locale) callbackParams.set('locale', locale)
  const { data, error } = await supabase.auth.signInWithOAuth({
    provider: 'discord',
    options: {
      redirectTo: `${websiteURL}api/auth/callback/${callbackParams.toString() ? `?${callbackParams.toString()}` : ''}`,
    },
  })
  if (data.url) {
    redirect(data.url)
  }
}

Google OAuth

Create Google Cloud Project

  1. Go to https://console.cloud.google.com
  2. Create a new project or select existing
  3. Enable Google+ API
  4. Go to Credentials > Create Credentials > OAuth Client ID
  5. Configure consent screen
  6. Add authorized redirect URIs:
    • Development: http://localhost:3000/api/auth/callback
    • Production: https://yourdomain.com/api/auth/callback
  7. Copy your Client ID and Client Secret

Configure Google in Supabase

  1. In Supabase Dashboard, go to Authentication > Providers
  2. Enable Google
  3. Enter your Google Client ID and Client Secret
  4. Save changes

Implementation

Sign in with Google:
server/auth.ts:91
export async function signInWithGoogle(next: string | null = null, locale?: string) {
  const supabase = await createClient()
  const websiteURL = await getWebsiteURL()
  const callbackParams = new URLSearchParams()
  if (next) callbackParams.set('next', next)
  if (locale) callbackParams.set('locale', locale)
  const { data, error } = await supabase.auth.signInWithOAuth({
    provider: 'google',
    options: {
      queryParams: {
        prompt: 'select_account',
      },
      redirectTo: `${websiteURL}api/auth/callback/${callbackParams.toString() ? `?${callbackParams.toString()}` : ''}`,
    },
  })
  if (data.url) {
    redirect(data.url)
  }
}
Email-based passwordless authentication:
server/auth.ts:117
export async function signInWithEmail(email: string, next: string | null = null, locale?: string) {
  try {
    const supabase = await createClient()
    const websiteURL = await getWebsiteURL()
    const callbackParams = new URLSearchParams()
    if (next) callbackParams.set('next', next)
    if (locale) callbackParams.set('locale', locale)
    const { error } = await supabase.auth.signInWithOtp({
      email: email,
      options: {
        emailRedirectTo: `${websiteURL}api/auth/callback/${callbackParams.toString() ? `?${callbackParams.toString()}` : ''}`,
      },
    })
    if (error) {
      throw new Error(error.message)
    }
  } catch (error: any) {
    handleAuthError(error)
  }
}

Email Configuration

Configure email templates in Supabase:
  1. Go to Authentication > Email Templates
  2. Customize the Magic Link template
  3. Ensure redirect URL matches your callback URL

Password Authentication

Sign Up with Password

server/auth.ts:279
export async function signUpWithPasswordAction(
  email: string,
  password: string,
  next: string | null = null,
  locale?: string
) {
  try {
    const supabase = await createClient()
    const websiteURL = await getWebsiteURL()
    const callbackParams = new URLSearchParams()
    if (next) callbackParams.set('next', next)
    if (locale) callbackParams.set('locale', locale)
    const { data, error } = await supabase.auth.signUp({
      email,
      password,
      options: {
        emailRedirectTo: `${websiteURL}api/auth/callback/${callbackParams.toString() ? `?${callbackParams.toString()}` : ''}`,
      },
    })
    if (error) {
      throw new Error(error.message)
    }
    
    // Auto sign-in if email confirmation is disabled
    if (data.user && data.session) {
      await ensureUserInDatabase(data.user, locale)
    }
    
    return { success: true, next }
  } catch (error: any) {
    handleAuthError(error)
  }
}

Sign In with Password

The signInWithPasswordAction function handles:
  • Existing users with passwords
  • New user registration (auto-creates account)
  • Users without passwords (triggers password reset)
See implementation at server/auth.ts:140.

Password Requirements

Configure in Supabase Dashboard under Authentication > Policies:
  • Minimum length: 8 characters
  • Optional: Require uppercase, lowercase, numbers, special characters

Session Management

Getting Current User

server/auth.ts:532
export async function getUserId(): Promise<string> {
  // First try middleware headers (optimized)
  const headersList = await headers()
  const userIdFromMiddleware = headersList.get("x-user-id")

  if (userIdFromMiddleware) {
    return userIdFromMiddleware
  }

  // Fallback to Supabase call
  const supabase = await createClient()
  const { data: { user }, error } = await supabase.auth.getUser()

  if (error || !user) {
    throw new Error("User not authenticated")
  }

  return user.id
}

Sign Out

server/auth.ts:111
export async function signOut() {
  const supabase = await createClient()
  await supabase.auth.signOut()
  redirect('/')
}

User Database Synchronization

Deltalytix maintains a user record in PostgreSQL linked to Supabase Auth:
server/auth.ts:367
export async function ensureUserInDatabase(user: User, locale?: string) {
  // Find existing user by auth_user_id
  const existingUserByAuthId = await prisma.user.findUnique({
    where: { auth_user_id: user.id },
  });

  // Update if exists
  if (existingUserByAuthId) {
    if (existingUserByAuthId.email !== user.email || locale !== existingUserByAuthId.language) {
      return await prisma.user.update({
        where: { auth_user_id: user.id },
        data: {
          email: user.email || existingUserByAuthId.email,
          language: locale || existingUserByAuthId.language
        },
      });
    }
    return existingUserByAuthId;
  }

  // Create new user
  const newUser = await prisma.user.create({
    data: {
      auth_user_id: user.id,
      email: user.email || '',
      id: user.id,
      language: locale || 'en'
    },
  });
  
  // Create default dashboard layout
  await createDefaultDashboardLayout(user.id);
  
  return newUser;
}
This function:
  • Syncs user data from Supabase Auth to PostgreSQL
  • Updates email and language preferences
  • Creates default dashboard layout for new users
  • Handles account conflicts gracefully

Identity Linking

Allow users to link multiple authentication providers:
server/auth.ts:599
export async function linkDiscordAccount() {
  const supabase = await createClient()
  const websiteURL = await getWebsiteURL()
  const { data, error } = await supabase.auth.linkIdentity({
    provider: 'discord',
    options: {
      redirectTo: `${websiteURL}api/auth/callback?action=link`,
    },
  })
  if (data.url) {
    redirect(data.url)
  }
}
server/auth.ts:616
export async function linkGoogleAccount() {
  const supabase = await createClient()
  const websiteURL = await getWebsiteURL()
  const { data, error } = await supabase.auth.linkIdentity({
    provider: 'google',
    options: {
      redirectTo: `${websiteURL}api/auth/callback?action=link`,
    },
  })
  if (data.url) {
    redirect(data.url)
  }
}
server/auth.ts:633
export async function unlinkIdentity(identity: any) {
  const supabase = await createClient()
  const { error } = await supabase.auth.unlinkIdentity(identity)
  if (error) {
    throw new Error(error.message)
  }
  return { success: true }
}

Security Considerations

Row Level Security (RLS)

Enable RLS policies in Supabase for all tables:
-- Example: Users can only access their own data
CREATE POLICY "Users can view their own data"
  ON users FOR SELECT
  USING (auth.uid() = auth_user_id);

Encryption Key

Set a secure encryption key for sensitive data:
.env
# Generate with: openssl rand -base64 32
ENCRYPTION_KEY="your_secure_random_key_here"
Changing the encryption key will make existing encrypted data unreadable. Back up data before rotating keys.

HTTPS Requirements

Always use HTTPS in production:
  • Protects session cookies
  • Prevents token interception
  • Required for OAuth providers

Troubleshooting

”User not authenticated” Error

Cause: Session expired or invalid cookies Solution:
  • Check cookie settings in middleware
  • Verify NEXT_PUBLIC_SUPABASE_URL is correct
  • Ensure cookies are enabled in browser

OAuth Redirect Mismatch

Cause: Redirect URI doesn’t match provider configuration Solution:
  • Verify REDIRECT_URL matches exactly (including protocol and trailing slash)
  • Check provider settings (Discord/Google)
  • Update Supabase provider configuration

”Invalid JSON” Error

Cause: Supabase API returning HTML error pages Solution: The application handles this gracefully in server/auth.ts:26:
function handleAuthError(error: any): never {
  if (error?.message?.includes('Unexpected token') ||
      error?.message?.includes('is not valid JSON')) {
    throw new Error(
      'Authentication service is temporarily unavailable. Please try again in a few moments.'
    )
  }
  throw error
}

Account Conflict Error

Cause: Email already associated with different auth method Solution:
  • Sign in with original auth method
  • Use identity linking to connect additional providers
  • Contact support if unable to access original account

Next Steps

Build docs developers (and LLMs) love