18 KiB
AGENTS.md - Convex Turbo Monorepo
For AI Agents Working in This Repo
Testing & Quality Checks:
- ALWAYS prefer
bun typecheckoverbun buildwhen 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/backendhas no roottsconfig.json(onlyconvex/tsconfig.json)- This is intentional - follows Convex's recommended structure
- Running
bun typecheckon 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
# 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:
"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
"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.
# 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)
// 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:
// ✅ 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:12apps/next/src/components/layout/auth/profile/avatar-upload.tsx:10-11apps/next/src/components/layout/auth/profile/header.tsx:6apps/next/src/components/layout/auth/profile/reset-password.tsx:11apps/next/src/components/layout/auth/profile/user-info.tsx:11
TypeScript
- Strict mode enabled (
noUncheckedIndexedAccess: true) - Use
typeimports:import type { X } from 'y' - Prefix unused vars with
_:const _unused = ... - Avoid
any- useunknownwith type guards
Naming Conventions
- Components:
PascalCase(UserProfile.tsx) - Functions/variables:
camelCase - Constants:
SCREAMING_SNAKE_CASE - Files:
kebab-case.ts(except components)
Error Handling
// 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:
"scripts": {
"dev": "bun with-env next dev --turbo",
"with-env": "dotenv -e ../../.env --"
}
Required Variables
# 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:
# 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)
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
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
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:
- Authentik - OAuth SSO provider
- Password - Custom password auth with email OTP verification via UseSend
Next.js Patterns
Convex Provider Setup (Dual-Layer)
Root Layout (app/layout.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):
'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
'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
'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)
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)
import '../styles.css';
// Use className like web: <View className="flex-1 bg-background" />
Secure Storage for Auth
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:
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/)
-
✅ No @acme references - Successfully migrated to @gib namespace
-
⚠️ TRPC in middleware matcher (
src/middleware.ts:32)// Can safely remove 'trpc' from this line (unused pattern): matcher: ['/(api|trpc)(.*)'];
Expo App (apps/expo/) - IGNORE FOR NOW
-
⚠️ @acme TRPC references (
src/utils/api.tsx:1, line 49)- Imports
AppRouterfrom@acme/api - Exports
RouterInputs,RouterOutputsfrom@acme/api
- Imports
-
⚠️ @acme tailwind config (
postcss.config.js:1)- Requires
@acme/tailwind-config/postcss-config - Should be
@gib/tailwind-config/postcss-config
- Requires
-
⚠️ Uses better-auth - Should migrate to
@convex-dev/auth(future work)
Backend Package (packages/backend/)
-
ℹ️ No root tsconfig.json - This is intentional, not a bug
- Only
convex/tsconfig.jsonexists (Convex requirement) - Running
bun typecheckshows TypeScript help message (expected) scripts/andtypes/folders outsideconvex/to avoid sync issues with Convex cloud
- Only
-
⚠️ Hardcoded branding (
convex/custom/auth/providers/usesend.ts:26)- Email templates reference "TechTracker" and "Study Buddy"
- Should be parameterized or use generic branding
-
⚠️ Password validation mismatch (
convex/custom/auth/providers/password.ts)types/auth.tsPASSWORD_REGEXrequires special charactersvalidatePassword()function doesn't enforce special characters- Either update regex or update validation function
-
⚠️ getAllUsers lacks auth (
convex/auth.ts)- Public query accessible to anyone
- Should require authentication or be internal-only
UI Package (packages/ui/)
-
⚠️ Uses pnpm dlx (
package.json:40)"ui-add": "pnpm dlx shadcn@latest add && prettier src --write --list-different"- Should use
bunx shadcn@latest addinstead - Keep prettier command as-is
- Should use
Adding UI Components
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:
- Never use
bun updatedirectly - Edit version in root
package.jsoncatalog - Run
bun install - Verify with
bun lint:ws