# 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= 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 ( {children} ); } ``` **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 ( {children} ); } ``` ### 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 ; }; 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; } 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: ``` ### 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`