From 8c6891f80d6e9397464e1a01240bfd3b3ce004b9 Mon Sep 17 00:00:00 2001 From: gibbyb Date: Fri, 27 Mar 2026 03:16:08 -0500 Subject: [PATCH] Update AGENTS.md --- AGENTS.md | 253 ++++++++++++++++++++++---------- apps/next/src/payload.config.ts | 2 +- 2 files changed, 176 insertions(+), 79 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e34b2e6..4e2cf7e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 `) | 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 `) | 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 `` 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) @@ -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 ``, site title, etc. +- `apps/next/src/app/(frontend)/layout.tsx` — update the root ``, 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//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 ``: @@ -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). diff --git a/apps/next/src/payload.config.ts b/apps/next/src/payload.config.ts index 8e87620..fc67d82 100644 --- a/apps/next/src/payload.config.ts +++ b/apps/next/src/payload.config.ts @@ -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,