# Build Journal: User Authentication API

**Date:** 2026-01-01
**Duration:** 4 hours
**Status:** Complete

---

## The Goal

Add JWT-based authentication to our REST API. Users need to register, login, and access protected endpoints. We're using Express.js with a PostgreSQL database.

This is foundational infrastructure - every future feature depends on knowing who the user is.

---

## What We Built

### Architecture

```
┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Client    │────▶│  Express    │────▶│  PostgreSQL │
│             │     │  + JWT      │     │  (users)    │
└─────────────┘     └─────────────┘     └─────────────┘
                          │
                    ┌─────▼─────┐
                    │  bcrypt   │
                    │ (hashing) │
                    └───────────┘
```

### Components

| Component | Status | Notes |
|-----------|--------|-------|
| POST /auth/register | Working | Creates user, returns JWT |
| POST /auth/login | Working | Validates creds, returns JWT |
| GET /auth/me | Working | Returns current user from token |
| authMiddleware | Working | Protects routes |
| User model | Working | email, password_hash, created_at |

---

## The Journey

### Phase 1: Database Setup

**What we tried:**
Set up the users table with a simple schema.

**What happened:**
Forgot to add a unique constraint on email. Found out when registration started silently creating duplicate users.

```
ERROR: duplicate key value violates unique constraint "users_email_key"
```

Wait, no - we didn't get this error. That's the problem. Duplicates were allowed.

**The fix:**
```sql
ALTER TABLE users ADD CONSTRAINT users_email_unique UNIQUE (email);
```

**Lesson:**
Always add unique constraints at the database level, not just application validation.

---

### Phase 2: Password Hashing

**What we tried:**
Used bcrypt to hash passwords on registration.

**What happened:**
Login always failed. Spent 30 minutes debugging before realizing we were comparing the plaintext password to itself, not to the hash.

```javascript
// Wrong - comparing password to password
const valid = await bcrypt.compare(password, password);

// Right - comparing password to stored hash
const valid = await bcrypt.compare(password, user.password_hash);
```

**The fix:**
Actually read the bcrypt docs. `compare(plaintext, hash)` - order matters.

**Lesson:**
When auth fails silently, log what you're actually comparing (redacted).

---

### Phase 3: JWT Middleware

**What we tried:**
Created middleware to verify JWT and attach user to request.

**What happened:**
Tokens from yesterday stopped working. Turns out we set expiry to 1 hour but were testing with day-old tokens from our notes.

```
JsonWebTokenError: jwt expired
```

**The fix:**
Set expiry to 7 days for development, added refresh token TODO for production.

**Lesson:**
Use short expiry in tests, longer in dev. Don't copy-paste tokens between sessions.

---

## Test Results

### Registration

**Command:**
```bash
curl -X POST http://localhost:3000/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email": "test@example.com", "password": "secretpass123"}'
```

**Response:**
```json
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "user": {
    "id": 1,
    "email": "test@example.com",
    "created_at": "2026-01-01T10:30:00Z"
  }
}
```

**Result:** Pass - user created, token returned

### Protected Route

**Command:**
```bash
curl http://localhost:3000/auth/me \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```

**Response:**
```json
{
  "id": 1,
  "email": "test@example.com"
}
```

**Result:** Pass - user data returned from token

---

## Code Samples

### Auth Middleware

```javascript
const authMiddleware = async (req, res, next) => {
  const header = req.headers.authorization;
  if (!header?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No token provided' });
  }

  try {
    const token = header.slice(7);
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    req.user = await User.findById(payload.userId);
    next();
  } catch (err) {
    res.status(401).json({ error: 'Invalid token' });
  }
};
```

This middleware extracts the JWT, verifies it, and attaches the full user object to the request. The `?.` optional chaining handles missing headers gracefully.

---

## What's Left

- [ ] Refresh token implementation
- [ ] Password reset flow
- [ ] Rate limiting on auth endpoints
- [ ] Email verification

---

## Cost/Performance Analysis

| Metric | Value | Notes |
|--------|-------|-------|
| bcrypt rounds | 12 | ~250ms hash time, good balance |
| JWT expiry | 7 days | Too long for prod, fine for dev |
| Token size | 284 bytes | Acceptable for headers |

---

## AI Experience Reflection

### What Worked Well

- Incremental approach: got each endpoint working before moving to the next
- Agent suggested adding the unique constraint after I described the duplicate user bug
- Quick iteration on the curl commands for testing

### What Was Frustrating

- Spent too long on the bcrypt comparison bug - should have asked for help sooner
- Context about the existing database schema was lost mid-conversation

### Communication Notes

- Worked best when I described the symptom ("login always fails") rather than my guess ("I think JWT is broken")
- Breaking the work into phases helped maintain focus

---

## Improvements

*Actionable learnings for future work.*

### Architectural

- Should have added database constraints BEFORE writing application code - the schema is the source of truth
- Password hashing belongs in the User model (as a pre-save hook), not in the route handler

### Workflow

- Write the curl test commands FIRST, then implement to make them pass - TDD but for APIs
- Test with fresh tokens, not ones from previous sessions

### Tool Usage

- Use `jwt.io` to decode tokens when debugging - faster than console.log
- The `--verbose` flag on curl shows headers, useful for auth debugging

### Domain Knowledge

- bcrypt.compare() argument order is (plaintext, hash) - not obvious
- JWT expiry is in seconds when using jsonwebtoken library, not milliseconds
- PostgreSQL unique constraints create an index automatically - no need to add one separately

---

## Files Changed

```
src/
├── routes/
│   └── auth.js           # Register, login, me endpoints
├── middleware/
│   └── auth.js           # JWT verification middleware
├── models/
│   └── user.js           # User model with password hashing
└── db/
    └── migrations/
        └── 001_users.sql # Users table with constraints
```

---

*Next entry: Adding role-based access control (admin vs regular users)*
