645 lines
18 KiB
Markdown
645 lines
18 KiB
Markdown
# 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`
|