Deltalytix uses Supabase Auth with multiple authentication providers to offer flexible sign-in options.
Supabase Auth Setup
Create Supabase Project
- Sign up at https://supabase.com
- Create a new project
- Note your project URL and API keys from Settings > API
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:
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
- Go to https://discord.com/developers/applications
- Click New Application
- Navigate to OAuth2 settings
- Add redirect URI:
- Development:
http://localhost:3000/api/auth/callback
- Production:
https://yourdomain.com/api/auth/callback
- Copy your Client ID and Client Secret
- In Supabase Dashboard, go to Authentication > Providers
- Enable Discord
- Enter your Discord Client ID and Client Secret
- Save changes
Environment Variables
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:
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
- Go to https://console.cloud.google.com
- Create a new project or select existing
- Enable Google+ API
- Go to Credentials > Create Credentials > OAuth Client ID
- Configure consent screen
- Add authorized redirect URIs:
- Development:
http://localhost:3000/api/auth/callback
- Production:
https://yourdomain.com/api/auth/callback
- Copy your Client ID and Client Secret
- In Supabase Dashboard, go to Authentication > Providers
- Enable Google
- Enter your Google Client ID and Client Secret
- Save changes
Implementation
Sign in with Google:
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)
}
}
Magic Link Authentication
Email-based passwordless authentication:
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:
- Go to Authentication > Email Templates
- Customize the Magic Link template
- Ensure redirect URL matches your callback URL
Password Authentication
Sign Up with Password
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
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
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:
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:
Link Discord
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)
}
}
Link Google
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)
}
}
Unlink Identity
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:
# 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