---
name: Profile ID generation & approval gating
description: How model profile IDs are generated and how the admin-approval gate is enforced
---

**Human-facing profile IDs (`MDL-XXXX`) must be derived from the row's own serial `id`, never from a row count.**

- Generate via `insertProfileWithGeneratedId` (api-server `src/lib/profile.ts`): insert with a temporary unique placeholder, then update `profileId = MDL-${pad(id)}` inside one transaction.
- **Why:** `MDL-${count+1}` is race-prone (concurrent inserts collide) and deletion-prone (reuses an existing ID after a delete), which throws unique-constraint errors. The serial `id` is monotonic and unique, so it never collides.
- **How to apply:** Use the shared helper for BOTH the user application path (`POST /profiles`) and the admin create path (`POST /admin/profiles`). Don't reintroduce count-based IDs.

**Admin-approval gate must be enforced at every consumption point, not just the directory listing.**

- A profile is only visible/bookable when `approvalStatus="approved"` AND `isActive=true`.
- `GET /profiles` filters on these; `POST /bookings` ALSO re-checks them (reject with 400 otherwise); `GET /profiles/:id` returns 404 unless approved+active OR the requester is the owner (`profile.userId === req.user.id`) OR an admin.
- **Why:** Filtering only the list leaves a hole — direct ID access could book or view pending/rejected/inactive providers. Owner/admin exceptions let an applicant see their own pending profile and let admins moderate.
