# AGENTS.md — Convex + Payload Turbo Monorepo

This is the definitive onboarding guide for AI agents (and humans) working in this
repository. Read it fully before making any changes. It covers architecture, patterns,
constraints, and known issues — everything you need to work effectively without
breaking things or introducing inconsistencies.

This file is a living document. If your work changes architecture, workflows,
deployment scripts, Payload structure, important file paths, or known issues, update
`AGENTS.md` in the same task so the next agent inherits the right context.

---

## Table of Contents

1. [What This Repo Is](#1-what-this-repo-is)
2. [Critical Rules — Read First](#2-critical-rules--read-first)
3. [Monorepo Architecture Overview](#3-monorepo-architecture-overview)
4. [Self-Hosted Stack & Runtime Requirements](#4-self-hosted-stack--runtime-requirements)
5. [Environment Variables — Complete Reference](#5-environment-variables--complete-reference)
6. [Dependency Management — The Catalog System](#6-dependency-management--the-catalog-system)
7. [Convex Backend Deep Dive](#7-convex-backend-deep-dive)
8. [Next.js App Deep Dive](#8-nextjs-app-deep-dive)
9. [UI Package (`@gib/ui`)](#9-ui-package-gibui)
10. [Tools — Shared Configuration Packages](#10-tools--shared-configuration-packages)
11. [Docker & Deployment](#11-docker--deployment)
12. [Code Style Reference](#12-code-style-reference)
13. [Adding New Features — Checklists](#13-adding-new-features--checklists)
14. [Expo App — Scaffold Only, Not Production-Ready](#14-expo-app--scaffold-only-not-production-ready)
15. [Known Issues & Technical Debt](#15-known-issues--technical-debt)
16. [Quick Command Reference](#16-quick-command-reference)

---

## 1. What This Repo Is

This is a personal full-stack monorepo template for spinning up self-hosted web
applications quickly. The core idea: clone the repo, configure the environment
variables, stand up the Convex services, connect the app to an external Postgres
database for Payload CMS, reverse-proxy everything through nginx-proxy-manager,
and you have a production-ready Next.js website with:

- **Self-hosted Convex** as the backend/database (not Convex Cloud)
- **Convex Auth** with two providers: Authentik (OAuth SSO) and a custom Password
  provider with email OTP via a self-hosted UseSend instance
- **Payload CMS** embedded inside the Next.js app for admin, REST/GraphQL APIs, and
  editable landing-page content backed by Postgres
- **Sentry** for error tracking and performance monitoring
- **Plausible** for privacy-focused analytics
- A **stubbed Expo app** that is present for future mobile work but is not the focus

The primary deliverable from this template is the **Next.js web app**. Every project
built from this template has been entirely focused on `apps/next/` and
`packages/backend/`. The Next.js app now contains both the public frontend and the
embedded Payload CMS surfaces. The Expo app exists structurally but is non-functional
and should be ignored unless explicitly asked to work on it.

When working in this repo as an AI agent, you are almost certainly working on an
existing running instance of this template — the Convex backend is already deployed,
auth is already configured, and your job is to build features on top of the
established foundation.

---

## 2. Critical Rules — Read First

These are hard constraints. Violating them will break things that are intentionally
configured and difficult to re-stabilize.

### Never do these without explicit instruction:

- **NEVER modify anything inside `packages/backend/convex/_generated/`** — these
  files are auto-generated by the Convex CLI and will be overwritten. Read them for
  type information, never edit them.

- **NEVER rename `apps/next/next.config.js` to `next.config.ts`** — the config uses
  `jiti` to import `./src/env.ts` (a TypeScript file) at build time. This pattern
  only works when the config itself is a `.js` file. Renaming it to `.ts` will break
  the build.

- **NEVER remove `withPayload(...)` from `apps/next/next.config.js`** — Payload's
  admin routes, REST API, GraphQL API, and generated integration files depend on the
  Next config being wrapped correctly.

- **NEVER rename `apps/next/src/proxy.ts` to `middleware.ts`** — `proxy.ts` is the
  correct and intentional name for the Next.js route handler file in Next.js 16+.
  This was an official rename from the Next.js team to reduce confusion. The file
  works exactly as middleware did; it's just named differently now.

- **NEVER manually edit Payload-generated files** — these are copied/generated by the
  Payload integration and may be rewritten at any time. Leave these alone unless you
  are explicitly regenerating Payload boilerplate:
  - `apps/next/payload-types.ts`
  - `apps/next/src/app/(payload)/layout.tsx`
  - `apps/next/src/app/(payload)/admin/importMap.js`
  - `apps/next/src/app/(payload)/admin/[[...segments]]/page.tsx`
  - `apps/next/src/app/(payload)/admin/[[...segments]]/not-found.tsx`
  - `apps/next/src/app/(payload)/api/[...slug]/route.ts`
  - `apps/next/src/app/(payload)/api/graphql/route.ts`
  - `apps/next/src/app/(payload)/api/graphql-playground/route.ts`

- **NEVER modify the Sentry configuration files without explicit instruction** — these
  took significant effort to get working correctly with the standalone Docker build,
  Plausible proxy, and self-hosted Sentry. The files to leave alone are:
  - `apps/next/next.config.js`
  - `apps/next/src/instrumentation.ts`
  - `apps/next/src/instrumentation-client.ts`
  - `apps/next/src/sentry.server.config.ts`

- **NEVER remove `typescript.ignoreBuildErrors: true` from `next.config.js`** — some
  shadcn/ui components produce spurious TypeScript build errors. This flag keeps the
  build working. It's intentional and should stay until explicitly removed.

- **NEVER modify `tools/tailwind/theme.css` without explicit instruction** — this file
  contains the entire OKLCH-based design system. When a theme change is needed, the
  file is replaced wholesale with a new theme generated from a shadcn theme generator.
  Don't tweak individual values without being asked.

- **NEVER touch `packages/backend/convex/http.ts`** — this file only calls
  `auth.addHttpRoutes(http)` and must stay exactly as it is for Convex Auth to work.

### Always do these:

- **ALWAYS use `bun typecheck` to validate your work**, never `bun build`. Typecheck
  is fast and sufficient. Build is slow and for production deployments only.

- **ALWAYS use `const` arrow functions over `function` declarations** — this is a
  strong style preference throughout the codebase:

  ```typescript
  // ✅ Correct
  const handleSubmit = async (data: FormData) => { ... };
  const MyComponent = () => { ... };

  // ❌ Wrong
  function handleSubmit(data: FormData) { ... }
  function MyComponent() { ... }
  ```

  Note: exported Convex functions (`query`, `mutation`, `action`) are already defined
  using the Convex builder pattern and are fine as-is.

- **ALWAYS import env vars from `@/env`**, never from `process.env` directly. The
  ESLint `restrictEnvAccess` rule will error if you use `process.env` anywhere outside
  of `apps/next/src/env.ts`.

- **ALWAYS add `.js` extensions** when importing from Convex-generated files:

  ```typescript
  // ✅ Correct
  import type { Id } from '@gib/backend/convex/_generated/dataModel.js';
  // ❌ Wrong — will fail at runtime
  import { api } from '@gib/backend/convex/_generated/api';
  import { api } from '@gib/backend/convex/_generated/api.js';
  ```

  The project uses `"type": "module"` in all `package.json` files, which requires
  explicit file extensions for ESM imports. The `.js` extension is required even
  though the source files are `.ts`.

- **ALWAYS put user-authored Next.js routes in `apps/next/src/app/(frontend)/`** —
  the `(payload)` route group is the embedded Payload integration layer. Treat it as
  copied/generated infrastructure, not as the place for custom frontend pages.

---

## 3. Monorepo Architecture Overview

This is a Turborepo monorepo with three top-level categories of packages:

```
convex-monorepo/
├── apps/
│   ├── next/           # @gib/next — Next.js 16 web application
│   └── expo/           # @gib/expo — Expo 54 mobile app (WIP, broken)
├── packages/
│   ├── backend/        # @gib/backend — Self-hosted Convex backend
│   └── ui/             # @gib/ui — Shared shadcn/ui component library
├── tools/
│   ├── eslint/         # @gib/eslint-config — ESLint configuration
│   ├── prettier/       # @gib/prettier-config — Prettier + import sorting
│   ├── tailwind/       # @gib/tailwind-config — Tailwind v4 + theme
│   └── typescript/     # @gib/tsconfig — Shared TypeScript configs
├── docker/             # Docker Compose for self-hosted deployment
├── turbo/              # Turborepo generators (scaffolding)
├── turbo.json          # Turborepo pipeline configuration
├── package.json        # Root workspace + dependency catalogs
└── .env                # Central environment variables (gitignored)
```

### How the pieces connect

The dependency graph flows like this:

```
@gib/tsconfig
  └── @gib/eslint-config
  └── @gib/tailwind-config
  └── @gib/prettier-config
        └── @gib/ui (components + shared styles)
        └── @gib/backend (Convex functions + types)
              └── @gib/next (consumes everything)
```

`tools/` packages are pure configuration — they export ESLint rules, TypeScript
compiler options, Tailwind PostCSS config, and Prettier options. Every app and package
extends them; you should rarely need to modify them.

`packages/ui` contains every shadcn/ui component plus a few custom additions.
Everything is re-exported from a single `@gib/ui` barrel — no package-specific deep
imports needed.

`packages/backend` is the Convex backend. Only the `convex/` subdirectory is synced
to the Convex deployment. The `scripts/` and `types/` directories sit outside
`convex/` intentionally so they're not uploaded to Convex cloud.

`apps/next` now contains two distinct app-router route groups:

- `src/app/(frontend)/` — the public site and authenticated app pages you actually
  build features in
- `src/app/(payload)/` — the embedded Payload admin/API surface that gets copied in
  from Payload's Next integration

### Turborepo pipeline

`turbo.json` defines the task pipeline. Key points for agents:

- **`globalEnv`** — all environment variables that affect builds must be listed here,
  or Turborepo won't include them in cache keys and cached outputs may be stale.
  Whenever you add a new env var, add it to `globalEnv`.
- **`globalDependencies`** — `**/.env.*local` is tracked, so changes to
  `.env.local` files invalidate the cache.
- The `build` task runs `^build` (dependencies first), then emits to `dist/`.
- The `dev` task is not cached and not persistent (`persistent: false` means
  Turborepo considers it non-blocking).
- `typecheck` and `lint` both depend on `^topo` and `^build`, meaning they wait for
  dependency packages to build first.

---

## 4. Self-Hosted Stack & Runtime Requirements

### Before any development work can start

The Convex backend must be running and reachable, and the Payload Postgres database
must be reachable for any Payload-backed pages or admin routes to work. `bun dev:next`
can start without one of these dependencies, but parts of the app will fail at runtime.
In practice, when you are working in this repo on an active project, the Convex
containers are already running on the server and the Payload database already exists.
If you are starting fresh from the template, see the README for the full setup
walkthrough.

The two Convex-related Docker containers are:

- **`convex-backend`** — the actual Convex runtime (default: `https://api.convexmonorepo.gbrown.org`)
- **`convex-dashboard`** — the admin UI (default: `https://dashboard.convexmonorepo.gbrown.org`)

### Payload Postgres

Payload is configured inside `apps/next/src/payload.config.ts` and uses a Postgres
connection string from `PAYLOAD_DB_URL`. Right now that database is expected to be
external to `docker/compose.yml`; the Compose file passes the env vars through to the
`next-app` container but does not provision a Postgres service for Payload.

### The `packages/backend/.env.local` file

You may notice a `.env.local` file in `packages/backend/`. This file is
auto-generated by the `convex dev` command and is not something agents need to create
or manage. It contains `CONVEX_URL` and `CONVEX_SITE_URL` values that Convex uses
internally when running `bun dev:backend`. It is gitignored and can be safely ignored.

### `CONVEX_SITE_URL` — a nuance worth understanding

`CONVEX_SITE_URL` is used by `packages/backend/convex/auth.config.ts` as the CORS
domain that Convex Auth trusts for token exchange. For **local development**, this
should always be `http://localhost:3000` (the Next.js dev server). For **production**,
this needs to match your public domain — but this value must be updated directly in
the **Convex Dashboard** (environment variables section), not just in the root `.env`.

The value in `.env` is always `http://localhost:3000` for dev. When you need Convex
Auth to work on your production domain, go to the Convex Dashboard at
`https://dashboard.convexmonorepo.gbrown.org` and update `CONVEX_SITE_URL` there.

---

## 5. Environment Variables — Complete Reference

### The single source of truth: `/.env`

All environment variables live in the root `.env` file. Every app and package loads
them via `dotenv-cli` using the `with-env` script pattern:

```json
"with-env": "dotenv -e ../../.env --"
```

The root `.env` is gitignored. The root `.env.example` is the committed template that
shows all required variable names without real values. **Keep `.env.example` up to
date whenever you add a new env var.**

The root `/.env` is also the only required Docker Compose env file. The helper scripts
in `scripts/` load this same root file before calling `docker compose`, so do not
create or rely on a separate `docker/.env`.

### Complete variable reference

| Variable                          | Used By           | Purpose                                                                  | Sync to Convex? |
| --------------------------------- | ----------------- | ------------------------------------------------------------------------ | --------------- |
| `NODE_ENV`                        | Next.js           | `development` / `production`                                             | No              |
| `SENTRY_AUTH_TOKEN`               | Next.js build     | Source map upload to Sentry                                              | No              |
| `NEXT_PUBLIC_SITE_URL`            | Next.js / Payload | Public URL of the Next.js app and Payload live-preview server URL        | No              |
| `NEXT_PUBLIC_CONVEX_URL`          | Next.js           | Convex backend API URL                                                   | No              |
| `NEXT_PUBLIC_PLAUSIBLE_URL`       | Next.js           | Self-hosted Plausible instance URL                                       | No              |
| `NEXT_PUBLIC_SENTRY_DSN`          | Next.js           | Sentry DSN for error reporting                                           | No              |
| `NEXT_PUBLIC_SENTRY_URL`          | Next.js build     | Self-hosted Sentry URL                                                   | No              |
| `NEXT_PUBLIC_SENTRY_ORG`          | Next.js build     | Sentry organization slug                                                 | No              |
| `NEXT_PUBLIC_SENTRY_PROJECT_NAME` | Next.js build     | Sentry project name                                                      | No              |
| `PAYLOAD_SECRET`                  | Payload CMS       | Payload application secret                                               | No              |
| `PAYLOAD_DB_URL`                  | Payload CMS       | Postgres connection string used by Payload                               | No              |
| `NETWORK`                         | Docker Compose    | External Docker network name                                             | No              |
| `NEXT_CONTAINER_NAME`             | Docker Compose    | Next.js container name                                                   | No              |
| `NEXT_DOMAIN`                     | Docker Compose    | Public Next.js domain used by compose interpolation                      | No              |
| `BACKEND_CONTAINER_NAME`          | Docker Compose    | Convex backend container name                                            | No              |
| `DASHBOARD_CONTAINER_NAME`        | Docker Compose    | Convex dashboard container name                                          | No              |
| `BACKEND_DOMAIN`                  | Docker Compose    | Public backend domain used in compose interpolation                      | No              |
| `DASHBOARD_DOMAIN`                | Docker Compose    | Public dashboard domain used in compose interpolation                    | No              |
| `INSTANCE_NAME`                   | Docker Compose    | Self-hosted Convex instance name                                         | No              |
| `INSTANCE_SECRET`                 | Docker Compose    | Self-hosted Convex instance secret                                       | No              |
| `CONVEX_CLOUD_ORIGIN`             | Docker Compose    | Public Convex API origin                                                 | No              |
| `CONVEX_SITE_ORIGIN`              | Docker Compose    | Public Convex site/auth origin                                           | No              |
| `NEXT_PUBLIC_DEPLOYMENT_URL`      | Docker Compose    | Deployment URL passed to Convex dashboard                                | No              |
| `POSTGRES_URL`                    | Docker Compose    | Self-hosted Convex Postgres connection string                            | No              |
| `CONVEX_SELF_HOSTED_URL`          | Convex CLI        | URL of the self-hosted Convex backend                                    | No              |
| `CONVEX_SELF_HOSTED_ADMIN_KEY`    | Convex CLI        | Admin key for the self-hosted backend                                    | No              |
| `CONVEX_SITE_URL`                 | Convex Auth       | CORS domain for token exchange (localhost:3000 for dev)                  | Yes (Dashboard) |
| `USESEND_API_KEY`                 | Convex backend    | API key for the self-hosted UseSend instance                             | Yes             |
| `USESEND_URL`                     | Convex backend    | URL of the self-hosted UseSend instance                                  | Yes             |
| `USESEND_FROM_EMAIL`              | Convex backend    | From address for transactional emails (e.g. `App <noreply@example.com>`) | Yes             |
| `AUTH_AUTHENTIK_ID`               | Convex backend    | Authentik OAuth client ID                                                | Yes             |
| `AUTH_AUTHENTIK_SECRET`           | Convex backend    | Authentik OAuth client secret                                            | Yes             |
| `AUTH_AUTHENTIK_ISSUER`           | Convex backend    | Authentik issuer URL                                                     | Yes             |

### Typesafe env vars — `apps/next/src/env.ts`

The Next.js app uses `@t3-oss/env-nextjs` with Zod to validate all environment
variables at build time. This file is the contract between the `.env` file and the
application code.

The ESLint `restrictEnvAccess` rule in `@gib/eslint-config` blocks direct
`process.env` access everywhere except in `env.ts`. All app code must import the
validated `env` object:

```typescript
import { env } from '@/env';

// ✅ Correct — validated and typed
const url = env.NEXT_PUBLIC_CONVEX_URL;

// ❌ Will trigger ESLint error
const url = process.env.NEXT_PUBLIC_CONVEX_URL;
```

### Adding a new environment variable — the 4-step checklist

Every time you introduce a new env var, all four of these steps are required:

**Step 1: Add to `/.env`**

```bash
MY_NEW_VAR=value
```

**Step 2: Add to `apps/next/src/env.ts`**

```typescript
// In the server{} block for server-only vars:
MY_NEW_VAR: z.string(),

// In the client{} block for NEXT_PUBLIC_ vars:
NEXT_PUBLIC_MY_VAR: z.url(),

// And in the runtimeEnv{} block:
MY_NEW_VAR: process.env.MY_NEW_VAR,
```

**Step 3: Add to `turbo.json` `globalEnv` array**

```json
"globalEnv": [
  ...,
  "MY_NEW_VAR"
]
```

Skipping this means Turborepo won't bust its cache when the variable changes.

**Step 4 (if the backend needs it): Sync to Convex**

```bash
# From packages/backend/
bun with-env npx convex env set MY_NEW_VAR "value"
```

Or set it directly in the Convex Dashboard.

Also update `/.env.example` with the new variable (empty value or a placeholder).

If the variable is used by Docker Compose or one of the helper scripts in `scripts/`,
it still belongs in the same root `/.env`.

---

## 6. Dependency Management — The Catalog System

### What catalogs are

The root `package.json` uses Bun workspaces with a catalog system — a single source
of truth for shared dependency versions. When a package declares `"react": "catalog:react19"`,
it means "use whatever version is defined in the `react19` catalog in the root
`package.json`". This prevents version drift across packages.

The current catalogs:

```json
"catalog": {
  "@eslint/js": "^10.0.1",
  "@tailwindcss/postcss": "^4.2.2",
  "@types/node": "^25.5.0",
  "eslint": "^10.1.0",
  "prettier": "^3.8.1",
  "tailwindcss": "^4.2.2",
  "typescript": "^6.0.2",
  "zod": "^4.3.6"
},
"catalogs": {
  "convex": {
    "@convex-dev/auth": "^0.0.87",
    "convex": "^1.34.1"
  },
  "react19": {
    "@types/react": "~19.2.14",
    "@types/react-dom": "~19.2.3",
    "react": "19.2.4",
    "react-dom": "19.2.4"
  }
}
```

Usage in package `package.json` files:

```json
"dependencies": {
  "convex": "catalog:convex",
  "react": "catalog:react19",
  "typescript": "catalog:",
  "zod": "catalog:"
},
"devDependencies": {
  "@gib/eslint-config": "workspace:*",
  "@gib/ui": "workspace:*"
}
```

The `catalog:` syntax (no name) refers to the default catalog. `catalog:convex` and
`catalog:react19` refer to named catalogs.

### The update regression — how to safely update dependencies

**Do NOT run `bun update` directly inside individual package directories.** This is a
known issue: running `bun update` in a package directory will replace `catalog:`
references with explicit hardcoded version strings, breaking the single-source-of-truth
system.

The correct dependency update workflow:

1. Edit the version in the **root `package.json`** catalog section
2. Run `bun install` from the root
3. Verify with `bun lint:ws` (which runs `sherif` to check workspace consistency)

### sherif runs on `postinstall`

The root `package.json` has `"postinstall": "sherif"`. This means every time you run
`bun install`, `sherif` runs automatically and will **fail the install** if any
`catalog:` references are broken or if package versions are inconsistent. This is a
feature, not a bug — it catches the regression described above immediately.

If you see `sherif` errors after running `bun install`, it usually means a
`catalog:` reference was accidentally replaced with a hardcoded version. Fix it by
restoring the `catalog:` syntax in the affected `package.json`.

### Adding new packages to the monorepo

Use the Turborepo generator to scaffold a new package:

```bash
bun turbo gen init
```

This will prompt you for:

1. **Package name** — enter with or without the `@gib/` prefix (it's stripped automatically)
2. **Dependencies** — space-separated list of npm packages to install

The generator creates:

- `packages/<name>/eslint.config.ts`
- `packages/<name>/package.json` (with workspace devDeps and latest versions for any
  specified deps)
- `packages/<name>/tsconfig.json`
- `packages/<name>/src/index.ts`

After scaffolding it runs `bun install` and formats the new files.

---

## 7. Convex Backend Deep Dive

### Package structure

```
packages/backend/
├── convex/                    # ← Only this directory is synced to Convex
│   ├── _generated/            # Auto-generated — NEVER edit
│   ├── custom/
│   │   └── auth/
│   │       ├── index.ts       # Barrel: Password + validatePassword exports
│   │       └── providers/
│   │           ├── password.ts    # Custom Password provider
│   │           └── usesend.ts     # UseSend email OTP provider
│   ├── auth.config.ts         # CORS domain configuration
│   ├── auth.ts                # Auth setup + all user-related functions
│   ├── crons.ts               # Cron jobs (commented examples)
│   ├── files.ts               # File upload/storage
│   ├── http.ts                # HTTP routes (auth only) — DO NOT TOUCH
│   ├── schema.ts              # Database schema — the source of truth
│   └── tsconfig.json          # Convex-required TS config
├── scripts/
│   └── generateKeys.mjs       # RS256 JWT keypair generator for Convex Auth
└── types/
    ├── auth.ts                # Password validation constants
    └── index.ts               # Barrel export
```

**Important:** The backend package has no root `tsconfig.json` — only
`convex/tsconfig.json`. This is intentional and follows Convex's recommended
structure. When `bun typecheck` runs from the repo root, it will print TypeScript help
output for `@gib/backend` (because there's no valid project to check). This is
expected and not an error.

### Schema design — always extend `users` directly

The preferred and enforced approach in this repo is to **extend the `users` table
directly** for all application data. Do not create a separate `profiles` table.

The Convex Auth library (`@convex-dev/auth`) provides base fields via `authTables`
(name, image, email, emailVerificationTime, phone, phoneVerificationTime, isAnonymous).
Custom fields are added directly to the `users` table definition in `schema.ts`:

```typescript
// packages/backend/convex/schema.ts
const applicationTables = {
  users: defineTable({
    name: v.optional(v.string()),
    image: v.optional(v.string()), // stores Convex storage ID as string
    email: v.optional(v.string()),
    emailVerificationTime: v.optional(v.number()),
    phone: v.optional(v.string()),
    phoneVerificationTime: v.optional(v.number()),
    isAnonymous: v.optional(v.boolean()),
    /* Custom fields below */
    themePreference: v.optional(
      v.union(v.literal('light'), v.literal('dark'), v.literal('system')),
    ),
  })
    .index('email', ['email'])
    .index('phone', ['phone'])
    .index('name', ['name']),
};

export default defineSchema({
  ...authTables,
  ...applicationTables,
});
```

The `themePreference` field demonstrates the pattern. When your app needs to store
user-specific data (preferences, profile info, settings), add a field here.

**Note on `image`:** The `image` field is typed as `v.string()` because `authTables`
defines it that way. In practice it stores a Convex storage ID (which is a string at
runtime). The `updateUser` mutation accepts `v.id('_storage')` and casts it when
writing. This is a known, harmless type mismatch — leave it as `v.string()` in the
schema for now.

### Writing backend functions

The three Convex function types:

- **`query`** — read-only, reactive, called from client with `useQuery` or server with
  `preloadQuery`. Always check auth if the data should be protected.
- **`mutation`** — write operations (database inserts, patches, deletes). Runs
  transactionally. Cannot make external HTTP calls.
- **`action`** — for side effects: calling external APIs, Convex storage operations,
  or certain `@convex-dev/auth` operations. Can call `ctx.runQuery` and
  `ctx.runMutation` internally.

Always authenticate inside protected functions:

```typescript
import { getAuthUserId } from '@convex-dev/auth/server';
import { ConvexError, v } from 'convex/values';

import { mutation, query } from './_generated/server';

export const getMyData = query({
  args: {},
  handler: async (ctx) => {
    const userId = await getAuthUserId(ctx);
    if (!userId) return null; // or throw if auth is truly required
    return ctx.db.get(userId);
  },
});

export const updateMyData = mutation({
  args: { value: v.string() },
  handler: async (ctx, args) => {
    const userId = await getAuthUserId(ctx);
    if (!userId) throw new ConvexError('Not authenticated.');
    await ctx.db.patch(userId, { someField: args.value });
  },
});
```

### Why `updateUserPassword` is an `action`, not a `mutation`

`@convex-dev/auth`'s `modifyAccountCredentials` function makes internal HTTP calls
under the hood. Convex mutations cannot make external/internal HTTP calls — only
actions can. That's why any password change operation must be an `action`:

```typescript
export const updateUserPassword = action({
  args: { currentPassword: v.string(), newPassword: v.string() },
  handler: async (ctx, { currentPassword, newPassword }) => {
    const userId = await getAuthUserId(ctx);
    if (!userId) throw new ConvexError('Not authenticated.');
    // retrieveAccount and modifyAccountCredentials both require action context
    const verified = await retrieveAccount(ctx, {
      provider: 'password',
      account: { id: user.email, secret: currentPassword },
    });
    if (!verified) throw new ConvexError('Current password is incorrect.');
    await modifyAccountCredentials(ctx, {
      provider: 'password',
      account: { id: user.email, secret: newPassword },
    });
  },
});
```

Similarly, any auth function that needs to call into Convex Auth internals
(`retrieveAccount`, `modifyAccountCredentials`, `createAccount`, etc.) must be an
action.

### Authentication setup

```typescript
// convex/auth.ts
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
  providers: [Authentik({ allowDangerousEmailAccountLinking: true }), Password],
});
```

Two providers are configured:

1. **Authentik** — OAuth SSO. The `allowDangerousEmailAccountLinking: true` flag
   allows users who previously registered with the Password provider to link their
   Authentik account if it shares the same email address.

2. **Password** — custom provider in `convex/custom/auth/providers/password.ts`.
   Uses `UseSendOTP` for email verification during sign-up and `UseSendOTPPasswordReset`
   for password reset flows.

### The UseSend email provider

`convex/custom/auth/providers/usesend.ts` is the custom email OTP provider. It reads
configuration from environment variables:

- `USESEND_API_KEY` — the API key for your UseSend instance
- `USESEND_URL` — the base URL of your self-hosted UseSend instance
- `USESEND_FROM_EMAIL` — the from address for emails (e.g. `My App <noreply@example.com>`)

The provider creates two exports used in `password.ts`:

- `UseSendOTP` — 20-minute OTP for sign-up email verification
- `UseSendOTPPasswordReset` — 1-hour OTP for password reset

Email templates are currently inline HTML strings in `usesend.ts`. The
`@react-email/components` and `react-email` packages are in `package.json` as planned
dependencies — React Email templates will eventually replace the inline HTML.

### File uploads

`convex/files.ts` provides two functions:

```typescript
// Get a signed upload URL (call this first)
const uploadUrl = await generateUploadUrl();

// POST your file to the URL, get back a storage ID
// Then store the ID:
await updateUser({ image: storageId });

// Get a temporary signed URL to display the image:
const imageUrl = await getImageUrl({ storageId });
```

The profile avatar upload in `apps/next/src/components/layout/auth/profile/avatar-upload.tsx`
demonstrates the complete flow: crop the image client-side → get an upload URL →
POST to Convex storage → save the storage ID to the user record.

### Cron jobs

`convex/crons.ts` is the place for scheduled jobs. The file currently contains
commented-out examples. When you need to add a cron job, add it to this file following
the pattern in the comments.

### Generating JWT keys for Convex Auth

When setting up a new deployment, you need to generate RS256 JWT keys for Convex Auth
to work:

```bash
# From packages/backend/
bun run scripts/generateKeys.mjs
```

This outputs `JWT_PRIVATE_KEY` and `JWKS` values that must be synced to Convex:

```bash
bun with-env npx convex env set JWT_PRIVATE_KEY "..."
bun with-env npx convex env set JWKS "..."
```

---

## 8. Next.js App Deep Dive

### Files you should not casually modify

| File / Area                              | Why                                                                     |
| ---------------------------------------- | ----------------------------------------------------------------------- |
| `next.config.js`                         | Sentry, Plausible, Payload, standalone build — carefully configured     |
| `src/proxy.ts`                           | Auth routing + IP banning — the project's middleware                    |
| `src/env.ts`                             | The env contract — add to it, don't restructure it                      |
| `src/instrumentation.ts`                 | Sentry server setup — don't touch                                       |
| `src/instrumentation-client.ts`          | Sentry client setup — don't touch                                       |
| `src/sentry.server.config.ts`            | Sentry server config — don't touch                                      |
| `src/app/(frontend)/styles.css`          | Tailwind setup + theme import — don't touch                             |
| `payload-types.ts`                       | Auto-generated by Payload                                               |
| `src/app/(payload)/**` generated entries | Payload admin/API/layout files are copied/generated and may be replaced |

### Route structure

The app uses the Next.js App Router, but it is now split into two top-level route
groups:

- **`(frontend)`** — all user-authored public pages and authenticated app pages
- **`(payload)`** — the embedded Payload CMS admin/API surface copied from Payload

All new product/frontend routes should go in `(frontend)`, not `(payload)`.

| Route                     | File                                             | Rendering        | Purpose                                  |
| ------------------------- | ------------------------------------------------ | ---------------- | ---------------------------------------- |
| `/`                       | `app/(frontend)/page.tsx`                        | Server Component | Landing page backed by a Payload global  |
| `/sign-in`                | `app/(frontend)/(auth)/sign-in/page.tsx`         | Client Component | Sign in, sign up, email OTP — all in one |
| `/profile`                | `app/(frontend)/(auth)/profile/page.tsx`         | Server Component | User profile management                  |
| `/forgot-password`        | `app/(frontend)/(auth)/forgot-password/page.tsx` | Client Component | Password reset flow                      |
| `/admin`                  | `app/(payload)/admin/[[...segments]]/page.tsx`   | Server Component | Payload admin panel                      |
| `/api/*`                  | `app/(payload)/api/[...slug]/route.ts`           | Route Handlers   | Payload REST API                         |
| `/api/graphql`            | `app/(payload)/api/graphql/route.ts`             | Route Handler    | Payload GraphQL API                      |
| `/api/graphql-playground` | `app/(payload)/api/graphql-playground/route.ts`  | Route Handler    | Payload GraphQL playground               |

The `/profile` and `/admin` routes are protected — `src/proxy.ts` redirects
unauthenticated users to `/sign-in`. The `/sign-in` route redirects
already-authenticated users to `/`.

To protect a new route, add its pattern to `isProtectedRoute` in `src/proxy.ts`:

```typescript
const isProtectedRoute = createRouteMatcher([
  '/profile',
  '/admin',
  '/dashboard',
]);
```

### Payload CMS integration

Payload is configured inside the Next app rather than as a separate service. The key
pieces are:

- **`apps/next/src/payload.config.ts`** — the source of truth for Payload config,
  collections, globals, editor, secret, and Postgres adapter
- **`apps/next/tsconfig.json`** — defines the `@payload-config` alias used by the
  generated admin/API files
- **`apps/next/src/payload/collections/`** and `apps/next/src/payload/globals/` — the
  editable Payload schema files you should actually work in
- **`apps/next/src/payload/globals/landing-page-blocks.ts`** — the current block schema
  definitions for the landing-page global
- **`apps/next/src/lib/payload/`** — server helpers for obtaining the cached Payload
  client and fetching CMS content
- **`apps/next/src/components/payload/refresh-route-on-save.tsx`** — enables live
  preview refreshes for the landing page when `?preview=true` is present

Current state of the template:

- Payload currently powers the homepage via the `landing-page` global, not a `pages`
  collection
- the editable block definitions live in
  `apps/next/src/payload/globals/landing-page-blocks.ts`
- the frontend reads the saved global through
  `apps/next/src/lib/payload/get-landing-page-content.ts`
- saved Payload content is merged with resilient defaults from
  `apps/next/src/components/landing/content.ts`, so the page still renders even when
  content is partial or missing fields
- live preview works by loading `/` with `?preview=true` and rendering
  `RefreshRouteOnSave`

If you need a deeper walkthrough, read `docs/payload-cms.md`. That file should stay in
sync with the actual Payload implementation in this repo.

The current homepage is backed by the `landing-page` Payload global. The server page at
`apps/next/src/app/(frontend)/page.tsx` calls
`apps/next/src/lib/payload/get-landing-page-content.ts`, which merges saved Payload
content with defaults from `apps/next/src/components/landing/content.ts`.

When adding more Payload-managed marketing content in this template, there are two
reasonable paths:

1. add another Payload global plus a matching frontend route if the page is singular
   and custom
2. introduce a `pages` collection once you need many reusable CMS-managed pages with
   shared routing patterns

### The dual Convex provider setup

Two providers are required and both must be present:

**1. `ConvexAuthNextjsServerProvider`** — wraps the root `<html>` element in
`app/(frontend)/layout.tsx`. This is the server-side half that handles cookie reading and
initial token passing. It has no visible UI.

**2. `ConvexClientProvider`** — a `'use client'` component in
`components/providers/ConvexClientProvider.tsx`. This wraps `ConvexAuthNextjsProvider`
and initializes the `ConvexReactClient`. All client-side reactive queries and
mutations flow through this.

```tsx
// app/(frontend)/layout.tsx (server)
<ConvexAuthNextjsServerProvider>
  <html lang='en'>
    <body>
      <ConvexClientProvider>{children}</ConvexClientProvider>
    </body>
  </html>
</ConvexAuthNextjsServerProvider>
```

Removing either provider breaks auth. The server provider is needed for SSR token
hydration; the client provider is needed for reactive updates.

Payload uses a separate generated layout at `apps/next/src/app/(payload)/layout.tsx`.
Do not try to merge the frontend and Payload layouts into one file.

### SSR data preloading — the preferred pattern for new pages

For any page that needs Convex data, use `preloadQuery` on the server and
`usePreloadedQuery` on the client. This avoids a loading flash and gives you
SSR-rendered content.

**Server component (the page file):**

```tsx
// app/(frontend)/(auth)/some-page/page.tsx
import { SomeComponent } from '@/components/some-component';
import { preloadQuery } from 'convex/nextjs';

import { api } from '@gib/backend/convex/_generated/api.js';

const SomePage = async () => {
  const preloadedData = await preloadQuery(api.someModule.someQuery);

  return <SomeComponent preloadedData={preloadedData} />;
};

export default SomePage;
```

**Client component (the interactive part):**

```tsx
// components/some-component.tsx
'use client';

import type { Preloaded } from 'convex/react';
import { usePreloadedQuery } from 'convex/react';

import type { api } from '@gib/backend/convex/_generated/api.js';

interface Props {
  preloadedData: Preloaded<typeof api.someModule.someQuery>;
}

export const SomeComponent = ({ preloadedData }: Props) => {
  const data = usePreloadedQuery(preloadedData);
  // data is immediately available with SSR value, then stays reactive
  return <div>{data.name}</div>;
};
```

This is the pattern used on the `/profile` page and should be used for all new
data-fetching pages.

### Auth-conditional UI — always use a loading skeleton

Any component that renders differently based on whether the user is authenticated
**must** show a loading skeleton while auth resolves. Never render a "signed in" state
optimistically or flash from "not logged in" to "logged in".

The `AvatarDropdown` component in
`src/components/layout/header/controls/AvatarDropdown.tsx` is the canonical example:

```tsx
'use client';

import { useAuthActions } from '@convex-dev/auth/react';
import { useConvexAuth } from 'convex/react';

export const AvatarDropdown = () => {
  const { isLoading, isAuthenticated } = useConvexAuth();

  if (isLoading) {
    // Always show a skeleton while auth state is resolving
    return <Skeleton className='h-9 w-9 rounded-full' />;
  }

  if (!isAuthenticated) {
    return <SignInButton />;
  }

  return <UserAvatarWithDropdown />;
};
```

### `src/proxy.ts` — route protection and IP banning

This file is the Next.js proxy/middleware. It does two things in sequence:

1. **IP banning** — `banSuspiciousIPs(request)` runs first. It checks the incoming
   request against known malicious patterns (path traversal, PHP probes, etc.) and
   rate-limits suspicious IPs. If a ban is triggered, a 403 is returned immediately
   before auth runs.

2. **Auth routing** — `convexAuthNextjsMiddleware` handles authentication:
   - Unauthenticated users hitting `/profile` are redirected to `/sign-in`
   - Unauthenticated users hitting `/admin` are redirected to `/sign-in`
   - Authenticated users hitting `/sign-in` are redirected to `/`

Session cookies are configured with a 30-day max age.

### The IP banning system (`src/lib/proxy/ban-sus-ips.ts`)

This is an in-memory security layer. Key behaviors:

- Tracks request counts per IP using an in-memory Map
- 10 requests matching malicious patterns within 1 minute → 30-minute ban
- 10 404 responses within 2 minutes → ban
- Detects and bans suspicious HTTP methods (TRACE, PUT, DELETE, PATCH)
- The ban response message is deliberately direct — consider softening it for
  professional/client-facing deployments
- **Bans reset on server restart** (in-memory). This is a known limitation; a future
  enhancement would persist bans to Convex or Redis.

Agents can extend the suspicious patterns by adding to the arrays at the top of the
file if new attack vectors are observed.

IP detection order: `x-forwarded-for` → `x-real-ip` → `cf-connecting-ip` → host.
This handles the nginx-proxy-manager reverse proxy setup correctly.

### `next.config.js` — key behaviors

- **`output: 'standalone'`** — generates a self-contained build for Docker deployment
- **`typescript.ignoreBuildErrors: true`** — suppresses TypeScript errors during build
  (shadcn components can produce spurious errors)
- **`serverExternalPackages: ['require-in-the-middle']`** — Sentry compatibility
- **`transpilePackages: ['@gib/backend', '@gib/ui']`** — workspace packages need
  transpilation since Next.js doesn't transpile `node_modules` by default
- **`withPayload(config)`** — required for embedded Payload admin/API support
- **`images.remotePatterns`** — currently only allows `*.gbrown.org`. If you're
  deploying for a different domain, update this or profile pictures won't load.
- **`serverActions.bodySizeLimit: '10mb'`** — allows large file uploads for avatar
- The config is wrapped with `withPlausibleProxy`, then `withPayload`, then
  `withSentryConfig`

### Fonts

Fonts are configured in `apps/next/src/app/(frontend)/layout.tsx` using `next/font`.
The current typeface system:

- **Geist** — primary sans font for the frontend layout
- **Geist Mono** — monospace font for the frontend layout
- **Kanit** — display font loaded locally in `src/components/layout/header/index.tsx`
  for the brand wordmark

To change the frontend base fonts, update the `next/font` imports in
`apps/next/src/app/(frontend)/layout.tsx`. If you also want the brand wordmark to
change, update `apps/next/src/components/layout/header/index.tsx`.

### Analytics and monitoring

**Plausible** is injected via `next-plausible` and proxied through the Next.js app
itself via `withPlausibleProxy` in `next.config.js`. This means analytics requests go
to `/` (the Next.js server) first, which then forwards them to the Plausible instance.
Ad blockers that block `plausible.io` directly cannot block this setup.

**Sentry** is configured with `tracesSampleRate: 1.0` (100% trace sampling) on both
client and server. This is appropriate for development and low-traffic deployments. For
high-traffic production sites, reduce this to `0.1`–`0.2` in
`instrumentation-client.ts` and `sentry.server.config.ts`.

---

## 9. UI Package (`@gib/ui`)

### The single-import rule

**Everything** from the UI package is importable from the single `@gib/ui` entry
point. Never import from specific component file paths:

```typescript
// ✅ Correct — single import from @gib/ui
import { Button, Card, CardContent, cn, useIsMobile } from '@gib/ui';
// ❌ Wrong — never import from subpaths like this
import { Button } from '@gib/ui/src/button';
```

This includes utility functions (`cn`, `ccn`), all components, and all hooks.

### Available components

The UI package contains the complete shadcn/ui component set (new-york style) plus
custom additions. Key custom components:

- **`BasedAvatar`** — shows user initials or a user icon when no image is set; use
  this instead of the base `Avatar` for user avatars
- **`BasedProgress`** — progress bar variant
- **`ImageCrop`** — image cropping UI built on `react-image-crop`; used in the profile
  avatar upload flow
- **`SubmitButton`** — button with built-in loading state for form submissions
- **`StatusMessage`** — standardized success/error message display
- **`Spinner`** — loading spinner

Full component list: Accordion, Alert, AlertDialog, AspectRatio, Avatar, Badge,
BasedAvatar, BasedProgress, Breadcrumb, Button, ButtonGroup, Calendar, Card, Carousel,
Chart, Checkbox, Collapsible, Combobox, Command, ContextMenu, Dialog, Drawer,
DropdownMenu, Empty, Field, Form, HoverCard, ImageCrop, Input, InputGroup, InputOTP,
Item, Kbd, Label, NavigationMenu, NativeSelect, Pagination, Popover, Progress,
RadioGroup, Resizable, ScrollArea, Select, Separator, Sheet, Sidebar, Skeleton, Slider,
Sonner (Toaster), Spinner, StatusMessage, SubmitButton, Switch, Table, Tabs, Textarea,
ThemeProvider, ThemeToggle, Toggle, ToggleGroup, Tooltip.

### Available hooks (also from `@gib/ui`)

- `useIsMobile` — returns `true` when viewport width is below 768px
- `useOnClickOutside` — fires a callback when a click occurs outside a ref element

### Adding a new component via `bun ui-add`

```bash
# From the repo root
bun ui-add
```

This runs `bunx shadcn@latest add` interactively. After adding a new component:

1. Any internal imports the component makes (to other shadcn components, `cn`, hooks)
   **must be updated** to come from `@gib/ui` instead of relative paths
2. Export the new component(s) from `packages/ui/src/index.tsx`

If you add a new hook, add it to `packages/ui/src/hooks/` and export it from both
`packages/ui/src/hooks/index.tsx` and the main `packages/ui/src/index.tsx`.

### `components.json` note

The shadcn `components.json` file specifies `style: "new-york"` and `baseColor: "zinc"`.
The `zinc` base color is irrelevant — the actual visual theme is entirely determined by
`tools/tailwind/theme.css` which overrides all color tokens with OKLCH values. The
`zinc` setting only affects how `bunx shadcn` generates new component defaults; the
runtime appearance is controlled by the theme.

---

## 10. Tools — Shared Configuration Packages

### `@gib/eslint-config`

Three exported configs used across the monorepo:

- **`baseConfig`** — TypeScript-aware ESLint with `recommended`, `recommendedTypeChecked`,
  and `stylisticTypeChecked` rules. Used by all packages.
- **`reactConfig`** — adds `eslint-plugin-react` and `react-hooks` rules.
- **`nextjsConfig`** — adds `@next/eslint-plugin-next` rules including core-web-vitals.
- **`restrictEnvAccess`** — blocks direct `process.env` access outside `env.ts`.

Key enforced rules:

- `@typescript-eslint/consistent-type-imports` — type imports must be separate (`import type`)
- `@typescript-eslint/no-non-null-assertion` — non-null assertions (`!`) are errors
- `@typescript-eslint/no-unused-vars` — unused vars are warnings; prefix with `_` to suppress
- `import/consistent-type-specifier-style` — prefer top-level `import type` over inline `type`

Note: the `consistent-type-imports` enforcement is a known item to potentially relax
in the future (see Known Issues).

### `@gib/prettier-config`

Key settings:

- `singleQuote: true`, `jsxSingleQuote: true` — single quotes throughout
- `trailingComma: 'all'` — trailing commas on all multi-line structures
- `printWidth: 80`, `tabWidth: 2`
- `tailwindFunctions: ['cn', 'cva']` — Tailwind class sorting in these functions

Import order (enforced automatically by `@ianvs/prettier-plugin-sort-imports`):

```
1. Type imports (all)
2. react / react-native
3. next / expo
4. Third-party packages
5. @gib/* type imports, then @gib/* value imports
6. Local type imports, then local relative imports
```

You do not need to manually order imports — Prettier handles it on format. Run
`bun format:fix` to apply.

### `@gib/tailwind-config`

- **`theme.css`** — the complete OKLCH-based design system. Light mode: warm cream
  background with reddish-brown primary. Dark mode: warm dark background with
  orange-warm primary. Do not modify without explicit instruction. When the theme
  needs to change, the entire file is replaced with a new theme generated from a
  shadcn theme generator.
- **`postcss-config.js`** — re-exports `{ plugins: { '@tailwindcss/postcss': {} } }`.
  Used by all apps via `postcss.config.js`.

### `@gib/tsconfig`

- **`base.json`** — strict TypeScript config for all packages. Notable settings:
  `noUncheckedIndexedAccess: true` (array/object access returns `T | undefined`),
  `moduleResolution: "Bundler"`, `module: "Preserve"`, `isolatedModules: true`.
  Uses `tsBuildInfoFile` for incremental caching in `.cache/`.
- **`compiled-package.json`** — extends base with declaration emit settings for
  packages that need to output `.d.ts` files.

---

## 11. Docker & Deployment

### Container overview

Three Docker services are defined in `docker/compose.yml`:

**`convex-backend`** (`ghcr.io/get-convex/convex-backend`)

- The self-hosted Convex runtime
- Data persists in `docker/data/` (gitignored, must be backed up)
- Health check via `curl http://localhost:3210/version`
- Watchtower-enabled for automatic image updates
- `stop_signal: SIGINT` + 10s grace period for clean shutdown

**`convex-dashboard`** (`ghcr.io/get-convex/convex-dashboard`)

- Admin UI for the Convex backend
- Starts only after `convex-backend` is healthy

**`next-app`** (built from `docker/Dockerfile`)

- Multi-stage build: `oven/bun:alpine` builder → `node:20-alpine` runner
- Standalone Next.js output (`CMD ["node", "apps/next/server.js"]`)
- Reverse-proxied by nginx — ports are not directly exposed

All services share the external `nginx-bridge` Docker network.

### Typical production deployment workflow

The Convex backend and dashboard are long-running and rarely need to be rebuilt.
The typical workflow when you've made changes to the Next.js app:

```bash
./scripts/build-next-app
```

If you need to start everything from scratch:

```bash
./scripts/docker-compose up -d backend dashboard
# Wait for backend to be healthy, then:
./scripts/docker-compose up -d next
```

### The `.dockerignore` situation (known issue)

There is one `.dockerignore` file at the repo root: `/.dockerignore`. Docker always
reads the `.dockerignore` from the root of the build context, which is set to `../`
(the repo root) in `compose.yml` — so the root file is the only one that ever applies.

The `.env` file is intentionally included in the build context (the `.env` lines are
commented out in `/.dockerignore`). This is how `SENTRY_AUTH_TOKEN` and `NEXT_PUBLIC_*`
variables reach the Next.js build step inside the container.

Do not un-comment those lines without understanding the full implications — removing
`.env` from the build context would break Sentry source map uploads and
`NEXT_PUBLIC_*` variable baking into the client bundle.

Docker Compose now reads from the root `/.env`, and the helper scripts in `scripts/`
wrap Compose so you do not need to repeatedly pass `--env-file` and `-f`.

### Docker helper scripts

The preferred deployment workflow now uses the helper scripts in `scripts/`:

- `scripts/docker-compose` — wrapper for custom Compose commands using the repo's root
  `/.env` and `docker/compose.yml`
- `scripts/build-next-app` — build and restart the Next container
- `scripts/update-next-app` — `git pull`, then build and restart the Next container
- `scripts/update-convex` — pull and restart the backend/dashboard containers
- `scripts/generate-convex-admin-key` — run the backend container's admin key script

The `scripts/docker-compose` wrapper also translates short aliases like `next`,
`backend`, and `dashboard` into the container names defined in the root `/.env`.

### Convex data persistence

`docker/data/` is mounted as a Docker volume and contains all Convex database data.
This directory is gitignored. Back it up before any server migrations, restarts, or
image updates.

### `generate-convex-admin-key`

A bash script in `scripts/` generates the admin key from a running Convex backend:

```bash
./scripts/generate-convex-admin-key
```

This runs the key generation script inside the `convex-backend` container and prints
the `CONVEX_SELF_HOSTED_ADMIN_KEY` value to stdout.

---

## 12. Code Style Reference

### Functions — always `const` arrow syntax

```typescript
// ✅ Correct — use const for all functions
const fetchUser = async (userId: string) => {
  return await db.get(userId);
};

const MyComponent = ({ name }: { name: string }) => {
  return <div>{name}</div>;
};

// ❌ Wrong — function declarations
function fetchUser(userId: string) { ... }
function MyComponent({ name }: { name: string }) { ... }
```

### Naming conventions

| Thing                       | Convention             | Example             |
| --------------------------- | ---------------------- | ------------------- |
| React components            | `PascalCase`           | `UserProfile.tsx`   |
| Non-component files         | `kebab-case.ts`        | `ban-sus-ips.ts`    |
| Functions and variables     | `camelCase`            | `const getUserById` |
| Constants                   | `SCREAMING_SNAKE_CASE` | `PASSWORD_MIN`      |
| TypeScript types/interfaces | `PascalCase`           | `type UserDoc`      |
| Unused vars/params          | prefix with `_`        | `const _unused`     |

### Imports

- Use `import type` for type-only imports (enforced by ESLint)
- Import order is handled automatically by Prettier on format — don't manually sort
- Always use `.js` extension for Convex-generated file imports
- All UI components and utilities come from `@gib/ui`
- All env vars come from `@/env`

### Error handling

```typescript
// Backend — throw ConvexError with a user-readable message
import { ConvexError } from 'convex/values';

throw new ConvexError('User not found.');

// Frontend — catch ConvexError and display with toast
try {
  await someConvexMutation(args);
} catch (e) {
  if (e instanceof ConvexError) {
    toast.error(e.message);
  }
}
```

### TypeScript

- Strict mode is on everywhere (`strict: true`, `noUncheckedIndexedAccess: true`)
- Array/object index access returns `T | undefined` — always handle the undefined case
- Avoid `any` — use `unknown` with type guards or proper generics
- Non-null assertions (`!`) are ESLint errors — prove your types instead

### Template customization points

When using this repo as a template for a **new project** (not working on the existing
deployment), these things should be updated:

- `apps/next/src/lib/metadata.ts` — update the title template and metadata
- `apps/next/next.config.js` — update `images.remotePatterns` for your domain
- `apps/next/src/app/(frontend)/layout.tsx` — update the root `<html lang>`, site title, etc.
- `apps/next/src/components/landing/content.ts` — update the fallback landing-page copy,
  repository URL, and quick-start text
- `apps/next/src/payload/globals/landing-page.ts` — update or replace the current landing
  page global schema
- `apps/next/src/payload/globals/landing-page-blocks.ts` — update the landing-page block
  definitions if the marketing content model changes
- `apps/next/src/payload.config.ts` — add/remove Payload collections and globals as the
  project evolves
- Root `package.json` — update the workspace `name` field to fit the new project
- `/.env` — fill out all values for the new deployment
- `/.env` — also fill out the Docker Compose container names and domain URLs

---

## 13. Adding New Features — Checklists

### Adding a new field to the users table

1. Add the field to `users` in `packages/backend/convex/schema.ts`
2. Add or update the relevant `mutation` in `packages/backend/convex/auth.ts`
3. Run `bun dev:backend` to let `convex dev` regenerate `_generated/` types
4. Add the corresponding UI in the profile page or wherever the field is managed
5. Update `packages/backend/types/auth.ts` if the field has shared validation constants

### Adding a new Convex function

1. Write the function in the appropriate `.ts` file in `packages/backend/convex/`
2. Run `bun dev:backend` — `convex dev --until-success` will push it and regenerate
   `_generated/api.ts` with the new export
3. Import using `api.moduleName.functionName` with the `.js` extension:
   ```typescript
   import { api } from '@gib/backend/convex/_generated/api.js';
   ```

### Adding a new page

1. Create the page inside `apps/next/src/app/(frontend)/...` unless you are explicitly
   working on the embedded Payload admin/API surface
2. If the page needs data: use the `preloadQuery` + `usePreloadedQuery` SSR pattern
3. If the page should be protected: add its path to `isProtectedRoute` in `src/proxy.ts`
4. Add a layout file if you need a custom `<title>`:
   ```typescript
   export const metadata = { title: 'Page Name' };
   ```
5. Run `bun typecheck` to validate

### Adding or changing Payload content

1. For the current homepage, edit the landing-page global or its block definitions in
   `apps/next/src/payload/globals/landing-page.ts` and
   `apps/next/src/payload/globals/landing-page-blocks.ts`
2. Keep the frontend data contract in sync with
   `apps/next/src/components/landing/content.ts`
3. Fetch or merge the content through `apps/next/src/lib/payload/`
4. If you need fresh Payload types, regenerate `apps/next/payload-types.ts`
5. If you change the database schema shape, regenerate
   `apps/next/src/payload-generated-schema.ts`
6. Do not hand-edit the generated files in `apps/next/src/app/(payload)/`
7. If you introduce additional Payload-managed pages or collections, update
   `docs/payload-cms.md` and this `AGENTS.md` file in the same task

### Adding a new environment variable

See the 4-step checklist in [Section 5](#5-environment-variables--complete-reference).

### Adding a new UI component via shadcn

1. Run `bun ui-add` from the repo root
2. Select the component(s) to add
3. Update any internal imports in the new files to use `@gib/ui` instead of relative paths
4. Add exports to `packages/ui/src/index.tsx`
5. Run `bun typecheck` to validate

---

## 14. Expo App — Scaffold Only, Not Production-Ready

The `apps/expo/` directory exists as a placeholder for future mobile development.
The broken T3 Turbo template code has been cleaned up — all `@acme/*` references,
tRPC, and `better-auth` have been removed and replaced with the correct Convex patterns.
The app will now load, but it is still a bare scaffold with no real features.

### Current state

The app is wired up with the correct providers and patterns:

- **`src/app/_layout.tsx`** — `ConvexAuthProvider` wraps the root, initialized with
  a `ConvexReactClient` pointed at the self-hosted backend
- **`src/utils/convex.ts`** — Convex client setup, derives the backend URL from the
  Expo dev server host in development; reads from `app.config.ts` `extra.convexUrl`
  in production
- **`src/app/index.tsx`** — A minimal home screen that reads auth state via
  `useConvexAuth()` and queries the current user via `api.auth.getUser`
- **`src/app/post/[id].tsx`** — A placeholder detail screen (route structure kept intact)
- **`src/utils/session-store.ts`** — `expo-secure-store` helpers for token storage
- **`src/utils/base-url.ts`** — Dev server localhost detection

### What still needs to be built

- Sign-in / sign-up screens (mirror the Next.js `(auth)/sign-in` flow, adapted for RN)
- Authenticated navigation / route protection (check `isAuthenticated` in layout)
- Any actual app screens and Convex queries for your specific project

### Production URL configuration

For production builds, set `convexUrl` in `app.config.ts` `extra`:

```typescript
extra: {
  convexUrl: 'https://api.convex.example.com',
},
```

The client in `src/utils/convex.ts` will prefer this over the dev-server-derived URL.

---

## 15. Known Issues & Technical Debt

A comprehensive list of known issues, organized by priority. Add new issues here as
they're discovered.

---

**1. Catalog update regression** ⚠️ High priority

Running `bun update` inside individual package directories replaces `catalog:`
references with hard-coded version strings, breaking the single-source-of-truth system.
The correct update workflow (edit root `package.json` catalog → `bun install` →
`bun lint:ws`) needs to be better enforced/documented. Investigate whether Bun has a
native fix for this or if a custom update script is needed.

---

**2. Root `.dockerignore` includes `.env` in build context** ℹ️ Known, works intentionally

The root `.dockerignore` has `.env` commented out, meaning the `.env` file is sent to
the Docker build context. This is how build-time env vars (Sentry token, `NEXT_PUBLIC_*`)
reach the Next.js build step. It's a known imperfect approach but fully functional.
A proper solution would use Docker build args (`ARG`) or multi-stage secrets, but this
requires careful restructuring to avoid breaking Sentry source map uploads.

---

**3. In-memory IP banning resets on restart** ℹ️ Future enhancement

`src/lib/proxy/ban-sus-ips.ts` stores bans in a JavaScript `Map`. Bans reset whenever
the Next.js server restarts (container restart, redeploy, etc.). A more robust solution
would persist bans to Convex (using a `bans` table with TTL-based cleanup via a cron
job) or to Redis.

---

**4. `apps/next/src/lib/metadata.ts` has hardcoded branding** ⚠️ Template concern

When using this as a template for a new project, `metadata.ts` must be updated:
the title template (`'%s | Convex Monorepo'`) and any other project-specific strings.
Same for `next.config.js` `images.remotePatterns` (currently `*.gbrown.org`).

---

**5. No CI/CD** ℹ️ Future enhancement

There is no `.github/workflows/` directory. All deployment is done manually via SSH.
A future enhancement would add GitHub Actions (or Gitea Actions, since this repo is
on Gitea) for automated lint and typecheck on pull requests.

---

**6. Scripts organization** ℹ️ Future exploration

Currently:

- `packages/backend/scripts/generateKeys.mjs` — generates JWT keys (run manually)
- `scripts/generate-convex-admin-key` — bash script to get the admin key from Docker
- `scripts/docker-compose` and related helpers — wrap Docker Compose with the root
  `/.env`

These are separate workflows that both need to be run once when setting up a new
deployment. Explore combining these into a unified setup script — potentially one that
generates the keys AND automatically syncs them to Convex in one step.

---

**7. React Email templates not yet implemented** ℹ️ Future enhancement

`@react-email/components` and `react-email` are in `packages/backend/package.json` as
planned dependencies. The current `usesend.ts` uses inline HTML strings for email
templates. These will be replaced with proper React Email templates in a future update.

---

## 16. Quick Command Reference

```bash
# ── Development ───────────────────────────────────────────────────────────────
bun dev              # Start all apps (Next.js + Expo + Convex backend)
bun dev:next         # Start Next.js + Convex backend (most common)
bun dev:backend      # Start Convex backend only
bun dev:expo         # Start Expo + Convex backend
bun dev:expo:tunnel  # Expo with tunnel for physical device testing

# ── Quality Checks ────────────────────────────────────────────────────────────
bun typecheck        # ← PREFERRED — TypeScript checking, fast
bun lint             # ESLint all packages
bun lint:fix         # ESLint with auto-fix
bun lint:ws          # Check workspace consistency with sherif
bun format           # Check Prettier formatting
bun format:fix       # Apply Prettier formatting

# ── Build (production only) ───────────────────────────────────────────────────
bun build            # Build all packages

# ── Utilities ─────────────────────────────────────────────────────────────────
bun ui-add           # Add shadcn/ui components interactively
bun clean            # Clean all node_modules (git clean -xdf)
bun clean:ws         # Clean workspace caches only

# ── Single Package (Turborepo filters) ────────────────────────────────────────
bun turbo run typecheck -F @gib/next      # Typecheck Next.js app only
bun turbo run lint -F @gib/backend        # Lint backend only
bun turbo run dev -F @gib/next            # Dev Next.js only (no Convex)

# ── Scaffolding ───────────────────────────────────────────────────────────────
bun turbo gen init   # Scaffold a new package

# ── Convex CLI (run from packages/backend/) ───────────────────────────────────
bun dev              # Runs convex dev (push functions + watch)
bun setup            # Runs convex dev --until-success (push once then exit)
bun with-env npx convex env set VAR "value"  # Sync env var to Convex deployment

# ── Docker / Deployment ───────────────────────────────────────────────────────
./scripts/docker-compose ps                   # Show compose status
./scripts/docker-compose up -d backend dashboard  # Start Convex
./scripts/build-next-app                      # Build and deploy Next.js app
./scripts/update-next-app                     # git pull + rebuild Next.js app
./scripts/update-convex                       # Pull and restart Convex services
./scripts/generate-convex-admin-key           # Get admin key
```
