# BumpOnIt admin: production handoff

## What has already been built

The present proof of concept contains the complete front-end workflow for:

- structured site wording;
- affiliate listing administration;
- Journal authoring and publishing;
- public catalogue, Journal index and article rendering;
- draft/published states, basic revisions, activity history and JSON backups.

Two deliberately replaceable browser adapters sit underneath those screens:

- `auth-adapter.js` obtains a Clerk session and asks `/api/admin-session` to verify the user's server-controlled role;
- `admin-access` stores the approved team email allowlist in Neon and maps each email to an application role;
- `cms-store.js` supplies copy, affiliate listing and Journal data while synchronising with `/api/cms`.

Clerk identity, admin-role verification and Neon persistence are connected. The browser cache is an offline/fallback copy; Neon is the shared source once the API has hydrated.

## Non-negotiable production boundary

Clerk proves who a user is. It does **not** store site wording, products, articles or images. A safe shared admin therefore needs all three of the following:

1. **Clerk** for sign-in, sessions and server-verified team roles. This is connected in the proof of concept.
2. **A shared Neon database** for copy, listings, posts, revisions and audit events. This is connected through Vercel Marketplace.
3. **Managed image storage** for product and Journal media.

The static HTML shell is public, but it contains no private records. The content studio stays hidden until `/api/admin-session` verifies the Clerk token and a server-controlled role. Every future content read or write endpoint must repeat the relevant server-side permission check; protecting the interface alone is not enough.

## Recommended production shape

The current static entry points and Vercel Functions can support the proof of concept. A later Next.js migration remains sensible as the product grows. The current stack uses:

- Clerk for identity and organization membership;
- Neon PostgreSQL through Vercel Marketplace for content;
- Vercel Blob or Cloudinary for licensed images;
- server routes/actions for every write and publish operation;
- a server redirect such as `/go/[listingId]` for affiliate click logging;
- Vercel Cron or a workflow/queue for expiry and stale-price checks.

The public catalogue and Journal should remain cacheable. Admin reads and all mutations should be dynamic and permission checked on the server.

## Current Clerk integration

- The repository is linked to the BumpOnIt Clerk application.
- The development instance is configured locally; Clerk's production instance still needs to be enabled before launch.
- `clerk-auth.js` loads Clerk's official JavaScript UI for shopper and admin accounts.
- `@clerk/backend` verifies session tokens in `/api/admin-session`.
- The **Permissions** panel in `admin.html` is the normal way to grant access: add the colleague's verified email and choose `admin`, `editor` or `author`.
- The server checks the signed-in Clerk user's primary email against `admin_access` before granting the role. The match is case-insensitive and is never taken from browser input.
- Existing `publicMetadata.role` values remain as a bootstrap fallback for the first owner; move ongoing access management into the Permissions panel.
- Optional `publicMetadata.permissions` can contain an explicit subset of the permissions below.
- Local credentials are in Git-ignored `.env.local`; Vercel needs the same variable names configured separately.

Example full-access public metadata:

```json
{ "role": "admin" }
```

## Clerk production checklist

1. Add the live production and preview domains to the Clerk application.
2. Add `CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` to each Vercel environment.
3. Use the Neon-backed Permissions panel to make administrator assignment invitation-only; keep Clerk role metadata server-controlled as a bootstrap fallback.
4. Use an organization role or server-controlled application role such as `org:admin`, `org:publisher` and `org:editor`.
5. Map those roles to the permissions already used by this prototype:
   - `content:edit`
   - `deals:edit`
   - `journal:edit`
   - `publish`
   - `admin:manage`
6. Protect every `/api/admin(.*)` route and every record it reads or mutates.
7. On every server mutation, call Clerk's server authentication helper and verify the required permission again. Client checks are convenience only.
8. Store the Clerk user ID on every audit record. Never allow a public endpoint to grant an admin role.
9. Add Clerk webhooks only if the app must mirror memberships into its own admin-profile table; verify webhook signatures.
10. Enable MFA for the client team and test signed-out, editor and publisher accounts separately.

### Adding a colleague

1. Open `admin.html` while signed in as an administrator.
2. Open **Permissions**, enter the email the colleague will use in Clerk, and choose a role.
3. Ask them to create/sign in to their Clerk account with that email.
4. If they were already signed in, have them sign out and back in so the new session is checked.
5. Pause the entry to remove access. Keep at least one active administrator so the workspace cannot be locked.

The future auth adapter should keep this conceptual contract:

```text
getCurrentUser() -> user | null
getPermissions() -> string[]
requireAdmin(permission) -> { allowed, user, reason? }
signOut()
```

## Connected database records

The versioned Drizzle schema lives in `db/schema.js`, with generated SQL migrations in `db/migrations/`.

### Site copy

- stable key and section;
- draft value and published value;
- updated/published timestamps and user IDs;
- immutable revisions.

### Affiliate listing

- ID, slug, title, brand, category and subcategory;
- card summary and editorial rationale;
- retailer, original URL, tracked URL and affiliate-network reference;
- current/reference price, currency, delivery and offer conditions;
- region, last-checked date and expiry date;
- image record and alt text;
- badge, featured flag and status;
- created/updated/published user IDs and timestamps.

Use statuses: `draft`, `review`, `published`, `expired`, `archived`.

### Journal post

- ID, slug, title, excerpt and sanitised body;
- author, category and tags;
- cover image and alt text;
- SEO title/description;
- featured flag, status and publish time;
- created/updated/published user IDs and timestamps;
- immutable revisions.

Use statuses: `draft`, `review`, `scheduled`, `published`, `archived`. The local prototype reveals a scheduled post when a page is loaded after its chosen time; production still needs a server process for reliable cache invalidation, notifications and audit state.

### Audit event

- actor Clerk user ID;
- action and entity type/ID;
- timestamp;
- before/after summary or revision ID;
- request ID and optional reason.

## Current API boundary

The public and admin browser caches are separate. Successful server saves notify other public tabs to fetch published content and redraw without reloading. Editor Publish actions await server confirmation; a failed save keeps the form open for correction or retry. The API enforces `publish` for changes to published or scheduled listings/posts, including removal of live records.

Paused Neon access entries override Clerk metadata. Database lookup failures fail closed, except a missing first-install permissions table can still use the owner’s existing metadata role to bootstrap setup.

`GET /api/cms?scope=public` returns published content. `GET /api/cms?scope=admin` and `PUT /api/cms` require a verified Clerk admin session. Writes are compared with the current workspace so the server can require `content:edit`, `deals:edit`, `journal:edit` and `publish` as appropriate.

As the catalogue grows, split the whole-workspace mutation into narrower routes such as:

The admin UI can retain its current intent while calls move to endpoints such as:

```text
GET  /api/content/copy
PUT  /api/admin/content/copy/draft
POST /api/admin/content/copy/publish

GET  /api/products
POST /api/admin/products
PUT  /api/admin/products/:id
POST /api/admin/products/:id/publish
DELETE /api/admin/products/:id

GET  /api/journal
POST /api/admin/journal
PUT  /api/admin/journal/:id
POST /api/admin/journal/:id/publish
DELETE /api/admin/journal/:id

POST /api/admin/media/presign
GET  /go/:listingId
```

All input must be schema validated on the server. Sanitize formatted article content on write and render. Permit HTTPS retailer URLs only and consider an allow-list for approved retailers and affiliate redirectors.

## Media rules

- Upload directly to managed storage with a short-lived signed token.
- Allow JPEG, PNG and WebP initially; verify the actual file type, dimensions and size server-side.
- Generate responsive sizes and retain original licence/source notes internally.
- Require useful alt text or an explicit decorative flag.
- Do not store base64 images in PostgreSQL or Clerk metadata.

## Affiliate redirect and offer maintenance

Public links should point to `/go/:listingId`, not expose mutable affiliate destinations throughout cached HTML. The server should:

1. load a currently published, unexpired listing;
2. record a privacy-conscious click event;
3. redirect to the approved affiliate URL;
4. reject unsafe or malformed destinations.

Add stale-offer flags based on `lastChecked`, expire offers automatically and retain an editor review step before feed/import changes become public. Generic scraping can breach site terms and is brittle; prefer affiliate-network feeds, retailer APIs and permitted imports.

## Definition of production-ready

- Admin pages and APIs reject unauthenticated users server-side.
- Editor/publisher permissions are tested.
- Two different devices see the same saved content.
- Media uploads survive logout and deploys.
- Draft, preview, publish, schedule, archive and restore flows are audited.
- Affiliate clicks redirect safely and are measurable.
- Expired/stale prices cannot remain silently promoted.
- Backups, error monitoring, accessibility checks and recovery procedures are in place.
