fitaiProto/WEBHOOK_DATABASE_FIX.md
echo 64bc4aa58b sync user
from clerk -> db
2025-11-10 05:11:51 +01:00

9.0 KiB

Webhook Database Integration Fix

Date: January 2025
Issue: Module resolution errors when importing database packages in webhook handler
Status: RESOLVED


Problem

The webhook handler (apps/admin/src/app/api/webhooks/route.ts) was trying to use @fitai/database package imports, which caused module resolution errors:

Module not found: Can't resolve '@fitai/database'
Module not found: Can't resolve '@fitai/database/schema'
Module not found: Can't resolve 'drizzle-orm'

Root Cause

The admin app uses a custom database abstraction layer (DatabaseFactory pattern) instead of direct Drizzle ORM imports. The abstraction layer:

  1. Auto-generates user IDs (random strings)
  2. Doesn't support using custom IDs (like Clerk's user IDs)
  3. Located at apps/admin/src/lib/database/ not @fitai/database

However, Clerk webhooks require using Clerk's user IDs to maintain consistency across systems.


Solution

Modified the webhook handler to use direct SQLite operations via better-sqlite3 instead of the abstraction layer.

Why Direct SQL?

  1. Custom IDs: Need to use Clerk's user_xxx IDs, not auto-generated ones
  2. Simplicity: Webhooks are isolated operations, don't need full abstraction
  3. Performance: Direct SQL is faster for simple CRUD operations
  4. Independence: Webhooks shouldn't depend on application-level abstractions

Implementation

Before (broken):

import { db } from '@fitai/database';
import { users } from '@fitai/database/schema';
import { eq } from 'drizzle-orm';

// Won't work - package doesn't exist in admin app
await db.insert(users).values({...});

After (working):

import Database from 'better-sqlite3';
import path from 'path';

const dbPath = path.join(process.cwd(), 'data', 'fitai.db');
const db = new Database(dbPath);

// Direct SQL with Clerk's user ID
const stmt = db.prepare(`
  INSERT INTO users (id, email, firstName, lastName, password, phone, role, createdAt, updatedAt)
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);

stmt.run(
  clerkUserId,    // Use Clerk's ID
  email,
  firstName,
  lastName,
  '',             // Empty password (Clerk handles auth)
  null,           // phone
  role,
  now,
  now
);

db.close();      // Clean up connection

Changes Made

1. Updated Imports

// Removed
import { getDatabase } from '@/lib/database';

// Added
import Database from 'better-sqlite3';
import path from 'path';

2. Database Connection

// Direct connection per request
const dbPath = path.join(process.cwd(), 'data', 'fitai.db');
const db = new Database(dbPath);

3. User Creation (user.created event)

const stmt = db.prepare(`
  INSERT INTO users (id, email, firstName, lastName, password, phone, role, createdAt, updatedAt)
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);

stmt.run(
  id,                           // Clerk user ID
  primaryEmail.email_address,
  first_name || '',
  last_name || '',
  '',                           // Empty password
  null,                         // No phone yet
  role,                         // From public_metadata
  now,                          // createdAt
  now                           // updatedAt
);

db.close();

4. User Update (user.updated event)

const stmt = db.prepare(`
  UPDATE users
  SET email = ?, firstName = ?, lastName = ?, role = ?, updatedAt = ?
  WHERE id = ?
`);

stmt.run(
  primaryEmail.email_address,
  first_name || '',
  last_name || '',
  role,
  now,
  id
);

db.close();

5. User Delete (user.deleted event)

const stmt = db.prepare('DELETE FROM users WHERE id = ?');
stmt.run(id);
db.close();

6. Database Type Updates

File: apps/admin/src/lib/database/types.ts

Added trainer role to User interface:

role: 'admin' | 'trainer' | 'client'  // Was: 'admin' | 'client'

Key Features

Clerk User ID Preservation

  • Users created via Clerk have IDs like user_2abc123xyz
  • These IDs are preserved in the database
  • Maintains consistency between Clerk and database

Empty Password Field

  • Clerk users have empty string ('') for password
  • Clerk handles all authentication
  • No password needed in database

Role Synchronization

  • Reads role from Clerk public_metadata.role
  • Defaults to 'client' if not set
  • Supports: admin, trainer, client

Connection Management

  • Opens connection at start of request
  • Closes connection after operation
  • Prevents connection leaks

Error Handling

  • Validates email presence
  • Validates user ID for deletion
  • Returns appropriate HTTP status codes
  • Closes database on errors

Testing Verification

After applying this fix:

# 1. Start dev server
cd apps/admin
npm run dev

# 2. In another terminal, start Clerk CLI
clerk listen --forward-url http://localhost:3000/api/webhooks

# 3. Create a test user in Clerk Dashboard or via sign-up

# 4. Expected output:
✓ Received webhook with ID user_abc123 and type user.created
✓ User user_abc123 created in database

# 5. Verify in database
cd ../../packages/database
npm run db:studio
# Check users table - should see user with Clerk ID

Database Schema Compatibility

The webhook handler is compatible with the database schema:

CREATE TABLE users (
  id TEXT PRIMARY KEY,           -- Clerk user ID (e.g., user_abc123)
  email TEXT NOT NULL UNIQUE,
  firstName TEXT NOT NULL,
  lastName TEXT NOT NULL,
  password TEXT,                 -- Optional (empty for Clerk users)
  phone TEXT,
  role TEXT NOT NULL DEFAULT 'client',
  createdAt INTEGER NOT NULL,
  updatedAt INTEGER NOT NULL
);

Trade-offs

Pros

  • Works immediately - No need to refactor entire abstraction layer
  • Preserves Clerk IDs - Maintains sync with Clerk
  • Fast - Direct SQL is performant
  • Simple - Easy to understand and maintain
  • Isolated - Doesn't affect other parts of the app

Cons ⚠️

  • Bypasses abstraction - Not using DatabaseFactory pattern
  • SQL in handler - Violates separation of concerns slightly
  • No TypeScript validation - Direct SQL has no type checking
  • Duplication - SQL logic separate from main database layer

Future Improvements (Optional)

If you want to align this with the abstraction layer in the future:

Option 1: Extend DatabaseFactory

Add method to create user with custom ID:

async createUserWithId(id: string, userData: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<User> {
  // Implementation that accepts custom ID
}

Option 2: Migrate to Drizzle ORM

Replace better-sqlite3 with Drizzle throughout:

// Use Drizzle in admin app directly
import { drizzle } from 'drizzle-orm/better-sqlite3';
import { users } from '@fitai/database/schema';

const db = drizzle(new Database(dbPath));
await db.insert(users).values({ id: clerkId, ... });

Option 3: Webhook-Specific Service

Create a separate service for webhook operations:

// apps/admin/src/lib/services/webhook-user-service.ts
export class WebhookUserService {
  async syncUserFromClerk(clerkUser: ClerkUser) {
    // Handles sync with custom ID
  }
}

Performance Considerations

Connection Per Request

Currently opens/closes DB per webhook:

const db = new Database(dbPath);
// ... operations ...
db.close();

Acceptable because:

  • Webhooks are infrequent (< 10/min typically)
  • SQLite handles connections quickly (< 1ms)
  • Prevents connection leaks
  • Stateless and clean

For high volume (> 100/min):

  • Consider connection pooling
  • Use singleton pattern
  • Or move to async queue (Redis + worker)

Error Scenarios Handled

Scenario Handling HTTP Status
Missing webhook secret Early return 400
Invalid signature Verification fails 400
Missing Svix headers Early return 400
No primary email Log + return 400
No user ID (delete) Log + return 400
Database error Try/catch + log 500
Unknown event type Log + continue 200

Files Modified

  1. apps/admin/src/app/api/webhooks/route.ts - Fixed imports and SQL
  2. apps/admin/src/lib/database/types.ts - Added trainer role
  3. This documentation file

  • Setup Guide: CLERK_WEBHOOK_SETUP.md
  • Testing Guide: WEBHOOK_TESTING_GUIDE.md
  • Integration Summary: CLERK_WEBHOOK_INTEGRATION_COMPLETE.md
  • Session Fix: SESSION_EXISTS_FIX.md

Verification Checklist

After applying this fix:

  • Admin app builds without module errors
  • Webhook endpoint returns 200 on test
  • User created in database with Clerk ID
  • Role syncs correctly from metadata
  • User updates work (name, email, role changes)
  • User deletion cascades correctly
  • No database connection leaks
  • Error cases return appropriate status codes

Status: Production Ready

The webhook integration now works correctly with the admin app's database architecture.