# AGENTS.md — Convex 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.

---

## 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 — Known Issues, Do Not Touch](#14-expo-app--known-issues-do-not-touch)
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 two Docker containers for Convex, reverse-proxy them 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
- **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 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 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 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`.

---

## 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.

### 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. `bun dev:next` will fail or behave
unexpectedly if it cannot connect to Convex. In practice, when you are working in this
repo on an active project, the Convex containers are already running on the server and
have been configured. 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`)

### 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.**

### 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        | Public URL of the Next.js app                                            | 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              |
| `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).

---

## 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": "^9.38.0",
  "@tailwindcss/postcss": "^4.1.16",
  "@types/node": "^22.19.15",
  "eslint": "^9.39.4",
  "prettier": "^3.8.1",
  "tailwindcss": "^4.1.16",
  "typescript": "^5.9.3",
  "zod": "^4.3.6"
},
"catalogs": {
  "convex": {
    "convex": "^1.33.1",
    "@convex-dev/auth": "^0.0.81"
  },
  "react19": {
    "@types/react": "~19.1.0",
    "@types/react-dom": "~19.1.0",
    "react": "19.1.4",
    "react-dom": "19.1.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                            | Why                                                        |
| ------------------------------- | ---------------------------------------------------------- |
| `next.config.js`                | Sentry, Plausible, 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/styles.css`            | Tailwind setup + theme import — don't touch                |

### Route structure

The app uses the Next.js App Router. All authenticated routes live in the `(auth)`
route group (which doesn't create a URL segment — it's just for organization):

| Route              | File                                  | Rendering        | Purpose                                        |
| ------------------ | ------------------------------------- | ---------------- | ---------------------------------------------- |
| `/`                | `app/page.tsx`                        | Server Component | Landing page (hero, features, tech stack, CTA) |
| `/sign-in`         | `app/(auth)/sign-in/page.tsx`         | Client Component | Sign in, sign up, email OTP — all in one       |
| `/profile`         | `app/(auth)/profile/page.tsx`         | Server Component | User profile management                        |
| `/forgot-password` | `app/(auth)/forgot-password/page.tsx` | Client Component | Password reset flow                            |

The `/profile` route is 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',
  '/dashboard',
  '/settings',
]);
```

### The dual Convex provider setup

Two providers are required and both must be present:

**1. `ConvexAuthNextjsServerProvider`** — wraps the root `<html>` element in
`app/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/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.

### 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/(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`
   - 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
- **`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 `withSentryConfig`

### Fonts

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

- **Kanit** — display font used in the header logo and CTA (loaded via `next/font/google`)
- **Poppins** — primary sans-serif body font
- **Libre Baskerville** — serif font (available for editorial use)
- **Victor Mono** — monospace font (available for code blocks)

The fonts are injected as CSS variables and mapped in `tools/tailwind/theme.css`.
To change fonts, update the `next/font` imports in `layout.tsx` and update the
corresponding CSS variable assignments in `theme.css`.

### 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
# On the production server (via SSH)
cd docker/

# Build the Next.js app with current source
sudo docker compose build next-app

# Once build succeeds, bring up the new container
sudo docker compose up -d next-app
```

If you need to start everything from scratch:

```bash
sudo docker compose up -d convex-backend convex-dashboard
# Wait for backend to be healthy, then:
sudo docker compose up -d next-app
```

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

There are two `.dockerignore` files:

- `/.dockerignore` (root) — has `.env` commented out, meaning the `.env` file IS
  included in the Docker build context
- `/docker/.dockerignore` — explicitly excludes `.env`

The Docker build context is set to the repo root in `compose.yml`. The root
`.dockerignore` is the one that applies. Because `.env` is not excluded, it gets sent
to the builder and environment variables are available at build time. This is how
`SENTRY_AUTH_TOKEN` and other build-time vars reach the Next.js build step.

This is a known imperfect approach but it works. Do not "fix" it 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.

### 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 `docker/` that generates the admin key from a running Convex backend:

```bash
cd docker/
./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/layout.tsx` — update the root `<html lang>`, site title, etc.
- Root `package.json` — update the workspace `name` field to fit the new project
- `/.env` — fill out all values for the new deployment
- `docker/.env` — fill out 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 `apps/next/src/app/<route>/page.tsx`
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 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 — Known Issues, Do Not Touch

The `apps/expo/` directory exists as a placeholder for future mobile development.
It is currently non-functional — it still contains the original T3 Turbo template
code and has not been migrated to use Convex or `@convex-dev/auth`.

**Do not work on the Expo app unless explicitly asked.** If you are asked to work on it,
be aware that the following are all broken and will need to be addressed:

| File                    | Issue                                                                                                              |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `src/utils/api.tsx`     | Imports `AppRouter` from `@acme/api` — package does not exist. Uses tRPC, which is not used anywhere in this repo. |
| `src/utils/auth.ts`     | Uses `better-auth` / `@better-auth/expo` instead of `@convex-dev/auth`                                             |
| `src/app/index.tsx`     | Old T3 Turbo template code using tRPC mutations and better-auth session patterns                                   |
| `src/app/post/[id].tsx` | Same — tRPC and better-auth patterns throughout                                                                    |
| `postcss.config.js`     | References `@acme/tailwind-config` — package does not exist                                                        |
| `src/styles.css`        | Imports from `@acme/tailwind-config` — package does not exist                                                      |
| `eslint.config.mts`     | Imports from `@acme/eslint-config` — package does not exist                                                        |
| `eas.json`              | Specifies `pnpm 9.15.4` as the package manager — should be bun                                                     |

When the time comes to properly implement the Expo app, the right approach is to start
fresh with Convex + `@convex-dev/auth` patterns (mirroring how the Next.js app works),
not to try to incrementally fix the broken T3 Turbo code.

---

## 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. Password validation mismatch** ⚠️ Medium priority

`packages/backend/types/auth.ts` exports `PASSWORD_REGEX` which requires special
characters. `packages/backend/convex/custom/auth/providers/password.ts::validatePassword()`
does NOT check for special characters. These two are inconsistent. The frontend sign-in
form does enforce special characters via its own Zod schema, but the backend validation
is weaker than the type definition implies. Either align `validatePassword()` to match
`PASSWORD_REGEX`, or update `PASSWORD_REGEX` to not require special characters.

---

**3. `import type` enforcement removed** ✅ Fixed

The `@typescript-eslint/consistent-type-imports` and `import/consistent-type-specifier-style`
rules that forced separate `import type` statements have been removed from
`tools/eslint/base.ts`. Type imports can now be mixed inline or on separate lines —
whatever reads most naturally in context.

---

**4. `const` arrow function lint rule added** ✅ Fixed

`eslint-plugin-prefer-arrow-functions` has been added to `tools/eslint/base.ts` and the
`prefer-arrow-functions/prefer-arrow-functions` rule is now enforced as a warning. All
function declarations across the codebase have been auto-converted to `const` arrow
function syntax.

---

**5. Turbo generator uses pnpm** ✅ Fixed in this session

`turbo/generators/config.ts` previously called `pnpm i` and `pnpm prettier`. This has
been fixed to use `bun install` and `bun prettier`.

---

**6. 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.

---

**7. 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.

---

**8. `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`).

---

**9. 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.

---

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

Currently:

- `packages/backend/scripts/generateKeys.mjs` — generates JWT keys (run manually)
- `docker/generate_convex_admin_key` — bash script to get the admin key from Docker

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, possibly
moving everything to `docker/scripts/`.

---

**11. Expo `eas.json` specifies pnpm** ⚠️ Low priority (Expo is WIP anyway)

`apps/expo/eas.json` specifies `pnpm 9.15.4` as the package manager. Should specify
bun. This is low priority since the Expo app is not in active development.

---

**12. 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 (run from docker/) ─────────────────────────────────────────────────
sudo docker compose up -d convex-backend convex-dashboard   # Start Convex
sudo docker compose build next-app                          # Build Next.js image
sudo docker compose up -d next-app                          # Deploy Next.js
sudo docker compose logs -f                                 # Stream all logs
./generate_convex_admin_key                                 # Get admin key
```
