What you'll build

In this part you'll add a complete authentication layer — users can register with an email and password, log in to receive a JWT token, and use that token to access protected endpoints.

By the end of this part:

Add the users table

In the Supabase SQL editor:

create table users (
  id            uuid primary key default gen_random_uuid(),
  email         text not null unique,
  password_hash text not null,
  name          text,
  role          text not null default 'user',
  created_at    timestamptz default now()
);

Install auth dependencies:

npm install bcryptjs jsonwebtoken
npm install -D @types/bcryptjs @types/jsonwebtoken

Build the auth layer

BlocWeave prompt

Add JWT authentication to the product catalog API. 1. src/schemas/auth.schema.ts — RegisterSchema: { email: string email, password: string min 8, name: string optional }. LoginSchema: { email: string email, password: string }. 2. src/controllers/auth.controller.ts — export: - register(req, res, next): check email not already in users table (return 409 if taken). Hash password with bcrypt (saltRounds=12). Insert into users. Sign a JWT with { sub: user.id, email, role } using JWT_SECRET, expires in "7d". Return 201 with { data: { token, user: { id, email, name, role } } }. - login(req, res, next): fetch user by email. If not found, return 401 { error: "Invalid credentials" } (same message for both failure cases to avoid enumeration). Compare password with bcrypt.compare. If mismatch, return 401. Sign JWT. Return 200 with { data: { token, user } }. - me(req, res, next): return the user attached to req.user by the auth middleware. 3. src/middleware/auth.ts — requireAuth middleware: reads Authorization header, expects "Bearer ". If missing or wrong format, return 401. Verify with jwt.verify(token, JWT_SECRET). If invalid or expired, return 401 { error: "Token invalid or expired" }. Fetch the user row from Supabase by the decoded sub (user ID). If not found, 401. Attach the user to req.user (extend Express.Request via declaration merging in src/types.ts). 4. src/routes/auth.router.ts — POST /register (validate RegisterSchema), POST /login (validate LoginSchema), GET /me (requireAuth). Mount the auth router in src/index.ts under /auth.

Protect the product write routes

BlocWeave prompt

In src/routes/products.router.ts, add requireAuth middleware to the POST, PUT, and DELETE routes. The GET routes remain public. Import requireAuth from src/middleware/auth.ts.

Test authentication

# Register a new user
curl -X POST http://localhost:3000/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"securepassword","name":"Test User"}'

# Log in (copy the token from the response)
curl -X POST http://localhost:3000/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"securepassword"}'

# Use the token (replace TOKEN with the actual value)
curl http://localhost:3000/auth/me \
  -H "Authorization: Bearer TOKEN"

# Try creating a product without a token (should return 401)
curl -X POST http://localhost:3000/products \
  -H "Content-Type: application/json" \
  -d '{"name":"Test","price_ghs":10,"stock":5}'

# Create a product with the token (should return 201)
curl -X POST http://localhost:3000/products \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer TOKEN" \
  -d '{"name":"Test Product","price_ghs":49.99,"stock":20}'

JWT vs sessions: JWTs are stateless — the server doesn't store them. This means you can't invalidate a token before it expires (without extra infrastructure like a token blocklist). For this API, 7-day expiry with short-lived tokens is a good default. If you need instant revocation (e.g. after a password change), add a token_version column to users and include it in the JWT payload.

Project structure after Part 3

src/
  controllers/
    auth.controller.ts
    products.controller.ts
    categories.controller.ts
  middleware/
    auth.ts
    errorHandler.ts
    validate.ts
  routes/
    auth.router.ts
    products.router.ts
    categories.router.ts
  schemas/
    auth.schema.ts
    product.schema.ts
    category.schema.ts
  types.ts
  index.ts

What's next

In Part 4 we add a Paystack webhook endpoint. When a customer pays for a product, Paystack calls your API with a signed event and you update the order status in the database.