---
plan: 36-01
phase: 36
title: "isAdmin schema, requireAdmin middleware, /api/auth/me surface, grant script"
status: complete
completed: 2026-04-19
---

## What Was Built

Server-side admin foundation for Phase 36:

1. **isAdmin column** added to the `users` pgTable in `src/db/schema.ts` — `boolean("is_admin").notNull().default(false)`.
2. **Drizzle migration** generated (`drizzle-pg/0009_spotty_lord_tyger.sql`) with `ALTER TABLE "users" ADD COLUMN "is_admin" boolean DEFAULT false NOT NULL`. DB push could not be applied (DB not reachable with default credentials — requires `DATABASE_URL` env var pointing to the running Postgres instance).
3. **requireAdmin middleware** added to `src/server/middleware/auth.ts` — reads `userId` from context (set by `requireAuth`), queries `users.isAdmin`, returns 401 if userId missing, 403 if `!user.isAdmin`, calls `next()` for admins.
4. **isAdmin in /api/auth/me** — `src/server/routes/auth.ts` now includes `isAdmin: fullUser?.isAdmin ?? false` in the returned user object.
5. **`/api/admin/` placeholder route** — `src/server/routes/admin.ts` applies `requireAuth` + `requireAdmin` middleware on `/*` and returns `{ ok: true }` on `GET /`.
6. **Route registration** — `src/server/index.ts` imports and registers `app.route("/api/admin", adminRoutes)`.
7. **grant-admin script** — `scripts/grant-admin.ts` grants or revokes `isAdmin` by `logto_sub`. Accepts `--revoke` flag. Exits 1 on missing sub or user not found.

## Key Files

- `src/db/schema.ts` — isAdmin column added to users table
- `drizzle-pg/0009_spotty_lord_tyger.sql` — migration file
- `src/server/middleware/auth.ts` — requireAdmin exported
- `src/server/routes/auth.ts` — isAdmin in /me response
- `src/server/routes/admin.ts` — new placeholder admin route
- `src/server/index.ts` — adminRoutes registered
- `scripts/grant-admin.ts` — admin grant/revoke script

## Deviations

- **DB push could not be applied** — the default PostgreSQL credentials (`gearbox:gearbox@localhost:5432/gearbox`) don't match the running instance. The migration file is generated and correct. Apply manually with the correct `DATABASE_URL`:
  ```
  DATABASE_URL=<connection-string> bun run db:push
  ```
  This is a deployment/environment concern, not a code defect.

## Self-Check: PASSED

- [x] isAdmin column in schema.ts
- [x] Migration file generated with correct SQL
- [x] requireAdmin middleware exported from auth.ts
- [x] isAdmin in /api/auth/me response
- [x] /api/admin route protected by requireAuth + requireAdmin
- [x] grant-admin.ts script created
- [x] bun run build exits 0
