# CLAUDE.md — Gate Pass Codebase Context

This file gives Claude Code the minimum context needed to work on this codebase without making naming, branding, or convention errors. Read it before writing any code, generating any file names, or producing any output that references the product.

---

## Product Identity

**Product name:** Gate Pass
**Type:** Multi-tenant SaaS visitor management / gate-pass platform
**Stack:** Node.js + Express + TypeScript (API), Prisma + PostgreSQL, Socket.IO; React 18 + Vite + TypeScript (web)

Gate Pass is a standalone product. Do not reference any parent brand or ecosystem in code, comments, file names, or UI strings unless explicitly instructed. The repo folder is named `Gate Pass` for legacy reasons — that folder name is not the product name. When generating identifiers, env vars, or UI strings, use `Gate Pass`.

---

## Scope Exclusions

The **web app (`web/src/`)** and the **API (`api/`)** are the only deployable surfaces in active scope. The web app has two route shells — `App.tsx` for desktop/admin and `AppMobile.tsx` for the reception/scanner — both built by the same Vite build under `web/`.

**Out of scope — do not generate, modify, or reference unless explicitly instructed:**

| Surface | Files / directories | Build scripts (in `web/package.json`) |
|---|---|---|
| iOS app shell (Capacitor) | `web/ios/`, Xcode project, App Store metadata | — |
| Android app shell (Capacitor) | `web/android/` | — |
| Capacitor config | `web/capacitor.config.ts` | `dev:mobile`, `build:mobile`, `mobile:sync`, any `npx cap …` command |
| Electron desktop shell | `web/electron/main.js`, `web/electron/preload.js` | `electron:dev`, `electron:build`, `electron:pack` |

**Native bundle ID — do not rename in this session:**
- **Live bundle:** `com.visitydemo.visitorportal` — appears in `web/capacitor.config.ts`, `web/package.json` (`appId`), `web/ios/App/App.xcodeproj/project.pbxproj`, `web/android/app/build.gradle`, `web/android/app/src/main/res/values/strings.xml`, and the Java package path `web/android/app/src/main/java/com/visitydemo/visitorportal/MainActivity.java`.
- **Planned future bundle:** `com.gatepass.app`.
- **Why not rename now:** the live bundle is the primary key for the iOS/Android/Electron apps already on customers' devices. Changing it requires a new App Store / Play Store listing, breaks every existing push token in `DeviceToken`, and invalidates auto-updates. Migration is a coordinated re-release with marketing comms, not a code refactor. Out of scope until that release is explicitly planned.

If a task implies mobile-app or desktop-shell work, ask before proceeding. The shells share their JS bundle with the web app, so changes you make in `web/src/` reach mobile and Electron through the next build — but you do not need to rebuild or test the shells in this session.

---

## Naming Rules — Mandatory

These rules are non-negotiable. Apply them to every file, variable, string, comment, env var, and commit message you generate.

**Forbidden brand tokens — never use in any new code, comment, doc, or UI string:**
- `Visity`, `HoSt Visity`, `hostvisity`
- `Visitor Portal`, `visitor demo`, `visitydemo`, `visitorportal`
- `visity_demo`, `visity-implementation-guide`
- Any variation or abbreviation of the above

**The noun "visitor" is a domain concept and is preserved.** The Prisma model `Visitor`, its relations (`VisitorScanLog`, `VisitorEditLog`, `VisitorCheckpoint`, `VisitorRequest`, `VisitorReason`), the enums (`VisitorStatus`, `VisitorFrequencyType`, `VisitorRequestStatus`), the foreign keys (`visitorId`), the controllers (`visitors.controller.ts`, `visitor-scanner.controller.ts`, …), the live API routes (`/api/visitors`, `/api/visitor-scanner`), and the noun used in UI copy ("Add visitor", "Visitor list", etc.) all stay. This rule forbids the **legacy brand/product names** that contain "visitor", not the noun.

**Brand and identifier conventions:**

| Context | Use |
|---|---|
| UI strings, page titles, email From name, app shells, push notification titles | `Gate Pass` (two words, with space) |
| Filenames, branch names, commit prefixes, lowercase identifiers | `gatepass` (one token, no space, no hyphen) |
| Implementation guide filename | `gatepass-implementation-guide.md` |
| Repo/folder/branch for staging environment | `UAT` |

**Internal package names — fine to leave as-is:**
- `api/package.json` → `"name": "visitor-api"`
- `web/package.json` → `"name": "visitor-web"`

These are Node package identifiers, not UI strings or brand references.

**Legacy bundle ID in out-of-scope shells:**
`com.visitydemo.visitorportal` appears in `web/ios/`, `web/android/`, `web/capacitor.config.ts`, `web/electron/`, and the `appId` in `web/package.json` (electron-builder). Per the Scope Exclusions section, do not modify mobile/desktop shells. Renaming this bundle in a live release would orphan every existing app install and require all users to reinstall. Leave it; do not propagate `visitydemo` / `visitorportal` to any new file.

If you encounter a forbidden brand token in existing in-scope code (`api/`, `web/src/`), flag it in your reply rather than silently renaming. The codebase has been swept and is currently clean of these tokens in the in-scope tree — any future appearance is a regression.

---

## Branch and Commit Conventions

- All work goes to the `gatepass` branch.
- Commit message format: `gatepass: <comma-separated topics>`
- Example: `gatepass: add walk-in QR filter to visitor list, fix scan log attribution`

---

## Audit Requirements

There is **one** audit log model in the schema: `VisitorEditLog`. It captures before/after diffs of whitelisted user-editable fields on `Visitor` along with `editedByAdminId` (null = workspace owner edited directly), plus a display snapshot (`editedByName`, `editedByEmail`) so deleted admins still surface as a name in the history.

The `updateVisitor` endpoint is the only writer today. When you add a new mutation that changes user-editable `Visitor` fields, write a `VisitorEditLog` row with:
- The actor's `Admin.id` from `req.adminId` (or `null` if the request is an owner — `req.adminId` undefined, `req.ownerId` set).
- A name/email snapshot resolved from the `Admin` row or the `Owner` row.
- A compact `{ field: { from, to } }` JSON limited to whitelisted fields.

For other mutations (create visitor, decision, scanner mark-arrived, walk-in submit, etc.), there is **no general `logActivity` helper**. If you introduce a new generic activity log, do it as a deliberate, scoped piece of work — don't sprinkle ad-hoc audit writes through controllers.

There is one approver-scoped activity feed today: `GET /api/visitors/approvers/activity`. It aggregates approver-relevant events for the signed-in approver/admin. It is **not** an event-id-scoped endpoint — there are no events in this product.

---

## Schema Change Protocol

1. Edit `api/src/prisma/schema.prisma`.
2. Run `npm run prisma:migrate -- --name <topic>` locally to generate the migration file (Prisma schema lives at `src/prisma/schema.prisma`, not the default root).
3. Commit the migration file with the code change in the same commit.
4. In production, CI runs `prisma migrate deploy`. **Never run `prisma db push` in production.** The `prisma:push` npm script is for local prototyping only.

---

## Environment Variable Reference

```env
# Backend (api/.env)
DATABASE_URL=postgresql://<user>:<pwd>@<host>/gatepass
JWT_SECRET=<random-256-bit>
PUBLIC_API_URL=https://api.gp.vcarrd.com
FRONTEND_URL=https://gp.vcarrd.com      # or '*' for permissive CORS in dev
EMAIL_FROM=noreply@vcarrd.com
S3_BUCKET_NAME=gatepass-assets
PORT=4200                                     # API default

# Frontend (web/.env)
VITE_APP_NAME=Gate Pass
VITE_API_URL=https://api.gp.vcarrd.com
```

Do not use old domain strings, old bucket names, or old database names when generating config files, documentation, or scaffolding.

---

## Type-Check Gate

Before committing any TypeScript changes, both packages must pass:

```bash
cd web && npx tsc --noEmit
cd api && npx tsc --noEmit
```

Do not generate code that skips this gate.

---

## Key Architectural Invariants

### Tenancy

- Every `Visitor`, `WalkInQR`, `VisitorScanLog`, `Notification`, `Admin`, `Approver`, `Department`, `VisitorReason`, `EmailTemplate`, `EmailAutomation`, and `VisitorCheckpoint` row is scoped by `ownerId`. Never write a query or endpoint that leaks data across tenants.
- The `Owner` row is the tenant root. `PlatformAdmin` is the product vendor (us) and sits above all owners — only platform routes mounted under `/api/platform` may bypass `ownerId` scoping.

### Auth / JWT

The JWT issued by `/api/auth` carries:

```ts
{
  ownerId: string,
  adminId?: string,
  parentAdminId?: string,            // when this is a sub-admin (receptionist)
  adminFlags?: {
    canManageVisitors, canManageApprovers, canManageSettings,
    canApproveRequests, canScanCheckpoint, canManageSubAdmins,
    canSeeAllVisitors,                // booleans
  },
}
```

`requireAuth` (in `api/src/middleware/auth.ts`) populates `req.ownerId`, and when present `req.adminId`, `req.parentAdminId`, `req.adminFlags`. For a workspace owner, `adminId` is undefined. For a sub-admin, both are set and `adminId !== ownerId`.

Scanner routes use a separate `scanContext` token shape — see `/api/visitor-scanner` middleware.

Platform-admin tokens carry `platformAdminId` instead of `ownerId`; never mix the two.

### Visitor model

- `Visitor.shortId` is the unguessable token embedded in the visitor's QR. Use it on public-facing endpoints (scanner lookups, decision pages). **Never embed `Visitor.id` in a public URL.**
- `VisitorStatus` is the state machine: `EXPECTED → ARRIVED → CHECKED_OUT`, plus the `AWAITING_APPROVAL` branch for live and pre-approval flows, and the terminal failures `REJECTED` / `CANCELLED` / `EXPIRED`.
- Frequent visitors (`isFrequent=true`) are excluded from auto-checkout, auto-cancel, and approval expiry, and a scan on an `ARRIVED` frequent visitor re-arrives them instead of returning "already arrived". When adding a new sweep or scanner branch, explicitly decide whether frequent visitors are included.
- There is no event/occurrence concept in this product. There is no `Attendee` model. Recurring scheduling is not modelled at the schema level — do not invent it.

### Approver / Admin merge

The legacy `Approver` model is being merged into `Admin` (with `Admin.isApprover=true`). New writes for "host who approves a visit" go to `Visitor.assignedAdminId`. `Visitor.assignedApproverId` is kept for back-compat reads. When writing new code:
- Read both columns; prefer `assignedAdminId` when both are set.
- Write only `assignedAdminId`.

---

*This file is the naming and convention authority for Claude Code sessions on the Gate Pass codebase. When in doubt, follow this file over any legacy comment or string found in the codebase.*
