# Admin-API Migration Audit **Purpose**: Input to Stage 4 of the edge-cache plan (§5). No routes are migrated here. **Scope**: `codebase/@features/admin/backend-api/src/routes/` → `codebase/@features/api/src/surfaces/admin/` **Date**: 2026-05-16 **DB clarification**: The plan (§5) describes the admin backend as writing to SQLite at `/var/www/quinn.admin/data/quinn.db`. This is inaccurate as of current code. `admin/backend-api/src/db.ts` imports `postgres` and reads `QUINN_ADMIN_DB_URL` — it already runs against postgres. Column 3 below therefore answers "does an equivalent postgres-backed entity already exist in quinn-api's schema?" rather than "does SQLite need a postgres mate?" --- ## Summary counts | Metric | Value | |---|---| | Route files audited | 27 | | Distinct resources | 24 | | Fully covered in quinn-api (`/admin/*`) | 13 | | Partial (some sub-routes missing) | 3 | | Retire without migrating (proxy passthrough) | 1 | | Pure MIGRATE (no quinn-api equivalent) | 7 | | Photo-upload coupling: HIGH | 1 resource (gallery — migrate last) | --- ## Route inventory by resource ### 1. Gallery (photos) | Method | Path | Writes | |---|---|---| | GET | `/api/gallery` | — | | POST | `/api/gallery` | `gallery_items` + disk files + manifest | | PUT | `/api/gallery/:id` | `gallery_items` | | DELETE | `/api/gallery/:id` | `gallery_items` + disk files + manifest | | PUT | `/api/gallery/reorder` | `gallery_items.sort_order` | | POST | `/api/gallery/:id/protect` | delegates to image-protection svc (:3030), polls async | | POST | `/api/gallery/:id/adversary-view` | delegates to image-protection svc (:3030) | | GET | `/api/gallery/adversary-view/jobs/:jobId` | — (proxy) | **Quinn-api equivalent**: `src/surfaces/admin/gallery-items.ts` — GET/POST/PUT/:id/DELETE/:id/POST /reorder. The `protect` and `adversary-view` proxy endpoints have no quinn-api equivalent. **Status**: PARTIAL — base CRUD exists. Protection trigger + adversary-view proxy: MIGRATE. **Postgres schema**: `gallery_items` entity exists in quinn-api (`src/entities/gallery-item/`). **Port collision hazard**: `gallery.ts` hardcodes `IMAGE_PROTECTION_URL` default as `http://localhost:3030` — the same port quinn-api itself binds. This must be resolved before migration: add an explicit non-default port for image-protection or force the env var in the unit file. **Purge paths on mutation**: `/provider-api/gallery`, `/photos/.jpg`, `/photos/.webp` --- ### 2. Rates | Method | Path | Writes | |---|---|---| | GET | `/api/rates` | — | | POST | `/api/rates` | `rate_sections` | | PUT | `/api/rates/:id` | `rate_sections` | | DELETE | `/api/rates/:id` | `rate_sections` + cascade `rate_entries` | | POST | `/api/rates/:sectionId/entries` | `rate_entries` | | PUT | `/api/rates/entries/:entryId` | `rate_entries` | | DELETE | `/api/rates/entries/:entryId` | `rate_entries` | **Quinn-api equivalent**: `src/surfaces/admin/rate-cards.ts` — full CRUD for sections + entries. **Status**: EXISTS (admin uses `rate_sections`/`rate_entries`; quinn-api uses `rate_cards`/`rate_card_entries` — verify table names match or confirm the entity is the same object before retiring). **Purge paths**: `/www/rates`, `/provider-api/rates` --- ### 3. Tour Stops | Method | Path | Writes | |---|---|---| | GET | `/api/tour-stops` | — | | POST | `/api/tour-stops` | `tour_stops` | | PUT | `/api/tour-stops/:id` | `tour_stops` | | DELETE | `/api/tour-stops/:id` | `tour_stops` | **Quinn-api equivalent**: `src/surfaces/admin/tour-stops.ts` — full CRUD. **Status**: EXISTS. **Purge paths**: `/www/tour`, `/provider-api/tour-stops` --- ### 4. Policies | Method | Path | Writes | |---|---|---| | GET | `/api/policies` | — | | POST | `/api/policies` | `policy_sections` | | PUT | `/api/policies/:id` | `policy_sections` | | DELETE | `/api/policies/:id` | `policy_sections` + cascade `policy_items` | | POST | `/api/policies/:sectionId/entries` | `policy_items` | | PUT | `/api/policies/entries/:entryId` | `policy_items` | | DELETE | `/api/policies/entries/:entryId` | `policy_items` | **Quinn-api equivalent**: `src/surfaces/admin/policies.ts` — full CRUD. **Status**: EXISTS. **Purge paths**: `/www/screening` (policies appear on screening/provider page) --- ### 5. Specialties | Method | Path | Writes | |---|---|---| | GET | `/api/specialties` | — | | POST | `/api/specialties` | `specialty_categories` + optional first entry | | PUT | `/api/specialties/:categorySlug` | `specialty_categories` | | DELETE | `/api/specialties/:categorySlug` | cascade all entries | | POST | `/api/specialties/:categorySlug/entries` | `specialties` | | PUT | `/api/specialties/entries/:itemId` | `specialties` | | DELETE | `/api/specialties/entries/:itemId` | `specialties` | **Quinn-api equivalent**: `src/surfaces/admin/specialties.ts` — full CRUD for categories + entries. **Status**: EXISTS. **Purge paths**: `/www/specialties`, `/provider-api/specialties` --- ### 6. Site Text | Method | Path | Writes | |---|---|---| | GET | `/api/site-text` | — | | POST | `/api/site-text` | `site_text` | | PUT | `/api/site-text` | `site_text` (upsert) | | PUT | `/api/site-text/upsert` | `site_text` (upsert alias) | | PUT | `/api/site-text/:id` | `site_text` | | DELETE | `/api/site-text/:id` | `site_text` | **Quinn-api equivalent**: `src/surfaces/admin/site-text.ts` — full CRUD + upsert. **Status**: EXISTS. **Purge paths**: varies by namespace — at minimum all public pages via `/www/*` --- ### 7. Profile (identity / physical / contact) | Method | Path | Writes | |---|---|---| | GET/PUT | `/api/identity` | provider identity fields | | GET/PUT | `/api/physical` | physical attribute fields | | GET/PUT | `/api/contact` | contact + social fields | **Quinn-api equivalent**: `src/surfaces/admin/provider-profile.ts` — GET/PUT / + PATCH /:section. **Status**: EXISTS (three discrete routes in admin vs section-patching in quinn-api; semantically equivalent). **Purge paths**: `/provider-api/profile`, `/www/about` --- ### 8. About **Quinn-api equivalent**: `src/surfaces/admin/about.ts` — GET + PUT / + activity CRUD. **Status**: EXISTS (confirm field parity before retiring admin copy — admin manages about content through `profile.ts`/`site-text.ts`; may not need a separate about route). **Purge paths**: `/www/about` --- ### 9. Shop Listings | Method | Path | Writes | |---|---|---| | GET | `/api/shop` | — | | POST | `/api/shop` | `shop_listings` | | GET/PUT/DELETE | `/api/shop/:id` | `shop_listings` | | POST | `/api/shop/:id/photos` | `shop_listing_photos` + disk | | DELETE | `/api/shop/:id/photos/:photoId` | `shop_listing_photos` + disk | | PUT | `/api/shop/:id/photos/reorder` | `shop_listing_photos.sort_order` | **Quinn-api equivalent**: `src/surfaces/admin/shop-listings.ts` — base CRUD. Photo sub-routes absent. **Status**: PARTIAL — listing CRUD exists; photo upload/delete/reorder on shop items: MIGRATE. **Purge paths**: `/www/shop`, `/provider-api/shop` --- ### 10. Roster Content | Method | Path | Writes | |---|---|---| | GET | `/api/roster-content` | — | | PUT | `/api/roster-content/:slug` | `roster_content` (upsert) | **Quinn-api equivalent**: `src/surfaces/admin/roster-content.ts` — GET + PUT /:slug. **Status**: EXISTS. **Purge paths**: `/provider-api/roster-content` --- ### 11. Verified Profiles | Method | Path | Writes | |---|---|---| | GET | `/api/verified-profiles` | — | | POST | `/api/verified-profiles` | `verified_profiles` | | PUT | `/api/verified-profiles/:id` | `verified_profiles` | | DELETE | `/api/verified-profiles/:id` | `verified_profiles` | **Quinn-api equivalent**: `src/surfaces/admin/verified-profiles.ts` — full CRUD. **Status**: EXISTS. **Purge paths**: `/www/verified-profiles` --- ### 12. Etiquette | Method | Path | Writes | |---|---|---| | GET | `/api/etiquette` | — | | POST | `/api/etiquette` | `etiquette_sections` | | PUT | `/api/etiquette/:id` | `etiquette_sections` | | DELETE | `/api/etiquette/:id` | cascade items | | POST/PUT/DELETE | `/api/etiquette/*/entries/*` | `etiquette_items` | **Quinn-api equivalent**: `src/surfaces/admin/etiquette.ts` — full CRUD. **Status**: EXISTS. **Purge paths**: `/www/etiquette` --- ### 13. Hero Strip | Method | Path | Writes | |---|---|---| | GET | `/api/hero-strip` | — | | POST | `/api/hero-strip` | `hero_strip_items` | | PUT | `/api/hero-strip/:id` | `hero_strip_items` | | DELETE | `/api/hero-strip/:id` | `hero_strip_items` | **Quinn-api equivalent**: `src/surfaces/admin/hero-strip.ts` — full CRUD. **Status**: EXISTS. **Purge paths**: `/www/home`, `/provider-api/hero-strip` --- ### 14. Mail Admin (mailserver accounts) | Method | Path | Writes | |---|---|---| | GET | `/api/mail-admin/accounts` | — | | POST | `/api/mail-admin/accounts` | docker-mailserver API | | DELETE | `/api/mail-admin/accounts/:email` | docker-mailserver API | **Quinn-api equivalent**: `src/surfaces/admin/mail-admin.ts` — full CRUD (mailserver proxy). **Status**: EXISTS. **Purge paths**: none (not public content) --- ### 15. System Status | Method | Path | Writes | |---|---|---| | GET | `/api/system/status` | — | **Quinn-api equivalent**: `src/surfaces/admin/system-status.ts` — GET /. **Status**: EXISTS (read-only — migrate first in the sequence). **Purge paths**: none --- ### 16. Bookings (proxy passthrough) | Method | Path | Writes | |---|---|---| | POST | `/api/bookings` | proxies to quinn-api `/public/bookings` | **Quinn-api equivalent**: this IS quinn-api — the admin backend is a passthrough to the public bookings endpoint. **Status**: RETIRE without migration. Point the admin frontend directly at quinn-api `/public/bookings`. **Purge paths**: none --- ### 17. Touring Subscribers — PARTIAL | Method | Path | Writes | |---|---|---| | POST | `/api/touring/subscribe` | `touring_subscriptions` (unauthenticated) | | GET | `/api/touring/subscribers` | — | **Quinn-api equivalent**: `src/surfaces/admin/touring-subscribers.ts` — GET / only. **Status**: PARTIAL — read list exists. The unauthenticated `subscribe` POST belongs in the quinn-api public surface; check `/public/touring` before migrating. **Purge paths**: none --- ### 18. MIGRATE — Cult of Lilith | Method | Path | Writes | |---|---|---| | GET | `/api/cult-of-lilith` | — | | PUT | `/api/cult-of-lilith/:key` | `cult_of_lilith_sections` | | PUT | `/api/cult-of-lilith/batch` | batch upsert | **Quinn-api equivalent**: none. `lore_sections` entity exists in quinn-api; `cult_of_lilith` may be a distinct table — verify on `QUINN_ADMIN_DB_URL` before migration. **Status**: MIGRATE. **Purge paths**: `/www/cult-of-lilith` --- ### 19. MIGRATE — Page Illustrations | Method | Path | Writes | |---|---|---| | GET | `/api/page-illustrations` | — | | PUT | `/api/page-illustrations` | illustration config (file-backed or in DB — unclear) | **Quinn-api equivalent**: none. Backing store needs investigation before migration. **Status**: MIGRATE. **Purge paths**: `/www/*` (illustrations appear across pages) --- ### 20. MIGRATE — Mail Threads (email client) | Method | Path | Writes | |---|---|---| | GET | `/api/mail/inboxes` | — | | GET/GET/:uid | `/api/mail/threads` | — | | POST | `/api/mail/threads/:uid/reply` | sends email via IMAP/SMTP | | PATCH | `/api/mail/threads/:uid/read` | IMAP flags | | POST | `/api/mail/threads/:uid/draft/approve` | `draft_replies` | | POST | `/api/mail/threads/:uid/draft/reject` | `draft_replies` | | PATCH | `/api/mail/draft/:id` | `draft_replies` | **Quinn-api equivalent**: none (messaging is in quinn.messenger / quinn.m surface, not the quinn-api admin surface). **Status**: MIGRATE. Consider whether this belongs in quinn.m's surface rather than `/admin/*`. **Purge paths**: none (internal inbox) --- ### 21. MIGRATE — Destinations (tour city pages) | Method | Path | Writes | |---|---|---| | GET | `/api/destinations` | — | | POST | `/api/destinations` | `destinations` | | PUT | `/api/destinations/:id` | `destinations` | | DELETE | `/api/destinations/:id` | `destinations` | **Quinn-api equivalent**: `src/surfaces/admin/pseo-destinations.ts` covers pSEO metro destinations (slug-based, different shape). The admin `destinations` table (id/sort_order-based) may be a separate concept. Verify schema before conflating. **Status**: MIGRATE (pending schema confirmation). **Purge paths**: `/www/destinations`, `/_/escorts/in-{city}` pages --- ### 22. MIGRATE — DB Sync `GET /api/sync/info`, `GET /api/sync/export`, `POST /api/sync/import`, `POST /api/sync/push`, `POST /api/sync/pull` **Status**: MIGRATE or RETIRE. These sync the admin DB to a remote. With both services on postgres, the push/pull mechanism may be fully obsolete. Audit live callers before deciding. --- ### 23. MIGRATE — Backup / Export / Photo Export / Restore `GET /api/backup`, `GET /api/export`, `GET /api/export/stats`, `POST /api/export/photos/:size`, `POST /api/restore` **Status**: MIGRATE or RETIRE. These are SQLite-era backup/restore + photo resize triggers. Audit usage before migrating. --- ### 24. MIGRATE — Device Link (TOTP pairing) `POST /api/device-link/start`, `GET /api/device-link/poll/:token`, `GET /auth/device-link/callback` **Status**: MIGRATE. Belongs in quinn-api's SSO/auth surface. **Purge paths**: none --- ## Recommended migration order 1. **System status** — GET-only, zero writes; safe first to verify routing plumbing. 2. **Tour stops** — full parity in quinn-api. Wire `purgeEdge(['/www/tour', '/provider-api/tour-stops'])` into the handler and retire admin copy. 3. **Site text** — full parity. Wire purge for affected namespaces. 4. **Rates, Policies, Etiquette, Specialties, Roster content, Verified profiles, Hero strip** — all have quinn-api equivalents; retire admin copies in a batch. Wire purge per resource. 5. **Provider profile / About** — exists in quinn-api; confirm field parity first. 6. **Shop listings** (base CRUD only — skip photo sub-routes for now) — quinn-api surface exists. 7. **Cult of Lilith** — new quinn-api surface needed under `/admin/cult-of-lilith`; straightforward CRUD. 8. **Destinations** — verify vs pseo-destinations schema before migrating. 9. **Touring subscribe** (public POST) + touring-subscribers (admin GET) — wire public surface endpoint. 10. **Mail threads** — largest non-content feature; placement in quinn.messenger (quinn.m) vs `/admin/*` needs a decision. 11. **Device link** — move to quinn-api SSO surface. 12. **DB Sync + Backup + Restore** — audit live usage; retire if no active caller. 13. **Page illustrations** — investigate disk vs DB backing before migrating. 14. **Gallery (photos)** — last. Three-way coupling: admin backend, image-protection service (port 3030 conflicts with quinn-api; must be resolved before migration), and photo origin on black (track B of the edge-cache plan). The upload flow — multipart → resize → WebP generation → manifest update → disk write → DB insert — is the highest-coupling path in the entire surface. Do not migrate until track B (dedicated photo server on black:8081) is complete and the image-protection port conflict is resolved.