🔐 Authentication Overview
NVLP uses magic link authentication powered by Supabase Auth. This provides a secure, passwordless authentication method that's both user-friendly and developer-friendly.
🎯 Key Benefits
- Passwordless - No passwords to remember or manage
- Secure - JWT tokens with automatic expiration
- Mobile-friendly - Works with deep links for mobile apps
- User-friendly - Simple email-based authentication
Authentication Endpoints
Endpoint | Method | Purpose |
---|---|---|
/functions/v1/auth-magic-link |
POST | Send magic link email |
/functions/v1/auth-user |
GET | Get current user profile |
/functions/v1/auth-user-update |
PATCH | Update user profile |
/functions/v1/auth-logout |
POST | Sign out user |
🔗 Magic Link Authentication Flow
The magic link authentication process involves several steps:
- Request Magic Link - Send user's email to the API
- Email Delivery - User receives email with magic link
- Link Click - User clicks link in email
- Token Extraction - App extracts access token from redirect
- API Access - Use token for authenticated requests
Step 1: Request Magic Link
// Send magic link request
const response = await fetch('https://your-project.supabase.co/functions/v1/auth-magic-link', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_SUPABASE_ANON_KEY'
},
body: JSON.stringify({
email: 'user@example.com',
redirectTo: 'https://yourapp.com/auth/callback' // or deep link: myapp://auth/callback
})
});
const result = await response.json();
console.log(result.message); // "Magic link sent successfully"
📱 Mobile App Integration
For mobile apps, use a deep link as the redirectTo
parameter:
- iOS:
nvlp://auth/callback
- Android:
nvlp://auth/callback
Step 2: Handle Magic Link Callback
When the user clicks the magic link, they'll be redirected to your callback URL with authentication tokens in the URL fragment:
// For web apps - extract tokens from URL fragment
function extractTokensFromURL() {
const hash = window.location.hash.substring(1);
const params = new URLSearchParams(hash);
const accessToken = params.get('access_token');
const refreshToken = params.get('refresh_token');
const expiresIn = params.get('expires_in');
if (accessToken) {
// Store tokens securely
localStorage.setItem('nvlp_access_token', accessToken);
localStorage.setItem('nvlp_refresh_token', refreshToken);
// Remove tokens from URL for security
window.history.replaceState({}, document.title, window.location.pathname);
return { accessToken, refreshToken, expiresIn };
}
return null;
}
// For iOS apps - handle deep link in AppDelegate
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
if url.scheme == "nvlp" && url.host == "auth" {
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
let fragment = components?.fragment
if let fragment = fragment {
let params = parseURLFragment(fragment)
if let accessToken = params["access_token"] {
// Store token securely in Keychain
storeTokenInKeychain(accessToken)
// Navigate to authenticated screen
navigateToMainApp()
}
}
}
return true
}
⚙️ Implementation Examples
JavaScript/TypeScript
class NVLPAuth {
constructor(supabaseUrl, anonKey) {
this.supabaseUrl = supabaseUrl;
this.anonKey = anonKey;
this.accessToken = this.getStoredToken();
}
// Send magic link
async sendMagicLink(email, redirectTo = window.location.origin + '/auth/callback') {
const response = await fetch(`${this.supabaseUrl}/functions/v1/auth-magic-link`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.anonKey}`
},
body: JSON.stringify({ email, redirectTo })
});
if (!response.ok) {
throw new Error(`Auth error: ${response.status}`);
}
return response.json();
}
// Get current user
async getCurrentUser() {
if (!this.accessToken) {
throw new Error('No access token available');
}
const response = await fetch(`${this.supabaseUrl}/functions/v1/auth-user`, {
headers: {
'Authorization': `Bearer ${this.accessToken}`
}
});
if (!response.ok) {
if (response.status === 401) {
await this.refreshToken();
return this.getCurrentUser(); // Retry with new token
}
throw new Error(`Failed to get user: ${response.status}`);
}
return response.json();
}
// Make authenticated API request
async apiRequest(endpoint, options = {}) {
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.accessToken}`,
'apikey': this.anonKey,
...options.headers
};
const response = await fetch(`${this.supabaseUrl}${endpoint}`, {
...options,
headers
});
if (response.status === 401) {
await this.refreshToken();
// Retry request with new token
headers['Authorization'] = `Bearer ${this.accessToken}`;
return fetch(`${this.supabaseUrl}${endpoint}`, { ...options, headers });
}
return response;
}
// Token management
getStoredToken() {
return localStorage.getItem('nvlp_access_token');
}
setToken(accessToken, refreshToken) {
this.accessToken = accessToken;
localStorage.setItem('nvlp_access_token', accessToken);
if (refreshToken) {
localStorage.setItem('nvlp_refresh_token', refreshToken);
}
}
async refreshToken() {
// Implement token refresh logic here
// This would typically involve calling Supabase's refresh endpoint
console.warn('Token refresh not implemented - user may need to re-authenticate');
}
logout() {
this.accessToken = null;
localStorage.removeItem('nvlp_access_token');
localStorage.removeItem('nvlp_refresh_token');
}
}
// Usage example
const auth = new NVLPAuth('https://your-project.supabase.co', 'your-anon-key');
// Send magic link
await auth.sendMagicLink('user@example.com');
// After user clicks link and tokens are extracted
auth.setToken(extractedAccessToken, extractedRefreshToken);
// Make authenticated requests
const user = await auth.getCurrentUser();
const budgets = await auth.apiRequest('/rest/v1/budgets');
Python
import requests
import json
from typing import Optional, Dict, Any
class NVLPAuth:
def __init__(self, supabase_url: str, anon_key: str):
self.supabase_url = supabase_url
self.anon_key = anon_key
self.access_token: Optional[str] = None
def send_magic_link(self, email: str, redirect_to: str) -> Dict[str, Any]:
"""Send magic link to user's email"""
response = requests.post(
f"{self.supabase_url}/functions/v1/auth-magic-link",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {self.anon_key}"
},
json={"email": email, "redirectTo": redirect_to}
)
response.raise_for_status()
return response.json()
def set_token(self, access_token: str):
"""Set the access token for authenticated requests"""
self.access_token = access_token
def get_current_user(self) -> Dict[str, Any]:
"""Get current user profile"""
if not self.access_token:
raise ValueError("No access token set")
response = requests.get(
f"{self.supabase_url}/functions/v1/auth-user",
headers={"Authorization": f"Bearer {self.access_token}"}
)
response.raise_for_status()
return response.json()
def api_request(self, endpoint: str, method: str = "GET", data: Optional[Dict] = None) -> requests.Response:
"""Make authenticated API request"""
if not self.access_token:
raise ValueError("No access token set")
headers = {
"Authorization": f"Bearer {self.access_token}",
"apikey": self.anon_key,
"Content-Type": "application/json"
}
response = requests.request(
method=method,
url=f"{self.supabase_url}{endpoint}",
headers=headers,
json=data
)
return response
# Usage example
auth = NVLPAuth("https://your-project.supabase.co", "your-anon-key")
# Send magic link
auth.send_magic_link("user@example.com", "https://myapp.com/callback")
# After token extraction from callback
auth.set_token("extracted_access_token")
# Make authenticated requests
user = auth.get_current_user()
budgets_response = auth.api_request("/rest/v1/budgets")
cURL Examples
# Send magic link
curl -X POST "https://your-project.supabase.co/functions/v1/auth-magic-link" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_ANON_KEY" \
-d '{
"email": "user@example.com",
"redirectTo": "https://myapp.com/auth/callback"
}'
# Get current user (after obtaining access token)
curl -X GET "https://your-project.supabase.co/functions/v1/auth-user" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# Make authenticated API request
curl -X GET "https://your-project.supabase.co/rest/v1/budgets" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "apikey: YOUR_ANON_KEY"
🔄 Token Management
Proper token management is crucial for a good user experience and security:
Token Storage
⚠️ Security Considerations
- Web Apps: Store tokens in localStorage or sessionStorage (not cookies for XSS protection)
- Mobile Apps: Use secure storage (iOS Keychain, Android Keystore)
- Server-side: Store in secure, encrypted databases
- Never: Store tokens in plain text files or unencrypted storage
Token Expiration & Refresh
NVLP uses JWT tokens that expire after a certain time. Here's how to handle token expiration:
// Check if token is expired
function isTokenExpired(token) {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
const currentTime = Math.floor(Date.now() / 1000);
return payload.exp < currentTime;
} catch (error) {
return true; // Assume expired if can't parse
}
}
// Automatic token refresh wrapper
async function makeAuthenticatedRequest(url, options = {}) {
let token = getStoredToken();
// Check if token is expired
if (!token || isTokenExpired(token)) {
// Redirect to login or refresh token
window.location.href = '/login';
return;
}
const response = await fetch(url, {
...options,
headers: {
'Authorization': `Bearer ${token}`,
...options.headers
}
});
// Handle 401 (unauthorized) responses
if (response.status === 401) {
// Token might have expired, redirect to login
window.location.href = '/login';
return;
}
return response;
}
Logout Implementation
async function logout() {
const token = getStoredToken();
if (token) {
try {
// Call logout endpoint to invalidate token on server
await fetch('https://your-project.supabase.co/functions/v1/auth-logout', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
}
});
} catch (error) {
console.warn('Logout request failed:', error);
// Continue with local cleanup even if server request fails
}
}
// Clear local storage
localStorage.removeItem('nvlp_access_token');
localStorage.removeItem('nvlp_refresh_token');
// Redirect to login page
window.location.href = '/login';
}
🛡️ Security Best Practices
Token Security
- HTTPS Only: Always use HTTPS in production
- Secure Storage: Use appropriate secure storage for your platform
- Token Rotation: Implement token refresh mechanisms
- Logout Cleanup: Clear tokens on logout
Magic Link Security
- One-time Use: Magic links should only work once
- Time Limits: Links should expire (typically 1 hour)
- Secure Redirect: Validate redirect URLs to prevent phishing
- Rate Limiting: Limit magic link requests per email
✅ NVLP Built-in Security
NVLP implements these security measures automatically:
- Magic links expire after 1 hour
- Links are single-use only
- Rate limiting on magic link requests
- Secure JWT token generation
- Row-level security on all data
Environment Variables
🔐 Never Expose Secret Keys
Only use the anon key in client-side code. Never expose the service role key in client applications.
# Safe for client-side use
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
# NEVER use in client-side code
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... # Server-only!
🔧 Troubleshooting
Common Issues
Magic Link Not Received
- Check spam/junk folder
- Verify email address is correct
- Check email provider isn't blocking emails
- Verify Supabase email settings are configured
401 Unauthorized Errors
- Check if access token is included in request headers
- Verify token hasn't expired
- Ensure correct
Authorization: Bearer TOKEN
format - Check if user has permission to access the resource
CORS Issues
- Verify your domain is in Supabase allowed origins
- Check if you're using the correct Supabase URL
- Ensure HTTPS is used in production
Testing Authentication
Use our test script to verify your authentication setup:
# Download and run auth test script
curl -o test-auth.sh https://nvlp.app/scripts/test-auth.sh
chmod +x test-auth.sh
# Set your environment variables
export SUPABASE_URL="https://your-project.supabase.co"
export SUPABASE_ANON_KEY="your-anon-key"
export TEST_EMAIL="your-email@example.com"
# Run the test
./test-auth.sh
Debug Mode
Enable debug logging to troubleshoot authentication issues:
// Enable debug mode
const auth = new NVLPAuth(supabaseUrl, anonKey);
auth.debug = true; // This will log all requests and responses
// Or add manual logging
console.log('Sending magic link request...');
const result = await auth.sendMagicLink(email);
console.log('Magic link response:', result);
Getting Help
📞 Support Resources
- API Documentation: Complete API Reference
- GitHub Issues: Report bugs or ask questions
- Test Scripts: Use our testing scripts to validate your setup
- Postman Collection: Download collection for easy testing
📚 Next Steps
Now that you understand authentication, explore these resources: