Clerk Integration
The Auth service receives webhook events from Clerk, syncs user data to PostgreSQL, and provides JWT validation middleware used by the gateway.
Webhook Flow
Clerk Dashboard
│
▼ (webhook POST)
Gateway (:8080)
└── /api/v1/auth/webhook (public, no JWT)
│
▼
Auth Service (:8200)
├── Verify Clerk signature
├── Parse event payload
├── Upsert user in gl_users
└── Publish to gl:events:users Redis Stream
Webhook Events
| Event | Action |
|---|---|
user.created | Insert new user into gl_users, publish user.created event |
user.updated | Update user record (email, name), publish user.updated event |
session.created | Update last_seen_at on user device record |
Webhook Handler
// services/auth/internal/clerk/webhook.go
func (h *WebhookHandler) HandleUserCreated(w http.ResponseWriter, r *http.Request) {
var event UserCreatedEvent
if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
render.Status(r, 400)
render.JSON(w, r, ErrBadRequest)
return
}
// Validate Clerk webhook signature
if !clerk.VerifySignature(r, h.cfg.ClerkWebhookSecret) {
render.Status(r, 401)
render.JSON(w, r, ErrUnauthorized)
return
}
// Persist to gl_users
user := &domain.User{
ClerkID: event.Data.ID,
Email: event.Data.EmailAddress[0].EmailAddress,
CreatedAt: time.Unix(event.Data.CreatedAt/1000, 0),
}
if err := h.userRepo.Upsert(r.Context(), user); err != nil {
render.Status(r, 500)
render.JSON(w, r, ErrInternal)
return
}
// Publish to Redis Stream for downstream consumers
h.events.Publish(r.Context(), "gl:events:users", event)
render.Status(r, 200)
render.JSON(w, r, map[string]string{"status": "ok"})
}
Signature Verification
Every incoming webhook is verified against Clerk's signing secret (GOSPELIB_AUTH_CLERK_WEBHOOK_SECRET). The signature is checked before any event processing occurs.
JWT Validation Flow
Client Request
│
├── Authorization: Bearer <JWT>
│
▼
Gateway Middleware
├── 1. Extract token from Authorization header
├── 2. Fetch Clerk JWKS (cached, refreshed periodically)
├── 3. Verify RS256 signature
├── 4. Check token expiry (exp claim)
├── 5. Validate issuer (iss claim matches Clerk instance)
├── 6. Extract user claims (sub, plan)
└── 7. Inject X-User-Id and X-User-Plan headers
│
▼
Downstream Service
└── Reads X-User-Id, X-User-Plan from headers (trusts gateway)
Token Claims
| Claim | Header | Description |
|---|---|---|
sub | X-User-Id | Clerk user ID (e.g., user_abc123) |
custom plan | X-User-Plan | Subscription plan (e.g., free, scholar, academic) |
JWKS Caching
The JWKS (JSON Web Key Set) from Clerk is cached in memory with periodic refresh. This avoids a network call on every request while ensuring key rotation is handled.
Idempotency
Webhook events from Clerk may be delivered more than once. The auth service handles this via upsert semantics:
Upsert(user)— Inserts ifclerk_iddoesn't exist, updates if it does- The user's
updated_attimestamp is always refreshed on upsert - Redis Stream consumers downstream handle duplicate events idempotently
User Endpoints
GET /users/{id}
Returns structured user profile data for internal service-to-service calls. Not exposed directly to external clients — accessed via the gateway.
GET /users/{id}/entitlements
Joins user record with their billing plan to return a list of entitled features. Used by the gateway's entitlement middleware to populate the Redis cache.
Related Pages
- Auth Service Overview — service setup and environment variables
- Architecture > Security > Auth Flow — end-to-end flow
- Architecture > Security > Entitlements — plan-based feature access