# Punch Drunk File Transfers — Application Guide

> Last updated: 2026-03-20 | Generated: 2026-04-04T11:17:04.062Z

---

## 1. App Overview

**Punch Drunk File Transfers** is a comprehensive file management system for the Punch Drunk production company. It enables public file uploads from clients (up to 50 GB), secure staff file sharing with password protection and expiration, an admin dashboard for system oversight, and optional Dropbox integration for automatic backups.

| Field | Value |
|-------|-------|
| Canonical URL | `https://files.punch-drunk.com` |
| Tech Stack | React + TypeScript (Vite) frontend, Express.js v5 backend |
| Database | PostgreSQL (Neon-backed, via Drizzle ORM) |
| Hosting | Cloudflare / Replit |
| Runtime | Node.js (tsx) |
| File Storage | Cloudflare R2 (S3-compatible) |

---

## 2. Environment Variables

| Variable | Purpose | Status | Required | Notes |
|----------|---------|--------|----------|-------|
| `SESSION_SECRET` | Express session encryption key | ✅ configured | Required (production) | Falls back to dev-only key in development |
| `PD_AUTH_API_KEY` | Punch Drunk Auth OAuth client ID | ✅ configured | Required | Used as `client_id` in OAuth flow |
| `PUNCH_DRUNK_SECRET_KEY` | Punch Drunk Auth secret key | ✅ configured | Optional | Reserved for future use |
| `DATABASE_URL` | PostgreSQL connection string | ✅ configured | Required | Managed by Replit |
| `R2_ACCOUNT_ID` | Cloudflare account ID | ✅ configured | Required | Used to build R2 endpoint URL |
| `R2_ACCESS_KEY_ID` | R2 access key ID | ✅ configured | Required | S3-compatible credential |
| `R2_SECRET_ACCESS_KEY` | R2 secret access key | ✅ configured | Required | S3-compatible credential |
| `R2_BUCKET_NAME` | R2 bucket name | ✅ configured | Required | Target bucket for all file storage |
| `SENDGRID_API_KEY` | SendGrid API key for email | ✅ configured | Required | Direct env var for email sending |
| `NODE_ENV` | Runtime environment | `production` | Optional | Defaults to development |
| `PORT` | Server port | `8080` | Optional | Defaults to 5000 |

**SendGrid** is configured via the `SENDGRID_API_KEY` environment variable.

**Dropbox** credentials (App Key, App Secret, access/refresh tokens) are stored in the `settings` database table, not as environment variables. They are configured via the admin Settings page.

---

## 3. Authentication Flow

This app uses **PD Auth (auth.punch-drunk.com)** with a **token exchange pattern** for mobile browser compatibility.

### Step-by-step flow:

1. **User clicks "Sign In"** → Frontend navigates to `/api/auth/login`

2. **`GET /api/auth/login`** (public)
   - Builds a redirect URI from the current host
   - Redirects to `https://auth.punch-drunk.com/authorize?client_id=...&redirect_uri=...`

3. **PD Auth authenticates the user** → Redirects back to `/auth/callback?token=...`

4. **`GET /auth/callback`** (public)
   - Receives the `token` query parameter from PD Auth
   - Validates the token by POSTing to `https://auth.punch-drunk.com/api/apps/validate-token` with the token and `PD_AUTH_API_KEY`
   - Looks up the user by email in the local database — if user doesn't exist, redirects with `?error=no_access`
   - Updates display name and PD Auth ID if changed
   - Generates a random one-time `auth_code` (32 bytes hex, 60-second TTL)
   - Stores user data in the `pending_auth_codes` PostgreSQL table (keyed by auth code, with expiry timestamp) — DB-backed so it works across multiple container instances
   - **Redirects to `/admin?auth_code=<code>`** — no cookies are set on this redirect

5. **Frontend detects `auth_code` in URL** (client-side)
   - Sends `POST /api/auth/exchange` with `{ code }` in the body

6. **`POST /api/auth/exchange`** (public, rate-limited)
   - Looks up the auth code in the `pending_auth_codes` DB table (checking expiry), deletes it atomically (one-time use)
   - Creates the session: sets `userId`, `userRole`, `userEmail`, `userName` on `req.session`
   - Calls `req.session.save()` explicitly
   - Returns 200 with user data — **Set-Cookie header works reliably on standard responses**

7. **`GET /api/auth/me`** (public, returns 401 if not authenticated)
   - Returns current user info from session
   - Sets no-cache headers

8. **`POST /api/auth/logout`** (public)
   - Destroys the session
   - Returns a redirect URL to PD Auth's logout endpoint

### Why the token exchange pattern?

Mobile browsers (especially Chrome on iOS/Android) silently discard `Set-Cookie` headers on `302` redirect responses. The standard OAuth pattern (callback sets cookie + redirects) fails on mobile. By separating session creation into a normal `POST` request, cookies are set on a `200` response, which works universally.

### Deviations from PD Auth Integration Guide

The standard integration guide is at `https://auth.punch-drunk.com/api/integration-guide`. This app deviates in the following ways:

| # | Standard Guide | This App | Reason |
|---|---------------|----------|--------|
| 1 | **Session-on-redirect**: Callback calls `req.session.regenerate()`, sets session fields, calls `req.session.save()`, then `res.redirect()` — session cookie is set on the 302 response | **Token exchange pattern**: Callback generates a one-time auth code, redirects to frontend with `?auth_code=...` (no cookie), then frontend POSTs to `/api/auth/exchange` which creates the session on a normal 200 response | Mobile browsers (Chrome on iOS/Android) silently discard `Set-Cookie` on redirect responses, causing login to fail silently on mobile devices |
| 2 | Uses `req.session.regenerate()` before setting session data (prevents session fixation) | **Does not call `session.regenerate()`** | The token exchange already creates a fresh session context via the POST request; the auth code is one-time-use with a 60-second TTL, so session fixation risk is minimal |
| 3 | Uses `PUNCH_DRUNK_API_KEY` env var | Uses **`PD_AUTH_API_KEY`** env var | The original `PUNCH_DRUNK_API_KEY` secret became stuck in Replit's secret store (couldn't be updated), so a new secret was created under a different name |
| 4 | Uses `PUNCH_DRUNK_REDIRECT_URI` env var for the callback URL | **Builds redirect URI dynamically** from `req.protocol` and `req.get("host")` | Avoids needing to configure a separate env var; works automatically across dev and production domains |
| 5 | Auto-creates/upserts local user on first login | **Requires users to be pre-created by an admin**; returns `?error=no_access` if the user's email is not found in the local database | Access control: only pre-approved staff/admins can log in. There is no self-registration. |
| 6 | Logout is `GET /logout` and directly redirects to PD Auth's logout URL | Logout is **`POST /api/auth/logout`**, returns JSON with `{ redirectUrl }` — frontend handles the redirect | SPA pattern: frontend needs to clear local state before navigating away |
| 7 | Cookie `maxAge`: 24 hours | Cookie `maxAge`: **30 days** | Longer sessions for staff convenience (reduces re-login frequency) |
| 8 | Cookie name: default (`connect.sid`) | Cookie name: **`sessionId`** | Explicit naming to avoid conflicts if multiple PD apps share a domain |
| 9 | Session table: uses `createTableIfMissing: true` in connect-pg-simple | Session table: **created manually** via `CREATE TABLE IF NOT EXISTS` SQL before session middleware initialization | Ensures the table and index exist before the session store is instantiated |

---

## 4. Session Configuration

| Setting | Value |
|---------|-------|
| Store type | PostgreSQL via `connect-pg-simple` |
| Session table | `"session"` |
| Table creation | Manual `CREATE TABLE IF NOT EXISTS` (not `createTableIfMissing`) |
| `resave` | `false` |
| `saveUninitialized` | `false` |
| Cookie name | `"sessionId"` |
| Cookie `secure` | `true` (true in production) |
| Cookie `httpOnly` | `true` |
| Cookie `sameSite` | `"lax"` |
| Cookie `path` | `"/"` |
| Cookie `maxAge` | 30 days (2,592,000,000 ms) |
| Cookie `domain` | Not set (defaults to current host) |
| `trust proxy` | `1` (trusts first proxy) |
| `proxy` (session) | `true` |

> **⚠️ CRITICAL — Unique Cookie Name Required:** PD Auth (`auth.punch-drunk.com`) sets its own `connect.sid` cookie on the parent `.punch-drunk.com` domain. Any app hosted on a subdomain of `punch-drunk.com` **MUST** use a custom session cookie name (e.g., `"sessionId"`, `"crewsheet.sid"`) to avoid collision. Using the default `"connect.sid"` will cause the app to read PD Auth's session instead of its own, resulting in authentication failures. This app uses `"sessionId"`.

### Session usage:

- `req.session.save()` is called explicitly in the `/api/auth/exchange` endpoint to ensure the session is persisted before the response is sent
- `req.session.regenerate()` is **not used** anywhere
- Session fields: `userId`, `userRole`, `userEmail`, `userName`, `dropboxOAuthState`

---

## 5. Middleware Order

Middleware is registered in this exact order across `registerRoutes()` (server/routes.ts) and the startup sequence (server/index.ts):

1. `app.set("trust proxy", 1)` — registered in `registerRoutes()`
2. `compression()` — Response compression — registered in `registerRoutes()`
3. Security headers (custom middleware) — registered in `registerRoutes()`:
   - `X-Content-Type-Options: nosniff`
   - `X-Frame-Options: DENY`
   - `X-XSS-Protection: 1; mode=block`
   - `Referrer-Policy: strict-origin-when-cross-origin`
4. `express-session` with PostgreSQL store — registered in `registerRoutes()`
5. R2 routes (`registerR2Routes`) — registered in `registerRoutes()`
6. Application routes (auth, uploads, shares, settings, themes, emails, etc.) — registered in `registerRoutes()`
7. Error handler middleware — registered in `server/index.ts` after `registerRoutes()` returns
8. Vite dev middleware + catch-all HTML handler (development) OR static file serving (production) — registered in `server/index.ts` after error handler

**Rate limiters** are applied per-route, not globally:
- `authLimiter`: 10 requests per 15 minutes, applied to `POST /api/auth/exchange`

---

## 6. Route Protection

### Auth middleware functions:

- **`requireAuth(req, res, next)`** — Checks `req.session.userId`. Returns `401 JSON { error: "Authentication required" }` if missing.
- **`requireAdmin(req, res, next)`** — Checks `req.session.userId` (401 if missing) AND `req.session.userRole === "admin"` (403 if not admin).

### Route categories:

**Public (no auth):**
- `GET /api/auth/login`, `GET /auth/callback`, `POST /api/auth/exchange`, `GET /api/auth/me`, `POST /api/auth/logout`
- `POST /api/public-uploads` (file upload submission)
- `GET /api/public-uploads/:id/download` (public download)
- `GET /api/share/:token`, `POST /api/share/:token/download` (share access)
- `POST /api/verify-email/send`, `POST /api/verify-email/check`
- `GET /api/settings/email-verification`
- `GET /api/generate-password`
- `GET /api/theme/public-config/:page`, `GET /api/theme/media/:id/thumbnail`
- `POST /api/internal/heartbeat`
- `GET /api/app-docs` (this document)

**Authenticated (requireAuth — staff or admin):**
- `GET /api/public-uploads`, `DELETE /api/public-uploads/:id`
- `POST /api/staff-shares`, `GET /api/staff-shares`, `PUT /api/staff-shares/:id`, `DELETE /api/staff-shares/:id`, `GET /api/staff-shares/:id/download`
- `GET /api/download-logs`, `GET /api/download-logs/export`
- `GET /api/settings`
- `GET /api/theme/media/:id/url`
- `GET /api/dropbox/progress`, `GET /api/dropbox/available`, `GET /api/dropbox/files`, `GET /api/dropbox/download-url`, `POST /api/dropbox/zip-files`

**Admin only (requireAdmin):**
- `POST /api/users`, `PATCH /api/users/:id`, `DELETE /api/users/:id`
- `PUT /api/settings/:key`
- All `/api/dropbox/*` management routes (connect, disconnect, status, folders, save-credentials)
- All `/api/theme/media/*` management routes (upload, delete, reprocess)
- `GET /api/theme/settings`, `POST /api/theme/settings/pages`, `POST /api/theme/settings/transcode`
- `GET /api/email-templates/:type`, `PUT /api/email-templates/:type`, `DELETE /api/email-templates/:type`
- `POST /api/notifications/test-email`

---

## 7. User Management (Pre-Created Users via PD Auth)

This section explains the complete user management pattern in detail. Another developer (or AI agent) can use this section to replicate the exact same functionality for any app that integrates with `auth.punch-drunk.com`.

### 7.1. Concept: Why Users Must Be Pre-Created

There is **no self-registration**. An admin must manually add each user before they can log in. This gives the admin full control over who has access and what role they receive. When a person visits the app and authenticates via PD Auth OAuth, the system looks up their PD Auth email in the local database. If no matching user record exists, access is denied.

### 7.2. Database Schema

The `users` table (defined in `shared/schema.ts`):

```
Column             | Type                  | Notes
-------------------|-----------------------|----------------------------------------------
id                 | varchar (PK)          | Auto-generated UUID via gen_random_uuid()
username           | text (unique, nullable)| Legacy field, not used in PD Auth flow
passwordHash       | text (nullable)       | Legacy field, not used in PD Auth flow
email              | text (unique, NOT NULL)| The key field — must match the user's PD Auth email exactly
displayName        | text (NOT NULL)       | Initially set to email prefix; updated from PD Auth on login
role               | enum('staff','admin') | Defaults to 'staff'; determines access level
punchDrunkAuthId   | text (nullable)       | Populated automatically on first successful login from PD Auth userId
createdAt          | timestamp (NOT NULL)  | Auto-set to current time on creation
```

Role enum definition in schema: `export const userRoleEnum = pgEnum("user_role", ["staff", "admin"]);`

### 7.3. Admin Workflow (Step-by-Step)

1. Admin navigates to the **Users** page in the admin dashboard
2. Admin clicks "Add User" and enters the person's **email address** — this must be the **exact same email** the person uses on `auth.punch-drunk.com`
3. Admin selects a **role**: "staff" (default) or "admin"
4. Admin submits the form
5. The server creates a user record with: the given email, role, and a displayName derived from the email prefix (e.g. `john@punch-drunk.com` → displayName `john`)
6. The person can now log in. Until they do, their `punchDrunkAuthId` field remains null

### 7.4. API Endpoints for User Management

All user-management write endpoints require the `requireAdmin` middleware.

**`POST /api/users`** — Create a new user (admin only)
- Request body: `{ email: string, role?: "staff" | "admin" }`
- Validates: email is required, checks for duplicate email (returns 409 if exists)
- Creates user with: email, role (defaults to "staff"), displayName = email prefix
- Response: the created user object

**`GET /api/users`** — List all users (authenticated, staff or admin)
- Response: array of all user objects

**`PATCH /api/users/:id`** — Update a user (admin only)
- Request body: `{ email?: string, displayName?: string, role?: "staff" | "admin" }`
- Response: updated user object (or 404)

**`DELETE /api/users/:id`** — Delete a user (admin only)
- Response: `{ success: true }`

### 7.5. Login Flow: How Email Matching Works

When a user clicks "Log in" and authenticates through PD Auth OAuth:

1. PD Auth redirects back to `GET /api/auth/callback?token=<token>`
2. The callback validates the token with PD Auth's `/api/validate-token` endpoint, which returns a user object containing `email`, `userId`, `displayName`, etc.
3. The server calls `getUserByEmail(pdUser.email.toLowerCase())` to look up the email in the local `users` table
4. **If no match is found** → the user is redirected to `/?error=no_access`. They cannot log in until an admin adds their email.
5. **If a match is found** → the server checks for first-login updates:
   - If the local user has no `punchDrunkAuthId` yet, it stores `pdUser.userId` (links the local record to the PD Auth account permanently)
   - If the PD Auth display name differs from the local one, it updates the local `displayName` to match PD Auth
6. A one-time auth code is generated and the user is redirected to `/admin?auth_code=<code>`
7. The frontend detects the auth code in the URL and exchanges it via `POST /api/auth/exchange`, which creates the session

### 7.6. Key Implementation Details

- **Email is the matching key**: The email the admin enters when creating the user must exactly match (case-insensitive) the email on `auth.punch-drunk.com`. The server lowercases the PD Auth email before lookup.
- **punchDrunkAuthId auto-links**: On the user's first successful login, the system stores the PD Auth `userId` on their local record. This field is informational — email remains the lookup key.
- **displayName syncs from PD Auth**: If the user changes their name on PD Auth, it auto-updates locally on their next login.
- **Duplicate prevention**: `POST /api/users` checks `getUserByEmail()` before creating. The `email` column also has a database-level UNIQUE constraint.
- **No password needed**: The local user record has no password. All authentication goes through PD Auth OAuth. The `passwordHash` and `username` columns exist for legacy reasons and are unused.

### 7.7. Replicating This Pattern in Another App

To implement the same pre-created-user pattern in a new app using PD Auth:

1. **Create a `users` table** with at minimum: `id` (PK), `email` (unique, not null), `displayName`, `role` (enum), `punchDrunkAuthId` (nullable text)
2. **Build an admin UI** that lets an admin add users by email and assign roles
3. **In your OAuth callback**, after validating the token with PD Auth:
   - Look up the user by `pdUser.email.toLowerCase()`
   - If not found, deny access (redirect with error)
   - If found, store `pdUser.userId` in `punchDrunkAuthId` (first login only), sync `displayName`, and create the session
4. **Protect the user-creation endpoint** with admin-only middleware
5. **Check for duplicate emails** before creating (both application-level and DB unique constraint)
6. **Use the token exchange pattern** (see Section 3) if mobile browser support is needed

Essential code references from this app:
- Schema: `shared/schema.ts` — `users` table definition
- Storage: `server/storage.ts` — `createUser()`, `getUserByEmail()`, `updateUser()`, `deleteUser()`
- API routes: `server/routes.ts` — `POST /api/users` (creation), `GET /api/auth/callback` (email lookup + auto-link)
- Frontend: `client/src/pages/admin/users.tsx` — admin user management UI

---

## 8. Staff Share Password Protection

This section documents the complete password protection system for staff-created file shares. It covers storage, generation, verification, and dashboard display — enough detail for another developer or AI agent to replicate the feature.

### 8.1. Concept & Schema

Passwords are stored in **two columns** on the `staff_shares` table (defined in `shared/schema.ts`):

```
Column        | Type          | Purpose
--------------|---------------|--------------------------------------------------
password      | text (nullable)| Plaintext password — stored so staff/admins can view and share it from the dashboard
passwordHash  | text (nullable)| bcrypt hash — used for secure verification when a download recipient enters the password
```

**Why both?** The hash is used for security during download verification (the plaintext is never sent to the public download page). The plaintext is kept so that staff members can retrieve the password from the admin dashboard and share it with the intended recipient (e.g., via email or message). Both columns are nullable — if both are null, the share has no password protection.

### 8.2. Password Generation / "Suggest" Feature

**Backend endpoint**: `GET /api/generate-password` (public, no authentication required)

How it works:
1. A hardcoded list of **80 simple English words** is defined inline (e.g., "apple", "brave", "cloud", "dance", "eagle", "flame", etc.)
2. `crypto.randomBytes(4)` generates 4 random bytes
3. Each byte selects a word from the list via modulo: `words[bytes[i] % words.length]`
4. The 4 words are concatenated **with no separator** to form the password (e.g., `"applecloudgracehappy"`)
5. Returns: `{ "password": "applecloudgracehappy" }`

**Frontend integration** (in `client/src/pages/admin/staff-shares.tsx`):
- A **"Suggest"** button (with a Key icon from lucide-react) sits next to the password input field in the "Create New Share" form
- Clicking it calls `fetch("/api/generate-password")`, then populates the form field with `form.setValue("password", data.password)`
- A **"Copy"** button appears next to the password field once a value is present, using `navigator.clipboard.writeText()` to copy to clipboard

### 8.3. Creating a Share with a Password

**Endpoint**: `POST /api/staff-shares` (requires authentication)

When the request body includes a `password` field:
1. The server hashes it: `const passwordHash = await bcrypt.hash(password, 10)` (10 salt rounds)
2. Both the plaintext `password` and `passwordHash` are saved to the database record
3. If no password is provided, both columns remain null and the share is publicly accessible (no password prompt)

### 8.4. Viewing Passwords in the Dashboard

The admin/staff shares table (`client/src/pages/admin/staff-shares.tsx`) displays password information:

- **Table indicator**: A lock icon is shown in the shares table row for any share that has a password set
- **Expandable row detail**: When a staff member expands a row, the password is displayed using a `PasswordReveal` component:
  - By default, the password is shown as dots/bullets (masked)
  - An **eye icon** toggles visibility, revealing the plaintext password (read directly from the `password` column returned by the API)
  - This lets staff retrieve the password to share with the intended recipient
- **Access control**: The `GET /api/staff-shares` endpoint (which returns the plaintext password) requires authentication — only logged-in staff/admins can see it

### 8.5. Download Page — Password Verification

The public-facing download page (`client/src/pages/download.tsx`) handles password-protected shares:

**Step 1 — Metadata fetch**: The page loads share info via `GET /api/share/:token`. The response includes `hasPassword: !!share.passwordHash` — a boolean that tells the frontend whether to show a password prompt. The actual password and hash are **never** sent to this public endpoint.

**Step 2 — Password prompt**: If `hasPassword` is true, the page renders a password input field above the download button.

**Step 3 — Download request**: When the user clicks download, the frontend sends `POST /api/share/:token/download` with `{ password }` in the JSON body.

**Step 4 — Server verification**: The server:
1. Retrieves the share record by token
2. If `share.passwordHash` exists, extracts the password from the request body
3. Compares using `bcrypt.compare(password, share.passwordHash)`
4. If invalid → returns `401 { error: "Incorrect password" }`
5. If valid (or no password required) → generates a signed download URL and returns it

**Step 5 — Frontend error handling**: On 401, the frontend displays "Incorrect password" to the user.

### 8.6. Editing / Removing Passwords

**Endpoint**: `PUT /api/staff-shares/:id` (requires authentication)

- **Changing password**: If a new `password` value is provided, the server re-hashes it with `bcrypt.hash()` and updates both the `password` and `passwordHash` columns
- **Removing password**: If the password field is cleared (empty string or null), both `password` and `passwordHash` are set to null, making the share publicly accessible

### 8.7. Replicating This Pattern in Another App

To implement the same password protection system:

1. **Add two columns** to your shares/links table: `password` (text, nullable) for plaintext display and `passwordHash` (text, nullable) for bcrypt verification
2. **Install bcrypt**: `npm install bcrypt @types/bcrypt`
3. **Create a password generator endpoint** (optional): Use `crypto.randomBytes` to pick random words from a word list, return as JSON
4. **On share creation**: If a password is provided, hash it with `bcrypt.hash(password, 10)` and save both the plaintext and hash
5. **On the public download page**:
   - Fetch metadata with a `hasPassword` boolean (derived from `!!record.passwordHash`)
   - Never send the actual password or hash to the public page
   - Verify via `bcrypt.compare()` on the server before providing the download URL
6. **In the admin dashboard**: Display the plaintext `password` column behind a reveal toggle so staff can retrieve it
7. **On edit**: Re-hash the new password or clear both columns to remove protection

Essential code references from this app:
- Schema: `shared/schema.ts` — `staffShares` table (`password`, `passwordHash` columns)
- Password generator: `server/routes.ts` — `GET /api/generate-password`
- Share creation: `server/routes.ts` — `POST /api/staff-shares` (bcrypt hash on create)
- Download verification: `server/routes.ts` — `POST /api/share/:token/download` (bcrypt compare)
- Dashboard UI: `client/src/pages/admin/staff-shares.tsx` — `PasswordReveal` component, "Suggest" button
- Download page: `client/src/pages/download.tsx` — password prompt and error handling

---

## 9. Database

| Field | Value |
|-------|-------|
| Type | PostgreSQL (Neon-backed) |
| ORM | Drizzle ORM |
| Schema file | `shared/schema.ts` |

### Tables:

| Table | Purpose |
|-------|---------|
| `users` | User accounts with roles (staff/admin), email, display name, PD Auth ID |
| `public_uploads` | Client file uploads with metadata, Dropbox sync status, content hash |
| `staff_shares` | Staff-created file shares with password, expiry, download tracking |
| `download_logs` | Download event tracking (IP, browser, OS, device type, user agent) |
| `settings` | Key-value configuration store (email, Dropbox, auto-delete, etc.) |
| `theme_media` | Uploaded background media files (images/videos) with processing status |
| `theme_page_settings` | Per-page theme configuration (mode, media, filters, overlays) |
| `session` | Express sessions (managed by `connect-pg-simple`, not Drizzle ORM) |

---

## 10. External Services

| Service | Purpose | Configuration |
|---------|---------|---------------|
| **PD Auth** (`auth.punch-drunk.com`) | OAuth authentication for admin/staff | `PD_AUTH_API_KEY` env var |
| **SendGrid** | Email notifications (upload alerts, verification, share notifications) | `SENDGRID_API_KEY` env var |
| **Dropbox API** | Optional file backup and staff file browsing | App Key & Secret stored in `settings` table, configured via admin UI |
| **Cloudflare R2** (S3-compatible) | Primary file storage for uploads, shares, and theme media | `R2_ACCOUNT_ID`, `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_BUCKET_NAME` env vars |

---

## 11. Known Issues & Workarounds

### Session Cookie Name Collision (Critical for All PD Auth Apps)
- **Problem**: PD Auth (`auth.punch-drunk.com`) sets a `connect.sid` session cookie on the parent `.punch-drunk.com` domain. If a subdomain app (e.g., `files.punch-drunk.com`, `crew.punch-drunk.com`) also uses the default `connect.sid` name, the browser sends PD Auth's cookie to the app instead of the app's own session cookie. This causes the app to try to read PD Auth's session data, which fails and results in perpetual authentication errors.
- **Workaround**: Every app on a `punch-drunk.com` subdomain must configure a **unique session cookie name** in its Express session setup. Examples: `"sessionId"`, `"crewsheet.sid"`, `"myapp.sid"`. This app uses `name: "sessionId"`.
- **Impact**: Without this fix, login will silently fail. The app will appear to authenticate but immediately lose the session. This is a **mandatory requirement** for any new PD Auth integration on a punch-drunk.com subdomain.

### Token Exchange Pattern (Mobile Auth Fix)
- **Problem**: Mobile browsers discard `Set-Cookie` on redirect responses
- **Workaround**: Auth code exchange via POST request instead of direct session-on-redirect
- **Standard approach it replaces**: Setting session cookie in the OAuth callback and redirecting
- **Impact**: Adds one extra round-trip but works universally across all browsers

### PD Auth API Key Name
- **Issue**: The env var is named `PD_AUTH_API_KEY` instead of `PUNCH_DRUNK_API_KEY`
- **Reason**: A Replit secret with the name `PUNCH_DRUNK_API_KEY` became stuck and couldn't be updated, so a new secret was created with a different name
- **Impact**: None — the app reads `PD_AUTH_API_KEY` consistently

### Dropbox Token Refresh During Long Uploads
- **Issue**: Dropbox access tokens expire during multi-hour uploads of large files
- **Workaround**: The upload pipeline detects 401/expired_access_token errors, refreshes the token via the refresh token, and retries the failed chunk automatically
- **Impact**: Long uploads complete reliably without manual intervention

### FFmpeg Concurrency Limit
- **Issue**: Processing multiple theme videos simultaneously can exhaust server memory
- **Workaround**: `MAX_CONCURRENT_JOBS = 1` limits FFmpeg to one concurrent transcoding job
- **Impact**: Theme media processing is serialized; upload queue may back up with many uploads

---

## 12. Build & Deploy

| Field | Value |
|-------|-------|
| Entry point | `server/index.ts` (run via `tsx`) |
| Dev command | `npm run dev` → `NODE_ENV=development tsx server/index.ts` |
| Production | `NODE_ENV=production`, detected via `process.env.NODE_ENV === "production"` |
| Port | `8080` (from `PORT` env var, defaults to 5000) |
| Frontend build | Vite (dev: HMR middleware, production: pre-built static files) |

### Startup tasks:

1. Database connection pool initialized (`server/db.ts`)
2. Schema sync via Drizzle (`drizzle-kit push`)
3. Session table created if not exists (`CREATE TABLE IF NOT EXISTS "session"`)
4. Auto-delete background job starts (checks every hour for expired files)
5. Auto-resume: checks for interrupted Dropbox uploads and resumes them
6. Express server binds to port

---

*This document is served from `GET /api/app-docs` and reflects live configuration. It never exposes actual secret values.*

*Last updated: 2026-03-20 | Generated: 2026-04-04T11:17:04.062Z*
