Files
convex-monorepo/AGENTS.md
2026-01-13 11:11:22 -06:00

645 lines
18 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`