User Sync Utility
The User Sync Utility provides a consolidated, reusable function for fetching user data from Clerk and automatically syncing it with your Supabase database. It eliminates repetitive code patterns and handles edge cases like users that don’t exist in the database yet.
Quick Start
Section titled “Quick Start”---import { fetchUserWithRole } from '#utils/user-sync'
const { userId } = Astro.locals
if (userId) { const { user, userRole, error, roleError } = await fetchUserWithRole(userId, Astro)
if (error) { // Handle critical error (Clerk fetch failed) }
if (roleError) { // Handle warning (role fetch failed, but user data is available) }
// Use user and userRole data}---Why Use This Utility?
Section titled “Why Use This Utility?”Before: Manual Fetching
Section titled “Before: Manual Fetching”---import { clerkClient } from '@clerk/astro/server'import { getSupabaseServiceRole } from '#libs/supabase-native'
const { userId } = Astro.locals
// Fetch from Clerklet user = nulltry { const client = clerkClient(Astro) user = await client.users.getUser(userId)} catch (err) { console.error('Failed:', err)}
// Fetch from Supabaselet role = nullif (user) { const supabase = getSupabaseServiceRole() const { data, error } = await supabase .from('users') .select('role') .eq('clerk_id', userId) .single()
// Handle PGRST116 error (user not found) if (error && error.code === 'PGRST116') { // Create user manually... (30+ more lines) } else { role = data?.role }}---After: Using Utility
Section titled “After: Using Utility”---import { fetchUserWithRole } from '#utils/user-sync'
const { userId } = Astro.localsconst { user, userRole, error, roleError } = await fetchUserWithRole(userId, Astro)---Benefits:
- ✅ 80% less code - One line instead of 40+
- ✅ Automatic user creation - Handles PGRST116 errors automatically
- ✅ Consistent error handling - Structured error fields
- ✅ Race condition safety - Uses upsert to prevent duplicates
Function Signature
Section titled “Function Signature”async function fetchUserWithRole( userId: string, astroContext: AstroGlobal): Promise<UserWithRoleResult>
interface UserWithRoleResult { user: ClerkUser | null userRole: UserRole | null error: string | null roleError: string | null}Parameters
Section titled “Parameters”| Parameter | Type | Description |
|---|---|---|
userId |
string |
Clerk user ID from Astro.locals.userId |
astroContext |
AstroGlobal |
The Astro global context object |
Return Value
Section titled “Return Value”The function returns an object with four fields:
| Field | Type | Description |
|---|---|---|
user |
ClerkUser | null |
Clerk user object, or null if fetch failed |
userRole |
UserRole | null |
User’s role from Supabase, or null if not found |
error |
string | null |
Critical error from Clerk fetch (user data unavailable) |
roleError |
string | null |
Warning from role fetch (user data available, but role missing) |
Usage Examples
Section titled “Usage Examples”Basic Component Usage
Section titled “Basic Component Usage”---import { fetchUserWithRole } from '#utils/user-sync'import type { User as ClerkUser } from '@clerk/backend'import type { UserRole } from '#utils/role-types'
const { userId } = Astro.locals
let user: ClerkUser | null = nulllet userRole: UserRole | null = nulllet error: string | null = nulllet roleError: string | null = null
if (userId) { const result = await fetchUserWithRole(userId, Astro) user = result.user userRole = result.userRole error = result.error roleError = result.roleError}---
<div class="user-profile"> {error ? ( <div class="error-state"> <p>Unable to load user profile</p> <p class="error-details">{error}</p> </div> ) : user ? ( <div class="profile-content"> <img src={user.imageUrl} alt={user.fullName} /> <h2>{user.fullName}</h2> <p>Email: {user.emailAddresses[0]?.emailAddress}</p>
{roleError ? ( <p class="warning">Role: Unavailable ({roleError})</p> ) : ( <p>Role: <span class="role-badge">{userRole || 'member'}</span></p> )} </div> ) : ( <p>Please sign in to view your profile</p> )}</div>---import { fetchUserWithRole } from '#utils/user-sync'
const { userId } = Astro.locals
// Concise destructuringconst { user, userRole, error, roleError } = userId ? await fetchUserWithRole(userId, Astro) : { user: null, userRole: null, error: null, roleError: null }---
{error && <div class="error">{error}</div>}{!error && user && ( <div> <h2>{user.fullName}</h2> <p>Role: {roleError ? 'N/A' : userRole}</p> </div>)}API Endpoint Usage
Section titled “API Endpoint Usage”import type { APIRoute } from 'astro'import { fetchUserWithRole } from '#utils/user-sync'
export const GET: APIRoute = async ({ locals, ...astroContext }) => { // Check authentication if (!locals.userId) { return new Response( JSON.stringify({ error: 'Unauthorized' }), { status: 401, headers: { 'Content-Type': 'application/json' } } ) }
// Fetch user with role const result = await fetchUserWithRole(locals.userId, astroContext as any)
// Handle critical errors if (result.error) { return new Response( JSON.stringify({ error: result.error }), { status: 500, headers: { 'Content-Type': 'application/json' } } ) }
// Return successful response (with role warning if applicable) return new Response( JSON.stringify({ user: { id: result.user?.id, email: result.user?.emailAddresses[0]?.emailAddress, fullName: result.user?.fullName, imageUrl: result.user?.imageUrl, }, role: result.userRole || 'member', roleWarning: result.roleError || null, }), { status: 200, headers: { 'Content-Type': 'application/json' } } )}Error Handling
Section titled “Error Handling”The utility uses a non-throwing error design, returning errors as part of the result object rather than throwing exceptions. This allows you to handle errors gracefully in your UI.
Error Types
Section titled “Error Types”When it occurs:
- Clerk API fails
- Network connectivity issues
- Invalid or expired user ID
- Clerk service unavailable
What it means:
- User data is completely unavailable
- Cannot proceed with displaying user information
How to handle:
const { error } = await fetchUserWithRole(userId, Astro)
if (error) { // Display error page or redirect return <ErrorPage message="Unable to load user profile" />}When it occurs:
- Supabase not configured
- Database connection failed
- User record creation failed
- Role query failed
What it means:
- User data IS available from Clerk
- Role information is missing or unavailable
- Page can still render with limited data
How to handle:
const { user, userRole, roleError } = await fetchUserWithRole(userId, Astro)
if (roleError) { // Log warning, but continue rendering console.warn('Role fetch failed:', roleError)}
// Display with fallback roleconst displayRole = userRole || 'member'Error Handling Best Practices
Section titled “Error Handling Best Practices”const { user, userRole, error, roleError } = await fetchUserWithRole(userId, Astro)
// 1. Check critical errors firstif (error) { // Cannot proceed - show error state return <ErrorState />}
// 2. Handle role warnings gracefullyif (roleError) { // Log for monitoring console.warn('Role unavailable for user:', userId, roleError) // But continue rendering with default role}
// 3. Use the available dataconst role = userRole || 'member' // Fallback to defaultAutomatic User Creation
Section titled “Automatic User Creation”The utility automatically creates user records in Supabase when they don’t exist yet. This happens when:
- User successfully authenticates with Clerk
- User accesses a page using this utility
- User doesn’t exist in Supabase (PGRST116 error returned)
Default User Data
Section titled “Default User Data”When auto-creating users, the following data is stored:
{ clerk_id: userId, // Primary key email: primaryEmail.emailAddress, // Primary email from Clerk username: user.username, // Clerk username (nullable) full_name: user.fullName, // Full name from Clerk avatar_url: user.imageUrl, // Profile image URL role: 'member', // Default role}Changing Default Role
Section titled “Changing Default Role”To change the default role for new users, modify the utility:
const newUser = { // ... role: 'guest' as UserRole, // Change from 'member' to your preferred default}Integration with Components
Section titled “Integration with Components”Example: UserInfo Component
Section titled “Example: UserInfo Component”The UserInfo.astro component demonstrates real-world usage:
---import { fetchUserWithRole } from '#utils/user-sync'import RoleBadge from '#components/react/RoleBadge.tsx'
const { userId } = Astro.locals
let user = nulllet userRole = nulllet error = nulllet roleError = null
if (userId) { const result = await fetchUserWithRole(userId, Astro) user = result.user userRole = result.userRole error = result.error roleError = result.roleError}---
<div class="user-info"> {error ? ( <div class="error">{error}</div> ) : user ? ( <div> <img src={user.imageUrl} alt={user.fullName} /> <h2>{user.fullName}</h2>
{roleError ? ( <span class="role-error">{roleError}</span> ) : userRole ? ( <RoleBadge role={userRole} client:only="react" /> ) : ( <span class="default-badge">Member</span> )} </div> ) : null}</div>What Happens Behind the Scenes
Section titled “What Happens Behind the Scenes”sequenceDiagram participant Component participant Utility participant Clerk participant Supabase
Component->>Utility: fetchUserWithRole(userId, Astro)
Utility->>Clerk: users.getUser(userId) alt Clerk Success Clerk-->>Utility: User data Utility->>Supabase: SELECT role WHERE clerk_id
alt User Exists Supabase-->>Utility: Role data Utility-->>Component: { user, role, no errors } else User Not Found (PGRST116) Supabase-->>Utility: PGRST116 error Utility->>Supabase: UPSERT user with default role Supabase-->>Utility: New user with role Utility-->>Component: { user, role, no errors } else Database Error Supabase-->>Utility: Error Utility-->>Component: { user, null role, roleError } end else Clerk Failure Clerk-->>Utility: Error Utility-->>Component: { null, null, error } endComparison with Other Approaches
Section titled “Comparison with Other Approaches”vs. Manual Fetching
Section titled “vs. Manual Fetching”| Feature | Manual Fetching | User Sync Utility |
|---|---|---|
| Lines of Code | 40-60 lines | 1 line |
| Error Handling | Custom per component | Consistent, built-in |
| Auto User Creation | Must implement manually | Automatic |
| Race Condition Safety | Must handle manually | Built-in upsert |
| Maintenance | Update all components | Update once in utility |
vs. API Endpoint
Section titled “vs. API Endpoint”Using /api/user/sync:
Pros:
- Explicit control over sync timing
- Can be called from client-side
Cons:
- Extra HTTP request (slower)
- Requires separate endpoint maintenance
- More complex error handling
When to use each:
- Use
fetchUserWithRolefor server-rendered pages (most cases) - Use
/api/user/syncfor client-side data fetching or manual sync triggers
vs. Webhooks Only
Section titled “vs. Webhooks Only”Relying solely on Clerk webhooks:
Pros:
- Proactive user creation
- Real-time updates
Cons:
- Requires webhook configuration
- Potential delays or failures
- No fallback if webhook misses
Best approach: Use both
- Webhooks for proactive sync
fetchUserWithRoleas safety net/fallback
Troubleshooting
Section titled “Troubleshooting”“Failed to load user information”
Section titled ““Failed to load user information””Cause: Clerk API call failed
Solutions:
- Verify
CLERK_SECRET_KEYin.env - Check Clerk service status
- Verify
userIdis valid - Check network connectivity
“Database not configured”
Section titled ““Database not configured””Cause: Supabase environment variables missing
Solutions:
- Run
npm run db:wizardto configure - Manually add to
.env:SUPABASE_URL=https://your-project.supabase.coSUPABASE_SERVICE_ROLE_KEY=your-service-role-key
“Role information unavailable”
Section titled ““Role information unavailable””Cause: Supabase query failed or user creation failed
Solutions:
- Check Supabase credentials
- Verify
userstable exists - Check service role permissions
- Review Supabase logs for specific error
User Created with Wrong Role
Section titled “User Created with Wrong Role”Cause: Default role is 'member'
Solution: Update role in Supabase:
UPDATE usersSET role = 'admin'WHERE clerk_id = 'user_...';Or create an admin API to update roles programmatically.
Related Resources
Section titled “Related Resources”- Clerk-Supabase Integration Setup
- Configurable Roles System
- Database Switching Guide
- Environment Configuration
Next Steps
Section titled “Next Steps”- Review the implementation: Check
src/utils/user-sync.ts - See it in action: Look at
src/components/astro/UserInfo.astro - Extend it: Add caching, custom role mapping, or batch fetching
- Contribute: Submit improvements via pull request
Have questions? Open an issue on GitHub or check the authentication guide.