Clerk Integration
Overview
Clerk handles all authentication for RestartiX Platform. We use Clerk for signup, login, MFA, password reset, social auth, and session management. Our internal users table stores app-specific data and links to Clerk via clerk_user_id.
Why Clerk?
- ✅ SOC 2 Type II compliant - Enterprise-grade security
- ✅ HIPAA BAA available - Enterprise plan includes BAA signing
- ✅ Pre-built UI components - Drop-in signup/login forms
- ✅ Session management - Short-lived tokens, automatic rotation
- ✅ Webhook-based sync - User changes propagate to our DB
- ✅ Social auth - Google, Apple, etc. (if needed)
Architecture
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Client App │ │ Go API (Core API) │ │ Clerk │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
│────── Login/Signup ───────────────────────────────────────→│
│ │ │
│←─── Session Token ─────────────────────────────────────────│
│ │ │
│ │ │
│── API Request ──────────────→│ │
│ Authorization: Bearer │ │
│ <clerk_session_token> │ │
│ │ │
│ │──── Verify Token ───────────→│
│ │ │
│ │←── Claims (clerk_user_id) ───│
│ │ │
│ │ 1. Lookup internal user │
│ │ 2. Load role, org, links │
│ │ 3. Set RLS session vars │
│ │ 4. Execute query │
│ │ │
│←────── Response ─────────────│ │
│ │ │User Synchronization (Webhooks)
Clerk sends webhooks to our API when user events occur. We process these to keep our users table in sync.
Webhook Events
| Event | Action | Notes |
|---|---|---|
user.created | Create user in users table | Use email as username |
user.updated | Update email/username | Email changes must propagate |
user.deleted | Set blocked = true | Never hard-delete (HIPAA requirement) |
Webhook Endpoint
POST /webhooks/clerkHeaders:
Svix-Id- Unique message ID (for idempotency)Svix-Timestamp- Webhook timestampSvix-Signature- HMAC signature (verified with CLERK_WEBHOOK_SECRET)
Authentication: Signature verification (not Bearer token)
Idempotency
Clerk may retry webhooks on network failures. We deduplicate using the Svix-Id header:
- Extract
Svix-Idfrom headers - Check Redis:
SETNX clerk_webhook:<svix_id> 1 EX 259200(72h TTL) - If key already exists → skip processing, return 204
- If key didn't exist → process webhook, return 204
- On error → delete Redis key so retry can be processed
Why 72 hours? Clerk stops retrying after 3 days. TTL matches their retry window.
Implementation
// internal/domain/user/handler.go
func (h *Handler) ClerkWebhook(w http.ResponseWriter, r *http.Request) {
// 1. Verify webhook signature (Clerk signs with CLERK_WEBHOOK_SECRET)
payload, err := clerk.VerifyWebhook(r, h.webhookSecret)
if err != nil {
httputil.Unauthorized(w, "invalid webhook signature")
return
}
// 2. Idempotency check
eventID := r.Header.Get("Svix-Id")
if eventID != "" {
idempotencyKey := "clerk_webhook:" + eventID
wasSet, err := h.redis.SetNX(r.Context(), idempotencyKey, "1", 72*time.Hour).Result()
if err != nil {
slog.Warn("redis idempotency check failed, processing anyway", "error", err)
// Fail open — process rather than risk missing event
} else if !wasSet {
// Already processed
slog.Info("duplicate webhook skipped", "event_id", eventID, "type", payload.Type)
httputil.NoContent(w)
return
}
}
// 3. Process event
switch payload.Type {
case "user.created":
var data clerk.UserCreatedEvent
json.Unmarshal(payload.Data, &data)
err = h.service.CreateFromClerk(r.Context(), &CreateFromClerkRequest{
ClerkUserID: data.ID,
Email: data.EmailAddresses[0].EmailAddress,
Username: data.EmailAddresses[0].EmailAddress,
})
case "user.updated":
var data clerk.UserUpdatedEvent
json.Unmarshal(payload.Data, &data)
err = h.service.UpdateFromClerk(r.Context(), data.ID, data.EmailAddresses[0].EmailAddress)
case "user.deleted":
var data clerk.UserDeletedEvent
json.Unmarshal(payload.Data, &data)
err = h.service.BlockUser(r.Context(), data.ID)
}
// 4. Error handling
if err != nil {
slog.Error("webhook processing failed", "type", payload.Type, "error", err)
// Delete idempotency key so Clerk's retry will be processed
if eventID != "" {
h.redis.Del(r.Context(), "clerk_webhook:"+eventID)
}
httputil.InternalError(w)
return
}
httputil.NoContent(w)
}Clerk Configuration
Environment Variables
# Core API (.env)
CLERK_SECRET_KEY=sk_live_... # For token verification
CLERK_WEBHOOK_SECRET=whsec_... # For webhook signature verificationClerk Dashboard Setup
Create Application - Sign up at https://clerk.com, create app
Get Secret Key - Dashboard → API Keys → Secret Key
Configure Webhook:
- Dashboard → Webhooks → Add Endpoint
- URL:
https://api.restartix.com/webhooks/clerk - Events:
user.created,user.updated,user.deleted - Copy webhook secret to
CLERK_WEBHOOK_SECRET
Frontend Setup:
- Dashboard → API Keys → Publishable Key
- Add to frontend env:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_...
Session Token Lifetime
Clerk sessions are short-lived (default: 1 week) and automatically rotated. The frontend SDK handles token refresh transparently.
Token verification: Every API request verifies the token with Clerk via their SDK (cached for performance).
HIPAA Compliance
BAA Signing
Clerk provides a Business Associate Agreement (BAA) on the Enterprise plan. This is required for HIPAA compliance.
Action items:
- [ ] Upgrade to Clerk Enterprise plan
- [ ] Sign BAA via Clerk dashboard
- [ ] Store signed BAA with compliance documentation
PHI in Clerk
Clerk stores:
- Email address
- Username (we use email)
- Authentication metadata (IP, device, login history)
Not stored in Clerk:
- Patient names, phone numbers, addresses
- Medical data
- Custom fields
All PHI lives in our database, not in Clerk.
Security Considerations
Token Verification
Every request to authenticated endpoints:
- Extracts
Authorization: Bearer <token>header - Verifies token with Clerk SDK
- Loads internal user from DB (single query)
- Blocks if
users.blocked = true
Webhook Signature Verification
Webhooks are verified using HMAC signatures. Clerk signs every webhook with CLERK_WEBHOOK_SECRET. We verify before processing.
Security: Without signature verification, attackers could create fake webhooks to manipulate user data.
Failed Verifications
| Failure | Response | Reason |
|---|---|---|
| Missing token | 401 Unauthorized | No auth header |
| Invalid token | 401 Unauthorized | Expired or tampered |
| User blocked | 403 Forbidden | Account disabled |
| User not found | 401 Unauthorized | Not synced from Clerk |
| Invalid webhook signature | 401 Unauthorized | Forged webhook |
Troubleshooting
User exists in Clerk but not in our DB
Cause: Webhook failed or wasn't received.
Fix:
- Check webhook delivery in Clerk dashboard (Webhooks → Logs)
- If failed, click "Retry"
- If missing, manually create user:sql
INSERT INTO users (clerk_user_id, email, username, role) VALUES ('user_2abc...', '[email protected]', '[email protected]', 'patient');
User deleted in Clerk but still active in our DB
Cause: user.deleted webhook failed.
Fix:
- Check webhook logs
- Manually block user:sql
UPDATE users SET blocked = true WHERE clerk_user_id = 'user_2abc...';
Duplicate webhook processing
Cause: Redis unavailable or Svix-Id header missing.
Impact: Low - operations are idempotent. CreateFromClerk uses INSERT ... ON CONFLICT DO NOTHING.
Fix: Ensure Redis is accessible. Check logs for redis idempotency check failed warnings.
Testing
Test Webhooks in Development
Clerk provides a webhook testing tool:
- Dashboard → Webhooks → Your Endpoint → Test
- Select event type (
user.created, etc.) - Click "Send Test"
The webhook will fire to your dev server (use ngrok if testing locally).
Manual User Creation (Testing Only)
For integration tests, bypass Clerk and create users directly:
user, err := userRepo.Create(ctx, &CreateUserRequest{
Email: "[email protected]",
Username: "[email protected]",
Role: "patient",
})Never do this in production. All production users must come from Clerk (HIPAA audit trail).
Migration from Legacy System
If migrating from an existing auth system (e.g., Strapi):
- Export users from legacy system (email, password hash)
- Import to Clerk via Clerk API or bulk import
- Sync clerk_user_id back to our DB
- Test login with migrated users
See ../../integrations/migration.md for full migration procedure.
Related Documentation
- API Endpoints -
/v1/me,/v1/me/switch-organization,/webhooks/clerk - Session Management - RLS setup, middleware stack
- Organizations - Organization context, multi-tenancy