Skip to main content

Overview

Deltalytix uses Supabase as its backend-as-a-service platform, providing authentication, PostgreSQL database access, file storage, and real-time capabilities.

Configuration

Environment Variables

Add these variables to your .env file:
# Supabase Configuration
NEXT_PUBLIC_SUPABASE_URL='https://your-project.supabase.co'
NEXT_PUBLIC_SUPABASE_ANON_KEY='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'

# Database Configuration
DATABASE_URL="postgresql://username:password@host:port/database?pgbouncer=true&connection_limit=1"
DIRECT_URL="postgresql://username:password@host:port/database"
The NEXT_PUBLIC_ prefix makes these variables accessible in client-side code. The anon key is safe to expose as it respects Row Level Security (RLS) policies.

Client Initialization

Browser Client

For client-side components (lib/supabase.ts):
lib/supabase.ts
import { createBrowserClient } from '@supabase/ssr'

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
}

Server Client

For server-side operations (server/auth.ts):
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
          }
        },
      },
    }
  )
}
Always use createServerClient for server-side code and createBrowserClient for client-side code. Mixing them will cause authentication issues.

Authentication

Supported Providers

Deltalytix supports multiple authentication methods:
  • Email/Password
  • Magic Link (OTP)
  • OAuth (Google, Discord)

Email/Password Authentication

Sign Up with Password

export async function signUpWithPasswordAction(
  email: string,
  password: string,
  next: string | null = null,
  locale?: string
) {
  const supabase = await createClient()
  const websiteURL = await getWebsiteURL()
  
  const { data, error } = await supabase.auth.signUp({
    email,
    password,
    options: {
      emailRedirectTo: `${websiteURL}api/auth/callback`,
    },
  })
  
  if (error) throw new Error(error.message)
  
  // If email confirmation is disabled, user is auto-signed in
  if (data.user && data.session) {
    await ensureUserInDatabase(data.user, locale)
  }
  
  return { success: true, next }
}

Sign In with Password

export async function signInWithPasswordAction(
  email: string,
  password: string,
  next: string | null = null,
  locale?: string
) {
  const supabase = await createClient()
  const { data, error } = await supabase.auth.signInWithPassword({ 
    email, 
    password 
  })
  
  if (error) {
    // Auto-create account if user doesn't exist
    if (error.message.includes('Invalid login credentials')) {
      const { data: signUpData, error: signUpError } = 
        await supabase.auth.signUp({ email, password })
      
      if (signUpError) throw new Error(signUpError.message)
    } else {
      throw new Error(error.message)
    }
  }

  const { data: { user } } = await supabase.auth.getUser()
  if (user) {
    await ensureUserInDatabase(user, locale)
  }

  return { success: true, next }
}
The sign-in function automatically creates an account if the user doesn’t exist, providing a seamless experience.
export async function signInWithEmail(
  email: string,
  next: string | null = null,
  locale?: string
) {
  const supabase = await createClient()
  const websiteURL = await getWebsiteURL()
  
  const { error } = await supabase.auth.signInWithOtp({
    email: email,
    options: {
      emailRedirectTo: `${websiteURL}api/auth/callback`,
    },
  })
  
  if (error) throw new Error(error.message)
}

Verify OTP

export async function verifyOtp(
  email: string,
  token: string,
  type: 'email' | 'signup' = 'email'
) {
  const supabase = await createClient()
  
  const { data, error } = await supabase.auth.verifyOtp({
    email,
    token,
    type
  })

  if (data.user && data.session) {
    await ensureUserInDatabase(data.user, locale)
  }

  if (error) throw new Error(error.message)
  return data
}

OAuth Authentication

Google Sign-In

export async function signInWithGoogle(
  next: string | null = null,
  locale?: string
) {
  const supabase = await createClient()
  const websiteURL = await getWebsiteURL()
  
  const { data, error } = await supabase.auth.signInWithOAuth({
    provider: 'google',
    options: {
      queryParams: {
        prompt: 'select_account',
      },
      redirectTo: `${websiteURL}api/auth/callback`,
    },
  })
  
  if (data.url) {
    redirect(data.url)
  }
}

Discord Sign-In

export async function signInWithDiscord(
  next: string | null = null,
  locale?: string
) {
  const supabase = await createClient()
  const websiteURL = await getWebsiteURL()
  
  const { data, error } = await supabase.auth.signInWithOAuth({
    provider: 'discord',
    options: {
      redirectTo: `${websiteURL}api/auth/callback`,
    },
  })
  
  if (data.url) {
    redirect(data.url)
  }
}

Account Linking

Users can link multiple authentication providers to a single account:
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)
  if (error) throw new Error(error.message)
}

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 }
}

User Management

Get Current User

export async function getUserId(): Promise<string> {
  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

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

Update User Password

export async function setPasswordAction(newPassword: string) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  
  if (!user) throw new Error('User not authenticated')
  
  const { data, error } = await supabase.auth.updateUser({ 
    password: newPassword 
  })
  
  if (error) throw new Error(error.message)
  return { success: true }
}

Database Integration

User Synchronization

The ensureUserInDatabase() function syncs Supabase Auth users with your application database:
export async function ensureUserInDatabase(user: User, locale?: string) {
  if (!user?.id) {
    await signOut()
    throw new Error('User ID is required')
  }

  // Find user by auth_user_id
  const existingUserByAuthId = await prisma.user.findUnique({
    where: { auth_user_id: user.id },
  })

  if (existingUserByAuthId) {
    // Update if needed
    const shouldUpdateEmail = existingUserByAuthId.email !== user.email
    const shouldUpdateLanguage = !!locale && locale !== existingUserByAuthId.language

    if (shouldUpdateEmail || shouldUpdateLanguage) {
      return await prisma.user.update({
        where: { auth_user_id: user.id },
        data: {
          email: shouldUpdateEmail ? user.email : existingUserByAuthId.email,
          language: shouldUpdateLanguage ? 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 is called automatically after successful authentication to ensure consistency between Supabase Auth and your application database.

Database Connection

Deltalytix uses Prisma with Supabase’s PostgreSQL database:
import { PrismaClient } from '@prisma/client'
import { PrismaPg } from '@prisma/adapter-pg'
import pg from 'pg'

const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL,
})

const adapter = new PrismaPg(pool)
const prisma = new PrismaClient({ adapter })

Security Best Practices

Row Level Security (RLS)

Always enable Row Level Security on tables containing user data:
ALTER TABLE users ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users can view own data"
  ON users FOR SELECT
  USING (auth.uid() = auth_user_id);

CREATE POLICY "Users can update own data"
  ON users FOR UPDATE
  USING (auth.uid() = auth_user_id);
The service role key bypasses RLS. Only use it in trusted server-side code:
// DON'T use service role key in client-side code
// DO use it for admin operations on the server
const supabaseAdmin = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY! // Server-side only
)
Always validate and sanitize user input before database operations:
import { z } from 'zod'

const userSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1).max(100),
})

const validatedData = userSchema.parse(userInput)

Authentication Security

  1. Session Management: Sessions are automatically managed via HTTP-only cookies
  2. Token Refresh: Access tokens are automatically refreshed before expiration
  3. PKCE Flow: OAuth uses PKCE for enhanced security
  4. Rate Limiting: Enable rate limiting in Supabase Dashboard

Error Handling

Handle authentication errors gracefully:
function handleAuthError(error: any): never {
  // Check for JSON parsing errors (indicates service issues)
  if (
    error?.message?.includes('Unexpected token') ||
    error?.message?.includes('is not valid JSON')
  ) {
    throw new Error(
      'Authentication service is temporarily unavailable. Please try again.'
    )
  }
  
  // Re-throw other errors
  throw error
}

Testing

Local Development

  1. Create a separate Supabase project for development
  2. Use test credentials in .env.local:
NEXT_PUBLIC_SUPABASE_URL='https://test-project.supabase.co'
NEXT_PUBLIC_SUPABASE_ANON_KEY='eyJhbGciOiJIUz...'

Test Users

Create test users in your development Supabase project:
// Test user creation
const { data, error } = await supabase.auth.signUp({
  email: '[email protected]',
  password: 'test-password-123',
  options: {
    data: {
      full_name: 'Test User',
    }
  }
})

Database Migrations

Run migrations using Prisma:
# Generate migration
pnpm prisma migrate dev --name add_new_field

# Apply migrations
pnpm prisma migrate deploy

# Reset database (development only)
pnpm prisma migrate reset

Common Issues

  • Check that callback URL is correctly configured in Supabase Dashboard
  • Verify NEXT_PUBLIC_SITE_URL environment variable
  • Ensure cookies are enabled in the browser
  • Use connection pooling with pgbouncer=true in connection string
  • Set appropriate connection_limit (recommended: 1 for serverless)
  • Check Supabase project limits in Dashboard
  • Verify user is authenticated: supabase.auth.getUser()
  • Check RLS policies match your query patterns
  • Use service role key for admin operations (server-side only)
  • Ensure ensureUserInDatabase() is called after authentication
  • Check for unique constraint violations
  • Verify auth_user_id column exists and is indexed

Additional Resources

Supabase Documentation

Official Supabase documentation

Supabase Auth Guide

Complete authentication guide

Row Level Security

RLS policies and best practices

Supabase Dashboard

Manage your Supabase projects

Build docs developers (and LLMs) love