Skip to main content

Overview

The backend uses Clerk for authentication, providing secure user management with JWT tokens and webhooks.

Clerk Setup

1

Create Clerk Account

Sign up at clerk.com and create a new application
2

Get API Keys

From the Clerk dashboard, copy:
  • Publishable Key (starts with pk_test_ or pk_live_)
  • Secret Key (starts with sk_test_ or sk_live_)
3

Configure Environment

Add keys to your .env file:
CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
CLERK_WEBHOOK_SECRET=whsec_...
4

Set Up Webhooks

Configure webhook endpoint at /api/webhooks/clerk to receive user events

Clerk Middleware

The Clerk middleware is applied globally to all routes.
src/server.js:94
app.use(clerkMiddleware());
This makes authentication information available via req.auth() on all requests.
The clerkMiddleware() does not block unauthenticated requests. Use protectRoute middleware to require authentication.

Protected Routes

The protectRoute middleware ensures users are authenticated and loads their data from the database.

Implementation

src/middleware/auth.middleware.js:5-37
export const protectRoute = [
    requireAuth(),
    async (req, res, next) => {
        try {
            const auth = req.auth();
            const clerkId = auth.userId;

            if (!clerkId) {
                return res.status(401).json({ 
                    message: "Unauthorized - Invalid token" 
                });
            }

            const user = await User.findOne({ clerkId });
            
            if (!user) {
                return res.status(404).json({ 
                    message: "User not found" 
                });
            }

            req.user = user;
            req.clerkAuth = auth;
            
            next();
        } catch (error) {
            console.error("Error in protectRoute middleware:", error);
            return res.status(500).json({ 
                message: "Internal server error" 
            });
        }
    }
];

How It Works

1

Require Authentication

requireAuth() from @clerk/express validates the JWT token
2

Extract User ID

Gets clerkId from the authenticated session
3

Load User Data

Queries MongoDB for the user record
4

Attach to Request

Adds req.user (database user) and req.clerkAuth (Clerk auth data) for use in route handlers

Usage Example

import { protectRoute } from '../middleware/auth.middleware.js';

router.get('/profile', protectRoute, async (req, res) => {
    // req.user contains the MongoDB user document
    // req.clerkAuth contains Clerk authentication data
    res.json({ user: req.user });
});

Admin Authorization

The adminOnly middleware restricts routes to admin users.

Implementation

src/middleware/auth.middleware.js:39-92
export const adminOnly = async (req, res, next) => {
    try {
        if (!req.user || !req.clerkAuth) {
            console.error("adminOnly: protectRoute no se ejecutó primero");
            return res.status(401).json({ 
                message: "Unauthorized - authentication required" 
            });
        }

        const userRole = req.clerkAuth.sessionClaims?.role;
        const userEmail = req.user.email;

        if (ENV.NODE_ENV === 'development') {
            console.log(`Admin check:`, {
                email: userEmail,
                role: userRole || 'sin role',
                clerkId: req.user.clerkId
            });
        }

        const isAdmin = userRole === 'admin';
        
        const isAdminByEmail = ENV.NODE_ENV === 'development' &&
                                ENV.ADMIN_EMAIL && 
                                ENV.ADMIN_EMAIL.split(',')
                                    .map(e => e.trim())
                                    .includes(userEmail);

        if (!isAdmin && !isAdminByEmail) {
            if (ENV.NODE_ENV === 'development') {
                console.log(`Acceso denegado:`, {
                    email: userEmail,
                    role: userRole || 'sin rol'
                });
            }        
            return res.status(403).json({ 
                message: "Forbidden - admin access only",
                details: ENV.NODE_ENV === 'development' 
                    ? `Rol actual: ${userRole || 'ninguno'}` 
                    : undefined
            });
        }

        if (ENV.NODE_ENV === 'development') {
            console.log(`Admin autorizado: ${userEmail} (${isAdmin ? 'por rol' : 'por email'})`);
        }
        next();
    } catch (error) {
        console.error("Error in adminOnly middleware:", error);
        return res.status(500).json({ 
            message: "Internal server error" 
        });
    }
};

Admin Authorization Methods

The user’s Clerk session must have role: 'admin' in session claims.Set this in Clerk dashboard under User Metadata or via API.
In development, you can specify admin emails in .env:
ADMIN_EMAIL=admin@example.com,admin2@example.com
This allows testing without configuring Clerk roles.

Usage Example

import { protectRoute, adminOnly } from '../middleware/auth.middleware.js';

// Admin-only route
router.post('/products', protectRoute, adminOnly, async (req, res) => {
    // Only admins can create products
    const product = await Product.create(req.body);
    res.json(product);
});
Always use protectRoute before adminOnly. The admin middleware depends on req.user and req.clerkAuth being set.

Role-Based Authorization

The requireRole middleware allows custom role checking.

Implementation

src/middleware/auth.middleware.js:94-119
export const requireRole = (allowedRoles) => {
    return async (req, res, next) => {
        try {
            if (!req.clerkAuth) {
                return res.status(401).json({ 
                    message: "Unauthorized - authentication required" 
                });
            }

            const userRole = req.clerkAuth.sessionClaims?.role;

            if (!allowedRoles.includes(userRole)) {
                return res.status(403).json({ 
                    message: "Forbidden - insufficient permissions"
                });
            }

            next();
        } catch (error) {
            console.error("Error in requireRole middleware:", error);
            return res.status(500).json({ 
                message: "Internal server error" 
            });
        }
    };
};

Usage Example

import { protectRoute, requireRole } from '../middleware/auth.middleware.js';

// Allow both admin and moderator
router.put('/products/:id', 
    protectRoute, 
    requireRole(['admin', 'moderator']), 
    async (req, res) => {
        // Update product
    }
);

Webhook Handling

Clerk webhooks sync user data between Clerk and MongoDB.

Webhook Endpoint

src/server.js:75-90
app.post("/api/webhooks/clerk", async (req, res) => {
  const event = req.body;

  console.log("Webhook received:", event.type);

  try {
    await inngest.send({
      name: `clerk.${event.type}`,
      data: event.data,
    });
    res.status(200).json({ received: true });
  } catch (error) {
    console.error("Error sending event to Inngest:", error);
    res.status(500).json({ error: "Inngest error" });
  }
});

Webhook Events

The webhook forwards events to Inngest for processing:
// Inngest function creates user in MongoDB
// See src/config/inngest.js:10-35

const syncUser = inngest.createFunction(
    {id: "sync-user"},
    {event:"clerk.user.created"},
    async ({event}) => {
        await connectDB();
        const {id, email_addresses, first_name, last_name, image_url} = event.data;
        const newUser = {
            clerkId: id,
            email: email_addresses[0]?.email_address,
            name: `${first_name || ""} ${last_name || ""}` || "Usuario",
            imageUrl: image_url,
            address: [],
            wishlist: [],
        };
        await User.create(newUser);
        
        sendWelcomeEmail({
            userName: `${first_name || ""} ${last_name || ""}`.trim() || "Usuario",
            userEmail: email_addresses[0]?.email_address,
        });
    }
);

Configure Clerk Webhook

1

Go to Clerk Dashboard

Navigate to Webhooks section
2

Add Endpoint

Add endpoint URL: https://your-domain.com/api/webhooks/clerkFor development, use ngrok or Clerk’s webhook forwarding
3

Select Events

Subscribe to:
  • user.created
  • user.deleted
4

Copy Signing Secret

Save the webhook signing secret to .env:
CLERK_WEBHOOK_SECRET=whsec_...
The webhook endpoint does not verify signatures in this implementation. For production, add signature verification using @clerk/clerk-sdk-node.

Authentication Flow

User Roles

The system supports the following roles:
RoleDescriptionAccess
adminAdministratorFull access to all endpoints
moderatorModeratorCustom permissions via requireRole
userRegular userStandard authenticated endpoints

Setting User Roles

Roles are set in Clerk’s session claims. You can set them:
  1. Via Clerk Dashboard: User metadata → Public metadata
  2. Via Clerk API: Update user metadata programmatically
Clerk Public Metadata
{
  "role": "admin"
}
For development, use the ADMIN_EMAIL environment variable to grant admin access without configuring Clerk roles.

Security Best Practices

  • Never expose CLERK_SECRET_KEY in client-side code
  • Always validate webhook signatures in production
  • Use HTTPS for all production endpoints
  • Rotate API keys regularly
  • Use separate Clerk applications for development and production

JWT Token Validation

Clerk’s requireAuth() middleware automatically:
  • Validates JWT signature
  • Checks token expiration
  • Verifies issuer and audience

Session Claims

Access custom session data:
const auth = req.auth();
const userId = auth.userId;           // Clerk user ID
const sessionId = auth.sessionId;     // Session ID
const role = auth.sessionClaims?.role; // Custom role

Protected Route Examples

User Profile Route

router.get('/profile', protectRoute, async (req, res) => {
    res.json({ user: req.user });
});

Admin Product Creation

router.post('/products', protectRoute, adminOnly, async (req, res) => {
    const product = await Product.create(req.body);
    res.json(product);
});

Multi-Role Access

router.patch('/orders/:id', 
    protectRoute, 
    requireRole(['admin', 'moderator']),
    async (req, res) => {
        const order = await Order.findByIdAndUpdate(
            req.params.id,
            { status: req.body.status },
            { new: true }
        );
        res.json(order);
    }
);

Troubleshooting

Cause: JWT token is missing, expired, or invalidSolution:
  • Ensure client sends token in Authorization header
  • Check token hasn’t expired
  • Verify CLERK_SECRET_KEY matches your Clerk application
Cause: User exists in Clerk but not in MongoDBSolution:
  • Check webhook is configured correctly
  • Manually trigger sync-user Inngest function
  • Verify MongoDB connection
Cause: User doesn’t have admin roleSolution:
  • Set role: 'admin' in Clerk public metadata
  • Or add email to ADMIN_EMAIL in development

Build docs developers (and LLMs) love