API Keys and Environment Setup
Safe handling of Supabase credentials and setting up your development environment
Last updated: 8/15/2025
Supabase uses API keys to control access to your database. Understanding the different key types and keeping them secure is crucial for your app's safety.
API Key Types
Anon Key
Safe for client-side code when RLS is properly configured.
// This key can be public in your frontend
const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
Capabilities:
- Read data allowed by RLS policies
- Write data allowed by RLS policies
- Sign up new users
- Sign in existing users
Limitations:
- Cannot bypass RLS policies
- Cannot access admin functions
- Limited to what your RLS policies allow
Service Role Key
Server-only secret with full database access.
// This key must NEVER be exposed publicly
const SUPABASE_SERVICE_ROLE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
Capabilities:
- Full read/write access to all tables
- Bypasses all RLS policies
- Can manage users and authentication
- Can call any database function
Security Rule: Keep this server-only, never in client code.
Environment Variable Setup
Local Development
Create a .env.local file (never commit this):
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Add to your .gitignore:
# .gitignore
.env.local
.env*.local
*.env
Shareable Template
Create .env.example for your team (safe to commit):
# .env.example
NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
Production Deployment
Set environment variables in your hosting platform:
Vercel:
- Project Settings (Environment Variables)
- Add each variable with appropriate values
Netlify:
- Site Settings (Environment Variables)
- Build environment variables
Railway/Render/Fly:
- Platform-specific environment variable settings
Client Initialization
Frontend Client (Safe for Browsers)
// lib/supabase.js
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
export const supabase = createClient(supabaseUrl, supabaseKey);
Server Client (Backend Only)
// lib/supabase-admin.js
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
export const supabaseAdmin = createClient(
supabaseUrl,
supabaseServiceKey,
{
auth: {
autoRefreshToken: false,
persistSession: false
}
}
);
Using Different Clients
Client-Side Operations (RLS Protected)
import { supabase } from '@/lib/supabase';
// This respects RLS policies
const { data: userPosts } = await supabase
.from('posts')
.select('*')
.eq('author_id', user.id);
// User authentication
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'password'
});
Server-Side Operations (Admin Access)
import { supabaseAdmin } from '@/lib/supabase-admin';
// API route or server function
export async function GET(request) {
// This bypasses RLS - be careful!
const { data: allPosts } = await supabaseAdmin
.from('posts')
.select('*');
return Response.json(allPosts);
}
// Admin user management
const { data: users } = await supabaseAdmin.auth.admin.listUsers();
Environment-Specific Setup
Multiple Environments
Create separate Supabase projects for different environments:
# .env.development
NEXT_PUBLIC_SUPABASE_URL=https://dev-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=dev_anon_key...
# .env.staging
NEXT_PUBLIC_SUPABASE_URL=https://staging-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=staging_anon_key...
# .env.production
NEXT_PUBLIC_SUPABASE_URL=https://prod-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=prod_anon_key...
Loading Environment Variables
// config/supabase.js
const getSupabaseConfig = () => {
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
if (!url || !anonKey) {
throw new Error('Missing Supabase environment variables');
}
return { url, anonKey };
};
export const { url: supabaseUrl, anonKey: supabaseAnonKey } = getSupabaseConfig();
Framework-Specific Examples
Next.js (App Router)
// app/lib/supabase.js
import { createClient } from '@supabase/supabase-js';
export function createClientComponentClient() {
return createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
);
}
// app/lib/supabase-server.js
export function createServerComponentClient() {
return createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.SUPABASE_SERVICE_ROLE_KEY,
{
auth: {
persistSession: false
}
}
);
}
React (Vite)
// src/lib/supabase.js
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
export const supabase = createClient(supabaseUrl, supabaseKey);
Node.js/Express
// server/config/supabase.js
require('dotenv').config();
const { createClient } = require('@supabase/supabase-js');
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_ROLE_KEY
);
module.exports = { supabase };
Security Best Practices
Never Log Secrets
// Bad: secrets in logs
console.log('Supabase config:', {
url: process.env.NEXT_PUBLIC_SUPABASE_URL,
key: process.env.SUPABASE_SERVICE_ROLE_KEY // DON'T DO THIS
});
// Good: hide sensitive values
console.log('Supabase config:', {
url: process.env.NEXT_PUBLIC_SUPABASE_URL,
keyLength: process.env.SUPABASE_SERVICE_ROLE_KEY?.length
});
Validate Environment Variables
// utils/env-validation.js
const requiredEnvVars = [
'NEXT_PUBLIC_SUPABASE_URL',
'NEXT_PUBLIC_SUPABASE_ANON_KEY'
];
const validateEnv = () => {
const missing = requiredEnvVars.filter(
envVar => !process.env[envVar]
);
if (missing.length > 0) {
throw new Error(`Missing environment variables: ${missing.join(', ')}`);
}
};
// Call during app startup
validateEnv();
Use Different Keys for Different Environments
Never reuse production keys in development or staging:
// Different projects = different keys = data isolation
const configs = {
development: {
url: 'https://dev-project.supabase.co',
key: 'dev-specific-key'
},
production: {
url: 'https://prod-project.supabase.co',
key: 'prod-specific-key'
}
};
Testing Your Setup
Verify Connection
// Test basic connection
const testConnection = async () => {
try {
const { data, error } = await supabase
.from('_test')
.select('*')
.limit(1);
if (error && error.code === '42P01') {
console.log('✅ Connected to Supabase (table not found is expected)');
} else {
console.log('✅ Connected to Supabase');
}
} catch (err) {
console.error('❌ Supabase connection failed:', err.message);
}
};
Verify Environment Separation
// Make sure you're not accidentally using prod data in dev
const verifyEnvironment = async () => {
const { data } = await supabase
.from('organisations')
.select('name')
.limit(5);
console.log('Connected to environment with orgs:',
data?.map(org => org.name)
);
};
Common Setup Issues
Missing Environment Variables
Error: supabaseUrl is required
Fix: Check your .env file exists and variables are named correctly.
Wrong Variable Names
# Wrong
SUPABASE_URL=...
SUPABASE_ANON_KEY=...
# Right
NEXT_PUBLIC_SUPABASE_URL=...
NEXT_PUBLIC_SUPABASE_ANON_KEY=...
Service Key in Frontend
Error: This looks like a service_role key
Fix: Only use anon keys in client-side code.
Proper environment setup is the foundation of secure Supabase usage. Take time to get this right from the start (it prevents security issues and deployment headaches later).
Related Topics
Continue building your security knowledge:
- Supabase Client Patterns - Common usage patterns
- Security Leak Prevention - Preventing API key leaks
- Security Incident Response - Emergency procedures for leaks
- Multi-tenant Security Patterns - Team and organisation data isolation