# AGENTS.md - Convex Turbo Monorepo

## For AI Agents Working in This Repo

**Testing & Quality Checks:**

- **ALWAYS prefer `bun typecheck`** over `bun build` when checking your work
  - Typecheck is faster and sufficient for validation
  - Build is for production deployment only and takes much longer

**Scope Focus:**

- **Ignore `apps/expo/` directory** unless explicitly asked to work on it
  - Focus on Next.js app and backend for now
  - Expo setup is incomplete and will be addressed later

**Backend Package Note:**

- `@gib/backend` has no root `tsconfig.json` (only `convex/tsconfig.json`)
  - This is intentional - follows Convex's recommended structure
  - Running `bun typecheck` on backend will show TypeScript help output
  - This is expected behavior, not an error

**Self-Hosted Setup:**

- This project uses self-hosted Convex (not Convex cloud)
- Database runs on separate server via Docker
- See `docker/` directory for deployment configuration

---

## Quick Reference

### Build/Lint/Test Commands

```bash
# Development
bun dev                      # Run all apps (Next.js + Expo + Backend)
bun dev:next                 # Run Next.js + Convex backend only
bun dev:expo                 # Run Expo + Convex backend only
bun dev:backend              # Run Convex backend only
bun dev:expo:tunnel          # Expo with tunnel for physical device

# Quality (PREFER typecheck over build for testing)
bun typecheck                # TypeScript checking (PREFERRED for validation)
bun lint                     # Lint all packages
bun lint:fix                 # Lint and auto-fix
bun format                   # Check formatting
bun format:fix               # Fix formatting

# Build (production only, slower)
bun build                    # Build all packages

# Single Package Commands (use Turborepo filters)
bun turbo run dev -F @gib/next           # Single app dev
bun turbo run lint -F @gib/backend       # Lint single package
bun turbo run typecheck -F @gib/ui       # Typecheck single package

# Cleanup
bun clean                    # Clean all node_modules (git clean)
bun clean:ws                 # Clean workspace caches
```

---

## Project Structure

```
convex-monorepo/
├── apps/
│   ├── next/           # Next.js 16.0.0 web app (@gib/next)
│   └── expo/           # Expo 54 mobile app (@gib/expo) [IGNORE FOR NOW]
├── packages/
│   ├── backend/        # Convex backend (@gib/backend)
│   │   ├── convex/     # Convex functions, schema, auth (synced to cloud)
│   │   ├── scripts/    # Build utilities (e.g., key generation)
│   │   └── types/      # Shared type definitions
│   └── ui/             # Shared shadcn/ui components (@gib/ui)
├── tools/
│   ├── eslint/         # @gib/eslint-config
│   ├── prettier/       # @gib/prettier-config
│   ├── tailwind/       # @gib/tailwind-config
│   └── typescript/     # @gib/tsconfig
├── docker/             # Self-hosted Convex deployment
└── .env                # Central environment variables
```

**Note:** Only `packages/backend/convex/` is synced to Convex cloud. Other directories (`scripts/`, `types/`) are kept separate to avoid sync issues.

---

## Dependency Management

### Catalogs (Single Source of Truth)

All shared dependencies are defined in root `package.json` catalogs:

```json
"catalog": {
  "prettier": "^3.6.2",
  "typescript": "^5.9.3",
  "eslint": "^9.38.0",
  "zod": "^4.1.12"
},
"catalogs": {
  "convex": { "convex": "^1.28.0", "@convex-dev/auth": "^0.0.81" },
  "react19": { "react": "19.1.4", "react-dom": "19.1.4" }
}
```

### Using Catalogs in Packages

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

### Updating Dependencies

**IMPORTANT:** Do NOT use `bun update` directly - it may replace `catalog:` with hard-coded versions.

```bash
# Correct workflow:
1. Edit version in root package.json catalog section
2. Run: bun install
3. Verify with: bun lint:ws (runs sherif)
```

---

## Code Style Guidelines

### Imports (via @gib/prettier-config)

```typescript
// Order: Types → React → Next/Expo → Third-party → @gib/* → Local
import type { Metadata } from 'next';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useMutation, useQuery } from 'convex/react';
import { ConvexError } from 'convex/values';

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

import { cn } from '@gib/ui';
import { Button } from '@gib/ui/button';

import type { User } from './types';
import { helper } from '../utils';
```

### Convex Imports (.js Extensions)

**IMPORTANT:** Convex generated files require `.js` extensions due to ESM compatibility:

```typescript
// ✅ Correct - with .js extension
import type { Id } from '@gib/backend/convex/_generated/dataModel.js';
// ❌ Wrong - will fail to import
import { api } from '@gib/backend/convex/_generated/api';
import { api } from '@gib/backend/convex/_generated/api.js';
```

**Why?** The project uses `"type": "module"` in package.json, requiring explicit file extensions for local module imports per ESM specification.

**Files using this pattern:**

- `apps/next/src/app/(auth)/profile/page.tsx:12`
- `apps/next/src/components/layout/auth/profile/avatar-upload.tsx:10-11`
- `apps/next/src/components/layout/auth/profile/header.tsx:6`
- `apps/next/src/components/layout/auth/profile/reset-password.tsx:11`
- `apps/next/src/components/layout/auth/profile/user-info.tsx:11`

### TypeScript

- Strict mode enabled (`noUncheckedIndexedAccess: true`)
- Use `type` imports: `import type { X } from 'y'`
- Prefix unused vars with `_`: `const _unused = ...`
- Avoid `any` - use `unknown` with type guards

### Naming Conventions

- Components: `PascalCase` (`UserProfile.tsx`)
- Functions/variables: `camelCase`
- Constants: `SCREAMING_SNAKE_CASE`
- Files: `kebab-case.ts` (except components)

### Error Handling

```typescript
// Convex functions - use ConvexError
import { ConvexError } from 'convex/values';

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

// Client-side - handle gracefully
try {
  await updateUser({ name });
} catch (e) {
  if (e instanceof ConvexError) {
    toast.error(e.message);
  }
}
```

### Formatting

- Single quotes, trailing commas
- 80 char line width, 2 space indent
- Tailwind classes sorted via prettier-plugin-tailwindcss
- Use `cn()` for conditional classes: `cn('base', condition && 'active')`

---

## Environment Variables

### Central .env (root)

All env vars in `/.env`. Apps load via `with-env` script:

```json
"scripts": {
  "dev": "bun with-env next dev --turbo",
  "with-env": "dotenv -e ../../.env --"
}
```

### Required Variables

```bash
# Convex Backend (Self-Hosted)
CONVEX_SELF_HOSTED_URL=https://api.convex.example.com
CONVEX_SELF_HOSTED_ADMIN_KEY=<generated via generateKeys.mjs>
CONVEX_SITE_URL=https://convex.example.com

# Next.js Public
NEXT_PUBLIC_CONVEX_URL=https://api.convex.example.com
NEXT_PUBLIC_SITE_URL=https://example.com
NEXT_PUBLIC_PLAUSIBLE_URL=https://plausible.example.com
NEXT_PUBLIC_SENTRY_DSN=
NEXT_PUBLIC_SENTRY_URL=
NEXT_PUBLIC_SENTRY_ORG=
NEXT_PUBLIC_SENTRY_PROJECT_NAME=

# Server-side
SENTRY_AUTH_TOKEN=

# Auth (sync to Convex deployment - see below)
AUTH_AUTHENTIK_ID=
AUTH_AUTHENTIK_SECRET=
AUTH_AUTHENTIK_ISSUER=
USESEND_API_KEY=
```

### Syncing to Convex Deployment

Environment variables needed by backend functions must be synced to Convex:

```bash
# Via CLI (from packages/backend/)
bun with-env npx convex env set AUTH_AUTHENTIK_ID "value"
bun with-env npx convex env set AUTH_AUTHENTIK_SECRET "value"
bun with-env npx convex env set AUTH_AUTHENTIK_ISSUER "value"
bun with-env npx convex env set USESEND_API_KEY "value"
bun with-env npx convex env set CONVEX_SITE_URL "https://convex.example.com"

# Or via Convex Dashboard at your self-hosted URL
```

---

## Convex Backend Patterns

### Package Structure

```
packages/backend/
├── convex/              # Convex functions (synced to cloud)
│   ├── _generated/      # Auto-generated API/types
│   ├── custom/          # Custom auth providers
│   │   └── auth/
│   │       └── providers/
│   │           ├── password.ts
│   │           └── usesend.ts
│   ├── auth.config.ts   # Auth CORS configuration
│   ├── auth.ts          # Auth setup + user queries/mutations
│   ├── crons.ts         # Scheduled jobs
│   ├── files.ts         # File upload/storage utilities
│   ├── http.ts          # HTTP routes for auth
│   ├── schema.ts        # Database schema
│   ├── utils.ts         # Helper functions
│   └── tsconfig.json    # Convex-specific TypeScript config
├── scripts/             # Build scripts (outside convex/)
│   └── generateKeys.mjs # JWT key generation for auth
└── types/               # Shared type exports (outside convex/)
    ├── auth.ts          # Password validation constants
    └── index.ts
```

**Note:** Only `convex/` directory is synced to Convex cloud. Other directories (`scripts/`, `types/`) are kept separate to avoid sync issues with Convex deployment.

### Schema (`packages/backend/convex/schema.ts`)

```typescript
import { authTables } from '@convex-dev/auth/server';
import { defineSchema, defineTable } from 'convex/server';
import { v } from 'convex/values';

export default defineSchema({
  ...authTables,
  users: defineTable({
    name: v.optional(v.string()),
    image: v.optional(v.id('_storage')),
    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()),
  })
    .index('email', ['email'])
    .index('name', ['name'])
    .index('phone', ['phone']),

  profiles: defineTable({
    userId: v.id('users'),
    theme_preference: v.optional(v.string()),
  }).index('userId', ['userId']),
});
```

### Queries & Mutations

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

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

export const getUser = query({
  args: { userId: v.optional(v.id('users')) },
  handler: async (ctx, args) => {
    const userId = args.userId ?? (await getAuthUserId(ctx));
    if (!userId) throw new ConvexError('Not authenticated.');

    const user = await ctx.db.get(userId);
    if (!user) throw new ConvexError('User not found.');

    return user;
  },
});

export const updateUser = mutation({
  args: {
    name: v.optional(v.string()),
    email: v.optional(v.string()),
    image: v.optional(v.id('_storage')),
  },
  handler: async (ctx, args) => {
    const userId = await getAuthUserId(ctx);
    if (!userId) throw new ConvexError('Not authenticated.');

    await ctx.db.patch(userId, args);
  },
});
```

### Auth Setup

```typescript
import { Authentik } from '@convex-dev/auth/providers/authentik';
import { convexAuth } from '@convex-dev/auth/server';

import { Password } from './custom/auth/providers/password';

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

**Providers:**

1. **Authentik** - OAuth SSO provider
2. **Password** - Custom password auth with email OTP verification via UseSend

---

## Next.js Patterns

### Convex Provider Setup (Dual-Layer)

**Root Layout** (`app/layout.tsx`):

```tsx
import { ConvexAuthNextjsServerProvider } from '@convex-dev/auth/nextjs/server';

export default function RootLayout({ children }) {
  return (
    <ConvexAuthNextjsServerProvider>
      <html lang="en">
        <body>
          <ConvexClientProvider>{children}</ConvexClientProvider>
        </body>
      </html>
    </ConvexAuthNextjsServerProvider>
  );
}
```

**Client Provider** (`components/providers/ConvexClientProvider.tsx`):

```tsx
'use client';

import { ConvexAuthNextjsProvider } from '@convex-dev/auth/nextjs';
import { ConvexReactClient } from 'convex/react';

const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);

export function ConvexClientProvider({ children }) {
  return (
    <ConvexAuthNextjsProvider client={convex}>
      {children}
    </ConvexAuthNextjsProvider>
  );
}
```

### Server Component Data Preloading

```tsx
'use server';

import { ProfileComponent } from '@/components/profile';
import { preloadQuery } from 'convex/nextjs';

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

const ProfilePage = async () => {
  const preloadedUser = await preloadQuery(api.auth.getUser);

  return <ProfileComponent preloadedUser={preloadedUser} />;
};

export default ProfilePage;
```

### Client Component Hydration

```tsx
'use client';

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

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

interface Props {
  preloadedUser: Preloaded<typeof api.auth.getUser>;
}

export function ProfileComponent({ preloadedUser }: Props) {
  const user = usePreloadedQuery(preloadedUser);
  const updateUser = useMutation(api.auth.updateUser);

  // ... interactive logic
}
```

### Path Aliases

- `@/*` → `./src/*` (Next.js)
- `~/*` → `./src/*` (Expo)

### Middleware (Auth Protection)

```typescript
import {
  convexAuthNextjsMiddleware,
  isAuthenticatedNextjs,
} from '@convex-dev/auth/nextjs/server';

export default convexAuthNextjsMiddleware(async (request, { convexAuth }) => {
  // Redirect authenticated users away from sign-in
  if (isSignInPage(request) && (await convexAuth.isAuthenticated())) {
    return nextjsMiddlewareRedirect(request, '/');
  }

  // Protect routes
  if (isProtectedRoute(request) && !(await convexAuth.isAuthenticated())) {
    return nextjsMiddlewareRedirect(request, '/sign-in');
  }
});
```

---

## Expo Patterns [IGNORE FOR NOW]

### NativeWind (Tailwind for RN)

```tsx
import '../styles.css';

// Use className like web: <View className="flex-1 bg-background" />
```

### Secure Storage for Auth

```typescript
import * as SecureStore from 'expo-secure-store';

// Tokens stored via SecureStore, not AsyncStorage
```

---

## Self-Hosted Deployment

This repo includes Docker configuration for self-hosted Convex deployment in `docker/`.

**Services:**

- `convex-backend` - Self-hosted Convex instance (port 3210)
- `convex-dashboard` - Admin dashboard (port 6791)
- `next-app` - Next.js application (standalone build)

**Quick Start:**

```bash
cd docker/
./generate_convex_admin_key  # Generate CONVEX_SELF_HOSTED_ADMIN_KEY
# Edit .env with your values
docker compose up -d
```

**Network:** Uses external `nginx-bridge` network by default. Change in `compose.yml` if needed.

See `docker/compose.yml` and `docker/.env.example` for full configuration details.

---

## Known Issues / Cleanup Needed

### Next.js App (apps/next/)

1. ✅ **No @acme references** - Successfully migrated to @gib namespace

2. ⚠️ **TRPC in middleware matcher** (`src/middleware.ts:32`)
   ```typescript
   // Can safely remove 'trpc' from this line (unused pattern):
   matcher: ['/(api|trpc)(.*)'];
   ```

### Expo App (apps/expo/) - IGNORE FOR NOW

3. ⚠️ **@acme TRPC references** (`src/utils/api.tsx:1`, line 49)
   - Imports `AppRouter` from `@acme/api`
   - Exports `RouterInputs`, `RouterOutputs` from `@acme/api`

4. ⚠️ **@acme tailwind config** (`postcss.config.js:1`)
   - Requires `@acme/tailwind-config/postcss-config`
   - Should be `@gib/tailwind-config/postcss-config`

5. ⚠️ **Uses better-auth** - Should migrate to `@convex-dev/auth` (future work)

### Backend Package (packages/backend/)

6. ℹ️ **No root tsconfig.json** - This is intentional, not a bug
   - Only `convex/tsconfig.json` exists (Convex requirement)
   - Running `bun typecheck` shows TypeScript help message (expected)
   - `scripts/` and `types/` folders outside `convex/` to avoid sync issues with Convex cloud

7. ⚠️ **Hardcoded branding** (`convex/custom/auth/providers/usesend.ts:26`)
   - Email templates reference "TechTracker" and "Study Buddy"
   - Should be parameterized or use generic branding

8. ⚠️ **Password validation mismatch** (`convex/custom/auth/providers/password.ts`)
   - `types/auth.ts` `PASSWORD_REGEX` requires special characters
   - `validatePassword()` function doesn't enforce special characters
   - Either update regex or update validation function

9. ⚠️ **getAllUsers lacks auth** (`convex/auth.ts`)
   - Public query accessible to anyone
   - Should require authentication or be internal-only

### UI Package (packages/ui/)

10. ⚠️ **Uses pnpm dlx** (`package.json:40`)
    ```json
    "ui-add": "pnpm dlx shadcn@latest add && prettier src --write --list-different"
    ```

    - Should use `bunx shadcn@latest add` instead
    - Keep prettier command as-is

---

## Adding UI Components

```bash
bun ui-add              # Interactive shadcn/ui component addition
```

**Note:** Currently uses `pnpm dlx` internally (see Known Issues #10). Will be updated to use `bunx`.

---

## Troubleshooting

### Backend typecheck shows help message

**Issue:** Running `bun typecheck` from root shows TypeScript help for `@gib/backend`.

**Explanation:** This is expected. The backend package has no root `tsconfig.json` (only `convex/tsconfig.json`) following Convex's recommended structure.

**Solution:** No action needed. This is intentional behavior.

### Imports from Convex fail without .js extension

**Issue:** TypeScript can't resolve `@gib/backend/convex/_generated/api`.

**Solution:** Add `.js` extension: `@gib/backend/convex/_generated/api.js`

**Why:** Project uses `"type": "module"` requiring explicit extensions for ESM.

### Catalog updates break workspace

**Issue:** After updating dependencies, packages show version mismatches.

**Solution:**

1. Never use `bun update` directly
2. Edit version in root `package.json` catalog
3. Run `bun install`
4. Verify with `bun lint:ws`
