Update AGENTS.md

This commit is contained in:
2026-03-27 03:16:08 -05:00
parent 3d54b75739
commit 8c6891f80d
2 changed files with 176 additions and 79 deletions

253
AGENTS.md
View File

@@ -1,4 +1,4 @@
# AGENTS.md — Convex Turbo Monorepo
# AGENTS.md — Convex + Payload Turbo Monorepo
This is the definitive onboarding guide for AI agents (and humans) working in this
repository. Read it fully before making any changes. It covers architecture, patterns,
@@ -22,7 +22,7 @@ breaking things or introducing inconsistencies.
11. [Docker & Deployment](#11-docker--deployment)
12. [Code Style Reference](#12-code-style-reference)
13. [Adding New Features — Checklists](#13-adding-new-features--checklists)
14. [Expo App — Known Issues, Do Not Touch](#14-expo-app--known-issues-do-not-touch)
14. [Expo App — Scaffold Only, Not Production-Ready](#14-expo-app--scaffold-only-not-production-ready)
15. [Known Issues & Technical Debt](#15-known-issues--technical-debt)
16. [Quick Command Reference](#16-quick-command-reference)
@@ -32,20 +32,24 @@ breaking things or introducing inconsistencies.
This is a personal full-stack monorepo template for spinning up self-hosted web
applications quickly. The core idea: clone the repo, configure the environment
variables, stand up two Docker containers for Convex, reverse-proxy them through
nginx-proxy-manager, and you have a production-ready Next.js website with:
variables, stand up the Convex services, connect the app to an external Postgres
database for Payload CMS, reverse-proxy everything through nginx-proxy-manager,
and you have a production-ready Next.js website with:
- **Self-hosted Convex** as the backend/database (not Convex Cloud)
- **Convex Auth** with two providers: Authentik (OAuth SSO) and a custom Password
provider with email OTP via a self-hosted UseSend instance
- **Payload CMS** embedded inside the Next.js app for admin, REST/GraphQL APIs, and
editable landing-page content backed by Postgres
- **Sentry** for error tracking and performance monitoring
- **Plausible** for privacy-focused analytics
- A **stubbed Expo app** that is present for future mobile work but is not the focus
The primary deliverable from this template is the **Next.js web app**. Every project
built from this template has been entirely focused on `apps/next/` and
`packages/backend/`. The Expo app exists structurally but is non-functional and
should be ignored unless explicitly asked to work on it.
`packages/backend/`. The Next.js app now contains both the public frontend and the
embedded Payload CMS surfaces. The Expo app exists structurally but is non-functional
and should be ignored unless explicitly asked to work on it.
When working in this repo as an AI agent, you are almost certainly working on an
existing running instance of this template — the Convex backend is already deployed,
@@ -70,11 +74,27 @@ configured and difficult to re-stabilize.
only works when the config itself is a `.js` file. Renaming it to `.ts` will break
the build.
- **NEVER remove `withPayload(...)` from `apps/next/next.config.js`** — Payload's
admin routes, REST API, GraphQL API, and generated integration files depend on the
Next config being wrapped correctly.
- **NEVER rename `apps/next/src/proxy.ts` to `middleware.ts`** — `proxy.ts` is the
correct and intentional name for the Next.js route handler file in Next.js 16+.
This was an official rename from the Next.js team to reduce confusion. The file
works exactly as middleware did; it's just named differently now.
- **NEVER manually edit Payload-generated files** — these are copied/generated by the
Payload integration and may be rewritten at any time. Leave these alone unless you
are explicitly regenerating Payload boilerplate:
- `apps/next/payload-types.ts`
- `apps/next/src/app/(payload)/layout.tsx`
- `apps/next/src/app/(payload)/admin/importMap.js`
- `apps/next/src/app/(payload)/admin/[[...segments]]/page.tsx`
- `apps/next/src/app/(payload)/admin/[[...segments]]/not-found.tsx`
- `apps/next/src/app/(payload)/api/[...slug]/route.ts`
- `apps/next/src/app/(payload)/api/graphql/route.ts`
- `apps/next/src/app/(payload)/api/graphql-playground/route.ts`
- **NEVER modify the Sentry configuration files without explicit instruction** — these
took significant effort to get working correctly with the standalone Docker build,
Plausible proxy, and self-hosted Sentry. The files to leave alone are:
@@ -134,6 +154,10 @@ configured and difficult to re-stabilize.
explicit file extensions for ESM imports. The `.js` extension is required even
though the source files are `.ts`.
- **ALWAYS put user-authored Next.js routes in `apps/next/src/app/(frontend)/`** —
the `(payload)` route group is the embedded Payload integration layer. Treat it as
copied/generated infrastructure, not as the place for custom frontend pages.
---
## 3. Monorepo Architecture Overview
@@ -186,6 +210,13 @@ imports needed.
to the Convex deployment. The `scripts/` and `types/` directories sit outside
`convex/` intentionally so they're not uploaded to Convex cloud.
`apps/next` now contains two distinct app-router route groups:
- `src/app/(frontend)/` — the public site and authenticated app pages you actually
build features in
- `src/app/(payload)/` — the embedded Payload admin/API surface that gets copied in
from Payload's Next integration
### Turborepo pipeline
`turbo.json` defines the task pipeline. Key points for agents:
@@ -207,17 +238,26 @@ to the Convex deployment. The `scripts/` and `types/` directories sit outside
### Before any development work can start
The Convex backend must be running and reachable. `bun dev:next` will fail or behave
unexpectedly if it cannot connect to Convex. In practice, when you are working in this
repo on an active project, the Convex containers are already running on the server and
have been configured. If you are starting fresh from the template, see the README for
the full setup walkthrough.
The Convex backend must be running and reachable, and the Payload Postgres database
must be reachable for any Payload-backed pages or admin routes to work. `bun dev:next`
can start without one of these dependencies, but parts of the app will fail at runtime.
In practice, when you are working in this repo on an active project, the Convex
containers are already running on the server and the Payload database already exists.
If you are starting fresh from the template, see the README for the full setup
walkthrough.
The two Convex-related Docker containers are:
- **`convex-backend`** — the actual Convex runtime (default: `https://api.convexmonorepo.gbrown.org`)
- **`convex-dashboard`** — the admin UI (default: `https://dashboard.convexmonorepo.gbrown.org`)
### Payload Postgres
Payload is configured inside `apps/next/src/payload.config.ts` and uses a Postgres
connection string from `PAYLOAD_DB_URL`. Right now that database is expected to be
external to `docker/compose.yml`; the Compose file passes the env vars through to the
`next-app` container but does not provision a Postgres service for Payload.
### The `packages/backend/.env.local` file
You may notice a `.env.local` file in `packages/backend/`. This file is
@@ -256,26 +296,28 @@ date whenever you add a new env var.**
### Complete variable reference
| Variable | Used By | Purpose | Sync to Convex? |
| --------------------------------- | -------------- | ------------------------------------------------------------------------ | --------------- |
| `NODE_ENV` | Next.js | `development` / `production` | No |
| `SENTRY_AUTH_TOKEN` | Next.js build | Source map upload to Sentry | No |
| `NEXT_PUBLIC_SITE_URL` | Next.js | Public URL of the Next.js app | No |
| `NEXT_PUBLIC_CONVEX_URL` | Next.js | Convex backend API URL | No |
| `NEXT_PUBLIC_PLAUSIBLE_URL` | Next.js | Self-hosted Plausible instance URL | No |
| `NEXT_PUBLIC_SENTRY_DSN` | Next.js | Sentry DSN for error reporting | No |
| `NEXT_PUBLIC_SENTRY_URL` | Next.js build | Self-hosted Sentry URL | No |
| `NEXT_PUBLIC_SENTRY_ORG` | Next.js build | Sentry organization slug | No |
| `NEXT_PUBLIC_SENTRY_PROJECT_NAME` | Next.js build | Sentry project name | No |
| `CONVEX_SELF_HOSTED_URL` | Convex CLI | URL of the self-hosted Convex backend | No |
| `CONVEX_SELF_HOSTED_ADMIN_KEY` | Convex CLI | Admin key for the self-hosted backend | No |
| `CONVEX_SITE_URL` | Convex Auth | CORS domain for token exchange (localhost:3000 for dev) | Yes (Dashboard) |
| `USESEND_API_KEY` | Convex backend | API key for the self-hosted UseSend instance | Yes |
| `USESEND_URL` | Convex backend | URL of the self-hosted UseSend instance | Yes |
| `USESEND_FROM_EMAIL` | Convex backend | From address for transactional emails (e.g. `App <noreply@example.com>`) | Yes |
| `AUTH_AUTHENTIK_ID` | Convex backend | Authentik OAuth client ID | Yes |
| `AUTH_AUTHENTIK_SECRET` | Convex backend | Authentik OAuth client secret | Yes |
| `AUTH_AUTHENTIK_ISSUER` | Convex backend | Authentik issuer URL | Yes |
| Variable | Used By | Purpose | Sync to Convex? |
| --------------------------------- | ----------------- | ------------------------------------------------------------------------ | --------------- |
| `NODE_ENV` | Next.js | `development` / `production` | No |
| `SENTRY_AUTH_TOKEN` | Next.js build | Source map upload to Sentry | No |
| `NEXT_PUBLIC_SITE_URL` | Next.js / Payload | Public URL of the Next.js app and Payload live-preview server URL | No |
| `NEXT_PUBLIC_CONVEX_URL` | Next.js | Convex backend API URL | No |
| `NEXT_PUBLIC_PLAUSIBLE_URL` | Next.js | Self-hosted Plausible instance URL | No |
| `NEXT_PUBLIC_SENTRY_DSN` | Next.js | Sentry DSN for error reporting | No |
| `NEXT_PUBLIC_SENTRY_URL` | Next.js build | Self-hosted Sentry URL | No |
| `NEXT_PUBLIC_SENTRY_ORG` | Next.js build | Sentry organization slug | No |
| `NEXT_PUBLIC_SENTRY_PROJECT_NAME` | Next.js build | Sentry project name | No |
| `PAYLOAD_SECRET` | Payload CMS | Payload application secret | No |
| `PAYLOAD_DB_URL` | Payload CMS | Postgres connection string used by Payload | No |
| `CONVEX_SELF_HOSTED_URL` | Convex CLI | URL of the self-hosted Convex backend | No |
| `CONVEX_SELF_HOSTED_ADMIN_KEY` | Convex CLI | Admin key for the self-hosted backend | No |
| `CONVEX_SITE_URL` | Convex Auth | CORS domain for token exchange (localhost:3000 for dev) | Yes (Dashboard) |
| `USESEND_API_KEY` | Convex backend | API key for the self-hosted UseSend instance | Yes |
| `USESEND_URL` | Convex backend | URL of the self-hosted UseSend instance | Yes |
| `USESEND_FROM_EMAIL` | Convex backend | From address for transactional emails (e.g. `App <noreply@example.com>`) | Yes |
| `AUTH_AUTHENTIK_ID` | Convex backend | Authentik OAuth client ID | Yes |
| `AUTH_AUTHENTIK_SECRET` | Convex backend | Authentik OAuth client secret | Yes |
| `AUTH_AUTHENTIK_ISSUER` | Convex backend | Authentik issuer URL | Yes |
### Typesafe env vars — `apps/next/src/env.ts`
@@ -357,25 +399,25 @@ The current catalogs:
```json
"catalog": {
"@eslint/js": "^9.38.0",
"@tailwindcss/postcss": "^4.1.16",
"@types/node": "^22.19.15",
"eslint": "^9.39.4",
"@eslint/js": "^10.0.1",
"@tailwindcss/postcss": "^4.2.2",
"@types/node": "^25.5.0",
"eslint": "^10.1.0",
"prettier": "^3.8.1",
"tailwindcss": "^4.1.16",
"typescript": "^5.9.3",
"tailwindcss": "^4.2.2",
"typescript": "^6.0.2",
"zod": "^4.3.6"
},
"catalogs": {
"convex": {
"convex": "^1.33.1",
"@convex-dev/auth": "^0.0.81"
"@convex-dev/auth": "^0.0.87",
"convex": "^1.34.1"
},
"react19": {
"@types/react": "~19.1.0",
"@types/react-dom": "~19.1.0",
"react": "19.1.4",
"react-dom": "19.1.4"
"@types/react": "~19.2.14",
"@types/react-dom": "~19.2.3",
"react": "19.2.4",
"react-dom": "19.2.4"
}
}
```
@@ -681,47 +723,80 @@ bun with-env npx convex env set JWKS "..."
### Files you should not casually modify
| File | Why |
| ------------------------------- | ---------------------------------------------------------- |
| `next.config.js` | Sentry, Plausible, standalone build — carefully configured |
| `src/proxy.ts` | Auth routing + IP banning — the project's middleware |
| `src/env.ts` | The env contract — add to it, don't restructure it |
| `src/instrumentation.ts` | Sentry server setup — don't touch |
| `src/instrumentation-client.ts` | Sentry client setup — don't touch |
| `src/sentry.server.config.ts` | Sentry server config — don't touch |
| `src/app/styles.css` | Tailwind setup + theme import — don't touch |
| File / Area | Why |
| ---------------------------------------- | ----------------------------------------------------------------------- |
| `next.config.js` | Sentry, Plausible, Payload, standalone build — carefully configured |
| `src/proxy.ts` | Auth routing + IP banning — the project's middleware |
| `src/env.ts` | The env contract — add to it, don't restructure it |
| `src/instrumentation.ts` | Sentry server setup — don't touch |
| `src/instrumentation-client.ts` | Sentry client setup — don't touch |
| `src/sentry.server.config.ts` | Sentry server config — don't touch |
| `src/app/(frontend)/styles.css` | Tailwind setup + theme import — don't touch |
| `payload-types.ts` | Auto-generated by Payload |
| `src/app/(payload)/**` generated entries | Payload admin/API/layout files are copied/generated and may be replaced |
### Route structure
The app uses the Next.js App Router. All authenticated routes live in the `(auth)`
route group (which doesn't create a URL segment — it's just for organization):
The app uses the Next.js App Router, but it is now split into two top-level route
groups:
| Route | File | Rendering | Purpose |
| ------------------ | ------------------------------------- | ---------------- | ---------------------------------------------- |
| `/` | `app/page.tsx` | Server Component | Landing page (hero, features, tech stack, CTA) |
| `/sign-in` | `app/(auth)/sign-in/page.tsx` | Client Component | Sign in, sign up, email OTP — all in one |
| `/profile` | `app/(auth)/profile/page.tsx` | Server Component | User profile management |
| `/forgot-password` | `app/(auth)/forgot-password/page.tsx` | Client Component | Password reset flow |
- **`(frontend)`** — all user-authored public pages and authenticated app pages
- **`(payload)`** — the embedded Payload CMS admin/API surface copied from Payload
The `/profile` route is protected — `src/proxy.ts` redirects unauthenticated users to
`/sign-in`. The `/sign-in` route redirects already-authenticated users to `/`.
All new product/frontend routes should go in `(frontend)`, not `(payload)`.
| Route | File | Rendering | Purpose |
| ------------------------- | ------------------------------------------------ | ---------------- | ---------------------------------------- |
| `/` | `app/(frontend)/page.tsx` | Server Component | Landing page backed by a Payload global |
| `/sign-in` | `app/(frontend)/(auth)/sign-in/page.tsx` | Client Component | Sign in, sign up, email OTP — all in one |
| `/profile` | `app/(frontend)/(auth)/profile/page.tsx` | Server Component | User profile management |
| `/forgot-password` | `app/(frontend)/(auth)/forgot-password/page.tsx` | Client Component | Password reset flow |
| `/admin` | `app/(payload)/admin/[[...segments]]/page.tsx` | Server Component | Payload admin panel |
| `/api/*` | `app/(payload)/api/[...slug]/route.ts` | Route Handlers | Payload REST API |
| `/api/graphql` | `app/(payload)/api/graphql/route.ts` | Route Handler | Payload GraphQL API |
| `/api/graphql-playground` | `app/(payload)/api/graphql-playground/route.ts` | Route Handler | Payload GraphQL playground |
The `/profile` and `/admin` routes are protected — `src/proxy.ts` redirects
unauthenticated users to `/sign-in`. The `/sign-in` route redirects
already-authenticated users to `/`.
To protect a new route, add its pattern to `isProtectedRoute` in `src/proxy.ts`:
```typescript
const isProtectedRoute = createRouteMatcher([
'/profile',
'/admin',
'/dashboard',
'/settings',
]);
```
### Payload CMS integration
Payload is configured inside the Next app rather than as a separate service. The key
pieces are:
- **`apps/next/src/payload.config.ts`** — the source of truth for Payload config,
collections, globals, editor, secret, and Postgres adapter
- **`apps/next/tsconfig.json`** — defines the `@payload-config` alias used by the
generated admin/API files
- **`apps/next/src/payload/collections/`** and `apps/next/src/payload/globals/` — the
editable Payload schema files you should actually work in
- **`apps/next/src/lib/payload/`** — server helpers for obtaining the cached Payload
client and fetching CMS content
- **`apps/next/src/components/payload/refresh-route-on-save.tsx`** — enables live
preview refreshes for the landing page when `?preview=true` is present
The current homepage is backed by the `landing-page` Payload global. The server page at
`apps/next/src/app/(frontend)/page.tsx` calls
`apps/next/src/lib/payload/get-landing-page-content.ts`, which merges saved Payload
content with defaults from `apps/next/src/components/landing/content.ts`.
### The dual Convex provider setup
Two providers are required and both must be present:
**1. `ConvexAuthNextjsServerProvider`** — wraps the root `<html>` element in
`app/layout.tsx`. This is the server-side half that handles cookie reading and
`app/(frontend)/layout.tsx`. This is the server-side half that handles cookie reading and
initial token passing. It has no visible UI.
**2. `ConvexClientProvider`** — a `'use client'` component in
@@ -730,7 +805,7 @@ and initializes the `ConvexReactClient`. All client-side reactive queries and
mutations flow through this.
```tsx
// app/layout.tsx (server)
// app/(frontend)/layout.tsx (server)
<ConvexAuthNextjsServerProvider>
<html lang='en'>
<body>
@@ -743,6 +818,9 @@ mutations flow through this.
Removing either provider breaks auth. The server provider is needed for SSR token
hydration; the client provider is needed for reactive updates.
Payload uses a separate generated layout at `apps/next/src/app/(payload)/layout.tsx`.
Do not try to merge the frontend and Payload layouts into one file.
### SSR data preloading — the preferred pattern for new pages
For any page that needs Convex data, use `preloadQuery` on the server and
@@ -752,7 +830,7 @@ SSR-rendered content.
**Server component (the page file):**
```tsx
// app/(auth)/some-page/page.tsx
// app/(frontend)/(auth)/some-page/page.tsx
import { SomeComponent } from '@/components/some-component';
import { preloadQuery } from 'convex/nextjs';
@@ -834,6 +912,7 @@ This file is the Next.js proxy/middleware. It does two things in sequence:
2. **Auth routing** — `convexAuthNextjsMiddleware` handles authentication:
- Unauthenticated users hitting `/profile` are redirected to `/sign-in`
- Unauthenticated users hitting `/admin` are redirected to `/sign-in`
- Authenticated users hitting `/sign-in` are redirected to `/`
Session cookies are configured with a 30-day max age.
@@ -865,24 +944,26 @@ This handles the nginx-proxy-manager reverse proxy setup correctly.
- **`serverExternalPackages: ['require-in-the-middle']`** — Sentry compatibility
- **`transpilePackages: ['@gib/backend', '@gib/ui']`** — workspace packages need
transpilation since Next.js doesn't transpile `node_modules` by default
- **`withPayload(config)`** — required for embedded Payload admin/API support
- **`images.remotePatterns`** — currently only allows `*.gbrown.org`. If you're
deploying for a different domain, update this or profile pictures won't load.
- **`serverActions.bodySizeLimit: '10mb'`** — allows large file uploads for avatar
- The config is wrapped with `withPlausibleProxy` then `withSentryConfig`
- The config is wrapped with `withPlausibleProxy`, then `withPayload`, then
`withSentryConfig`
### Fonts
Fonts are configured in `apps/next/src/app/layout.tsx` using `next/font`. The current
typeface system:
Fonts are configured in `apps/next/src/app/(frontend)/layout.tsx` using `next/font`.
The current typeface system:
- **Kanit** — display font used in the header logo and CTA (loaded via `next/font/google`)
- **Poppins** — primary sans-serif body font
- **Libre Baskerville** — serif font (available for editorial use)
- **Victor Mono** — monospace font (available for code blocks)
- **Geist** — primary sans font for the frontend layout
- **Geist Mono** — monospace font for the frontend layout
- **Kanit** — display font loaded locally in `src/components/layout/header/index.tsx`
for the brand wordmark
The fonts are injected as CSS variables and mapped in `tools/tailwind/theme.css`.
To change fonts, update the `next/font` imports in `layout.tsx` and update the
corresponding CSS variable assignments in `theme.css`.
To change the frontend base fonts, update the `next/font` imports in
`apps/next/src/app/(frontend)/layout.tsx`. If you also want the brand wordmark to
change, update `apps/next/src/components/layout/header/index.tsx`.
### Analytics and monitoring
@@ -1195,7 +1276,13 @@ deployment), these things should be updated:
- `apps/next/src/lib/metadata.ts` — update the title template and metadata
- `apps/next/next.config.js` — update `images.remotePatterns` for your domain
- `apps/next/src/app/layout.tsx` — update the root `<html lang>`, site title, etc.
- `apps/next/src/app/(frontend)/layout.tsx` — update the root `<html lang>`, site title, etc.
- `apps/next/src/components/landing/content.ts` — update the fallback landing-page copy,
repository URL, and quick-start text
- `apps/next/src/payload/globals/landing-page.ts` — update or replace the current landing
page global schema
- `apps/next/src/payload.config.ts` — add/remove Payload collections and globals as the
project evolves
- Root `package.json` — update the workspace `name` field to fit the new project
- `/.env` — fill out all values for the new deployment
- `docker/.env` — fill out container names and domain URLs
@@ -1224,7 +1311,8 @@ deployment), these things should be updated:
### Adding a new page
1. Create `apps/next/src/app/<route>/page.tsx`
1. Create the page inside `apps/next/src/app/(frontend)/...` unless you are explicitly
working on the embedded Payload admin/API surface
2. If the page needs data: use the `preloadQuery` + `usePreloadedQuery` SSR pattern
3. If the page should be protected: add its path to `isProtectedRoute` in `src/proxy.ts`
4. Add a layout file if you need a custom `<title>`:
@@ -1233,6 +1321,15 @@ deployment), these things should be updated:
```
5. Run `bun typecheck` to validate
### Adding or changing Payload content
1. Edit the relevant schema file in `apps/next/src/payload/collections/` or
`apps/next/src/payload/globals/`
2. Register the collection/global in `apps/next/src/payload.config.ts`
3. Fetch the content from server code via `apps/next/src/lib/payload/`
4. If you need fresh Payload types, regenerate `apps/next/payload-types.ts`
5. Do not hand-edit the generated files in `apps/next/src/app/(payload)/`
### Adding a new environment variable
See the 4-step checklist in [Section 5](#5-environment-variables--complete-reference).

View File

@@ -11,7 +11,7 @@ export default buildConfig({
editor: lexicalEditor(),
collections: [Users],
globals: [LandingPage],
secret: env.PAYLOAD_SECRET ?? '',
secret: env.PAYLOAD_SECRET,
db: postgresAdapter({
pool: {
connectionString: env.PAYLOAD_DB_URL,