1436 lines
59 KiB
Markdown
1436 lines
59 KiB
Markdown
# AGENTS.md — Convex 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,
|
||
constraints, and known issues — everything you need to work effectively without
|
||
breaking things or introducing inconsistencies.
|
||
|
||
---
|
||
|
||
## Table of Contents
|
||
|
||
1. [What This Repo Is](#1-what-this-repo-is)
|
||
2. [Critical Rules — Read First](#2-critical-rules--read-first)
|
||
3. [Monorepo Architecture Overview](#3-monorepo-architecture-overview)
|
||
4. [Self-Hosted Stack & Runtime Requirements](#4-self-hosted-stack--runtime-requirements)
|
||
5. [Environment Variables — Complete Reference](#5-environment-variables--complete-reference)
|
||
6. [Dependency Management — The Catalog System](#6-dependency-management--the-catalog-system)
|
||
7. [Convex Backend Deep Dive](#7-convex-backend-deep-dive)
|
||
8. [Next.js App Deep Dive](#8-nextjs-app-deep-dive)
|
||
9. [UI Package (`@gib/ui`)](#9-ui-package-gibui)
|
||
10. [Tools — Shared Configuration Packages](#10-tools--shared-configuration-packages)
|
||
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)
|
||
15. [Known Issues & Technical Debt](#15-known-issues--technical-debt)
|
||
16. [Quick Command Reference](#16-quick-command-reference)
|
||
|
||
---
|
||
|
||
## 1. What This Repo Is
|
||
|
||
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:
|
||
|
||
- **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
|
||
- **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.
|
||
|
||
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,
|
||
auth is already configured, and your job is to build features on top of the
|
||
established foundation.
|
||
|
||
---
|
||
|
||
## 2. Critical Rules — Read First
|
||
|
||
These are hard constraints. Violating them will break things that are intentionally
|
||
configured and difficult to re-stabilize.
|
||
|
||
### Never do these without explicit instruction:
|
||
|
||
- **NEVER modify anything inside `packages/backend/convex/_generated/`** — these
|
||
files are auto-generated by the Convex CLI and will be overwritten. Read them for
|
||
type information, never edit them.
|
||
|
||
- **NEVER rename `apps/next/next.config.js` to `next.config.ts`** — the config uses
|
||
`jiti` to import `./src/env.ts` (a TypeScript file) at build time. This pattern
|
||
only works when the config itself is a `.js` file. Renaming it to `.ts` will break
|
||
the build.
|
||
|
||
- **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 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:
|
||
- `apps/next/next.config.js`
|
||
- `apps/next/src/instrumentation.ts`
|
||
- `apps/next/src/instrumentation-client.ts`
|
||
- `apps/next/src/sentry.server.config.ts`
|
||
|
||
- **NEVER remove `typescript.ignoreBuildErrors: true` from `next.config.js`** — some
|
||
shadcn/ui components produce spurious TypeScript build errors. This flag keeps the
|
||
build working. It's intentional and should stay until explicitly removed.
|
||
|
||
- **NEVER modify `tools/tailwind/theme.css` without explicit instruction** — this file
|
||
contains the entire OKLCH-based design system. When a theme change is needed, the
|
||
file is replaced wholesale with a new theme generated from a shadcn theme generator.
|
||
Don't tweak individual values without being asked.
|
||
|
||
- **NEVER touch `packages/backend/convex/http.ts`** — this file only calls
|
||
`auth.addHttpRoutes(http)` and must stay exactly as it is for Convex Auth to work.
|
||
|
||
### Always do these:
|
||
|
||
- **ALWAYS use `bun typecheck` to validate your work**, never `bun build`. Typecheck
|
||
is fast and sufficient. Build is slow and for production deployments only.
|
||
|
||
- **ALWAYS use `const` arrow functions over `function` declarations** — this is a
|
||
strong style preference throughout the codebase:
|
||
|
||
```typescript
|
||
// ✅ Correct
|
||
const handleSubmit = async (data: FormData) => { ... };
|
||
const MyComponent = () => { ... };
|
||
|
||
// ❌ Wrong
|
||
function handleSubmit(data: FormData) { ... }
|
||
function MyComponent() { ... }
|
||
```
|
||
|
||
Note: exported Convex functions (`query`, `mutation`, `action`) are already defined
|
||
using the Convex builder pattern and are fine as-is.
|
||
|
||
- **ALWAYS import env vars from `@/env`**, never from `process.env` directly. The
|
||
ESLint `restrictEnvAccess` rule will error if you use `process.env` anywhere outside
|
||
of `apps/next/src/env.ts`.
|
||
|
||
- **ALWAYS add `.js` extensions** when importing from Convex-generated files:
|
||
|
||
```typescript
|
||
// ✅ Correct
|
||
import type { Id } from '@gib/backend/convex/_generated/dataModel.js';
|
||
// ❌ Wrong — will fail at runtime
|
||
import { api } from '@gib/backend/convex/_generated/api';
|
||
import { api } from '@gib/backend/convex/_generated/api.js';
|
||
```
|
||
|
||
The project uses `"type": "module"` in all `package.json` files, which requires
|
||
explicit file extensions for ESM imports. The `.js` extension is required even
|
||
though the source files are `.ts`.
|
||
|
||
---
|
||
|
||
## 3. Monorepo Architecture Overview
|
||
|
||
This is a Turborepo monorepo with three top-level categories of packages:
|
||
|
||
```
|
||
convex-monorepo/
|
||
├── apps/
|
||
│ ├── next/ # @gib/next — Next.js 16 web application
|
||
│ └── expo/ # @gib/expo — Expo 54 mobile app (WIP, broken)
|
||
├── packages/
|
||
│ ├── backend/ # @gib/backend — Self-hosted Convex backend
|
||
│ └── ui/ # @gib/ui — Shared shadcn/ui component library
|
||
├── tools/
|
||
│ ├── eslint/ # @gib/eslint-config — ESLint configuration
|
||
│ ├── prettier/ # @gib/prettier-config — Prettier + import sorting
|
||
│ ├── tailwind/ # @gib/tailwind-config — Tailwind v4 + theme
|
||
│ └── typescript/ # @gib/tsconfig — Shared TypeScript configs
|
||
├── docker/ # Docker Compose for self-hosted deployment
|
||
├── turbo/ # Turborepo generators (scaffolding)
|
||
├── turbo.json # Turborepo pipeline configuration
|
||
├── package.json # Root workspace + dependency catalogs
|
||
└── .env # Central environment variables (gitignored)
|
||
```
|
||
|
||
### How the pieces connect
|
||
|
||
The dependency graph flows like this:
|
||
|
||
```
|
||
@gib/tsconfig
|
||
└── @gib/eslint-config
|
||
└── @gib/tailwind-config
|
||
└── @gib/prettier-config
|
||
└── @gib/ui (components + shared styles)
|
||
└── @gib/backend (Convex functions + types)
|
||
└── @gib/next (consumes everything)
|
||
```
|
||
|
||
`tools/` packages are pure configuration — they export ESLint rules, TypeScript
|
||
compiler options, Tailwind PostCSS config, and Prettier options. Every app and package
|
||
extends them; you should rarely need to modify them.
|
||
|
||
`packages/ui` contains every shadcn/ui component plus a few custom additions.
|
||
Everything is re-exported from a single `@gib/ui` barrel — no package-specific deep
|
||
imports needed.
|
||
|
||
`packages/backend` is the Convex backend. Only the `convex/` subdirectory is synced
|
||
to the Convex deployment. The `scripts/` and `types/` directories sit outside
|
||
`convex/` intentionally so they're not uploaded to Convex cloud.
|
||
|
||
### Turborepo pipeline
|
||
|
||
`turbo.json` defines the task pipeline. Key points for agents:
|
||
|
||
- **`globalEnv`** — all environment variables that affect builds must be listed here,
|
||
or Turborepo won't include them in cache keys and cached outputs may be stale.
|
||
Whenever you add a new env var, add it to `globalEnv`.
|
||
- **`globalDependencies`** — `**/.env.*local` is tracked, so changes to
|
||
`.env.local` files invalidate the cache.
|
||
- The `build` task runs `^build` (dependencies first), then emits to `dist/`.
|
||
- The `dev` task is not cached and not persistent (`persistent: false` means
|
||
Turborepo considers it non-blocking).
|
||
- `typecheck` and `lint` both depend on `^topo` and `^build`, meaning they wait for
|
||
dependency packages to build first.
|
||
|
||
---
|
||
|
||
## 4. Self-Hosted Stack & Runtime Requirements
|
||
|
||
### 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 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`)
|
||
|
||
### The `packages/backend/.env.local` file
|
||
|
||
You may notice a `.env.local` file in `packages/backend/`. This file is
|
||
auto-generated by the `convex dev` command and is not something agents need to create
|
||
or manage. It contains `CONVEX_URL` and `CONVEX_SITE_URL` values that Convex uses
|
||
internally when running `bun dev:backend`. It is gitignored and can be safely ignored.
|
||
|
||
### `CONVEX_SITE_URL` — a nuance worth understanding
|
||
|
||
`CONVEX_SITE_URL` is used by `packages/backend/convex/auth.config.ts` as the CORS
|
||
domain that Convex Auth trusts for token exchange. For **local development**, this
|
||
should always be `http://localhost:3000` (the Next.js dev server). For **production**,
|
||
this needs to match your public domain — but this value must be updated directly in
|
||
the **Convex Dashboard** (environment variables section), not just in the root `.env`.
|
||
|
||
The value in `.env` is always `http://localhost:3000` for dev. When you need Convex
|
||
Auth to work on your production domain, go to the Convex Dashboard at
|
||
`https://dashboard.convexmonorepo.gbrown.org` and update `CONVEX_SITE_URL` there.
|
||
|
||
---
|
||
|
||
## 5. Environment Variables — Complete Reference
|
||
|
||
### The single source of truth: `/.env`
|
||
|
||
All environment variables live in the root `.env` file. Every app and package loads
|
||
them via `dotenv-cli` using the `with-env` script pattern:
|
||
|
||
```json
|
||
"with-env": "dotenv -e ../../.env --"
|
||
```
|
||
|
||
The root `.env` is gitignored. The root `.env.example` is the committed template that
|
||
shows all required variable names without real values. **Keep `.env.example` up to
|
||
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 |
|
||
|
||
### Typesafe env vars — `apps/next/src/env.ts`
|
||
|
||
The Next.js app uses `@t3-oss/env-nextjs` with Zod to validate all environment
|
||
variables at build time. This file is the contract between the `.env` file and the
|
||
application code.
|
||
|
||
The ESLint `restrictEnvAccess` rule in `@gib/eslint-config` blocks direct
|
||
`process.env` access everywhere except in `env.ts`. All app code must import the
|
||
validated `env` object:
|
||
|
||
```typescript
|
||
import { env } from '@/env';
|
||
|
||
// ✅ Correct — validated and typed
|
||
const url = env.NEXT_PUBLIC_CONVEX_URL;
|
||
|
||
// ❌ Will trigger ESLint error
|
||
const url = process.env.NEXT_PUBLIC_CONVEX_URL;
|
||
```
|
||
|
||
### Adding a new environment variable — the 4-step checklist
|
||
|
||
Every time you introduce a new env var, all four of these steps are required:
|
||
|
||
**Step 1: Add to `/.env`**
|
||
|
||
```bash
|
||
MY_NEW_VAR=value
|
||
```
|
||
|
||
**Step 2: Add to `apps/next/src/env.ts`**
|
||
|
||
```typescript
|
||
// In the server{} block for server-only vars:
|
||
MY_NEW_VAR: z.string(),
|
||
|
||
// In the client{} block for NEXT_PUBLIC_ vars:
|
||
NEXT_PUBLIC_MY_VAR: z.url(),
|
||
|
||
// And in the runtimeEnv{} block:
|
||
MY_NEW_VAR: process.env.MY_NEW_VAR,
|
||
```
|
||
|
||
**Step 3: Add to `turbo.json` `globalEnv` array**
|
||
|
||
```json
|
||
"globalEnv": [
|
||
...,
|
||
"MY_NEW_VAR"
|
||
]
|
||
```
|
||
|
||
Skipping this means Turborepo won't bust its cache when the variable changes.
|
||
|
||
**Step 4 (if the backend needs it): Sync to Convex**
|
||
|
||
```bash
|
||
# From packages/backend/
|
||
bun with-env npx convex env set MY_NEW_VAR "value"
|
||
```
|
||
|
||
Or set it directly in the Convex Dashboard.
|
||
|
||
Also update `/.env.example` with the new variable (empty value or a placeholder).
|
||
|
||
---
|
||
|
||
## 6. Dependency Management — The Catalog System
|
||
|
||
### What catalogs are
|
||
|
||
The root `package.json` uses Bun workspaces with a catalog system — a single source
|
||
of truth for shared dependency versions. When a package declares `"react": "catalog:react19"`,
|
||
it means "use whatever version is defined in the `react19` catalog in the root
|
||
`package.json`". This prevents version drift across packages.
|
||
|
||
The current catalogs:
|
||
|
||
```json
|
||
"catalog": {
|
||
"@eslint/js": "^9.38.0",
|
||
"@tailwindcss/postcss": "^4.1.16",
|
||
"@types/node": "^22.19.15",
|
||
"eslint": "^9.39.4",
|
||
"prettier": "^3.8.1",
|
||
"tailwindcss": "^4.1.16",
|
||
"typescript": "^5.9.3",
|
||
"zod": "^4.3.6"
|
||
},
|
||
"catalogs": {
|
||
"convex": {
|
||
"convex": "^1.33.1",
|
||
"@convex-dev/auth": "^0.0.81"
|
||
},
|
||
"react19": {
|
||
"@types/react": "~19.1.0",
|
||
"@types/react-dom": "~19.1.0",
|
||
"react": "19.1.4",
|
||
"react-dom": "19.1.4"
|
||
}
|
||
}
|
||
```
|
||
|
||
Usage in package `package.json` files:
|
||
|
||
```json
|
||
"dependencies": {
|
||
"convex": "catalog:convex",
|
||
"react": "catalog:react19",
|
||
"typescript": "catalog:",
|
||
"zod": "catalog:"
|
||
},
|
||
"devDependencies": {
|
||
"@gib/eslint-config": "workspace:*",
|
||
"@gib/ui": "workspace:*"
|
||
}
|
||
```
|
||
|
||
The `catalog:` syntax (no name) refers to the default catalog. `catalog:convex` and
|
||
`catalog:react19` refer to named catalogs.
|
||
|
||
### The update regression — how to safely update dependencies
|
||
|
||
**Do NOT run `bun update` directly inside individual package directories.** This is a
|
||
known issue: running `bun update` in a package directory will replace `catalog:`
|
||
references with explicit hardcoded version strings, breaking the single-source-of-truth
|
||
system.
|
||
|
||
The correct dependency update workflow:
|
||
|
||
1. Edit the version in the **root `package.json`** catalog section
|
||
2. Run `bun install` from the root
|
||
3. Verify with `bun lint:ws` (which runs `sherif` to check workspace consistency)
|
||
|
||
### sherif runs on `postinstall`
|
||
|
||
The root `package.json` has `"postinstall": "sherif"`. This means every time you run
|
||
`bun install`, `sherif` runs automatically and will **fail the install** if any
|
||
`catalog:` references are broken or if package versions are inconsistent. This is a
|
||
feature, not a bug — it catches the regression described above immediately.
|
||
|
||
If you see `sherif` errors after running `bun install`, it usually means a
|
||
`catalog:` reference was accidentally replaced with a hardcoded version. Fix it by
|
||
restoring the `catalog:` syntax in the affected `package.json`.
|
||
|
||
### Adding new packages to the monorepo
|
||
|
||
Use the Turborepo generator to scaffold a new package:
|
||
|
||
```bash
|
||
bun turbo gen init
|
||
```
|
||
|
||
This will prompt you for:
|
||
|
||
1. **Package name** — enter with or without the `@gib/` prefix (it's stripped automatically)
|
||
2. **Dependencies** — space-separated list of npm packages to install
|
||
|
||
The generator creates:
|
||
|
||
- `packages/<name>/eslint.config.ts`
|
||
- `packages/<name>/package.json` (with workspace devDeps and latest versions for any
|
||
specified deps)
|
||
- `packages/<name>/tsconfig.json`
|
||
- `packages/<name>/src/index.ts`
|
||
|
||
After scaffolding it runs `bun install` and formats the new files.
|
||
|
||
---
|
||
|
||
## 7. Convex Backend Deep Dive
|
||
|
||
### Package structure
|
||
|
||
```
|
||
packages/backend/
|
||
├── convex/ # ← Only this directory is synced to Convex
|
||
│ ├── _generated/ # Auto-generated — NEVER edit
|
||
│ ├── custom/
|
||
│ │ └── auth/
|
||
│ │ ├── index.ts # Barrel: Password + validatePassword exports
|
||
│ │ └── providers/
|
||
│ │ ├── password.ts # Custom Password provider
|
||
│ │ └── usesend.ts # UseSend email OTP provider
|
||
│ ├── auth.config.ts # CORS domain configuration
|
||
│ ├── auth.ts # Auth setup + all user-related functions
|
||
│ ├── crons.ts # Cron jobs (commented examples)
|
||
│ ├── files.ts # File upload/storage
|
||
│ ├── http.ts # HTTP routes (auth only) — DO NOT TOUCH
|
||
│ ├── schema.ts # Database schema — the source of truth
|
||
│ └── tsconfig.json # Convex-required TS config
|
||
├── scripts/
|
||
│ └── generateKeys.mjs # RS256 JWT keypair generator for Convex Auth
|
||
└── types/
|
||
├── auth.ts # Password validation constants
|
||
└── index.ts # Barrel export
|
||
```
|
||
|
||
**Important:** The backend package has no root `tsconfig.json` — only
|
||
`convex/tsconfig.json`. This is intentional and follows Convex's recommended
|
||
structure. When `bun typecheck` runs from the repo root, it will print TypeScript help
|
||
output for `@gib/backend` (because there's no valid project to check). This is
|
||
expected and not an error.
|
||
|
||
### Schema design — always extend `users` directly
|
||
|
||
The preferred and enforced approach in this repo is to **extend the `users` table
|
||
directly** for all application data. Do not create a separate `profiles` table.
|
||
|
||
The Convex Auth library (`@convex-dev/auth`) provides base fields via `authTables`
|
||
(name, image, email, emailVerificationTime, phone, phoneVerificationTime, isAnonymous).
|
||
Custom fields are added directly to the `users` table definition in `schema.ts`:
|
||
|
||
```typescript
|
||
// packages/backend/convex/schema.ts
|
||
const applicationTables = {
|
||
users: defineTable({
|
||
name: v.optional(v.string()),
|
||
image: v.optional(v.string()), // stores Convex storage ID as string
|
||
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()),
|
||
/* Custom fields below */
|
||
themePreference: v.optional(
|
||
v.union(v.literal('light'), v.literal('dark'), v.literal('system')),
|
||
),
|
||
})
|
||
.index('email', ['email'])
|
||
.index('phone', ['phone'])
|
||
.index('name', ['name']),
|
||
};
|
||
|
||
export default defineSchema({
|
||
...authTables,
|
||
...applicationTables,
|
||
});
|
||
```
|
||
|
||
The `themePreference` field demonstrates the pattern. When your app needs to store
|
||
user-specific data (preferences, profile info, settings), add a field here.
|
||
|
||
**Note on `image`:** The `image` field is typed as `v.string()` because `authTables`
|
||
defines it that way. In practice it stores a Convex storage ID (which is a string at
|
||
runtime). The `updateUser` mutation accepts `v.id('_storage')` and casts it when
|
||
writing. This is a known, harmless type mismatch — leave it as `v.string()` in the
|
||
schema for now.
|
||
|
||
### Writing backend functions
|
||
|
||
The three Convex function types:
|
||
|
||
- **`query`** — read-only, reactive, called from client with `useQuery` or server with
|
||
`preloadQuery`. Always check auth if the data should be protected.
|
||
- **`mutation`** — write operations (database inserts, patches, deletes). Runs
|
||
transactionally. Cannot make external HTTP calls.
|
||
- **`action`** — for side effects: calling external APIs, Convex storage operations,
|
||
or certain `@convex-dev/auth` operations. Can call `ctx.runQuery` and
|
||
`ctx.runMutation` internally.
|
||
|
||
Always authenticate inside protected functions:
|
||
|
||
```typescript
|
||
import { getAuthUserId } from '@convex-dev/auth/server';
|
||
import { ConvexError, v } from 'convex/values';
|
||
|
||
import { mutation, query } from './_generated/server';
|
||
|
||
export const getMyData = query({
|
||
args: {},
|
||
handler: async (ctx) => {
|
||
const userId = await getAuthUserId(ctx);
|
||
if (!userId) return null; // or throw if auth is truly required
|
||
return ctx.db.get(userId);
|
||
},
|
||
});
|
||
|
||
export const updateMyData = mutation({
|
||
args: { value: v.string() },
|
||
handler: async (ctx, args) => {
|
||
const userId = await getAuthUserId(ctx);
|
||
if (!userId) throw new ConvexError('Not authenticated.');
|
||
await ctx.db.patch(userId, { someField: args.value });
|
||
},
|
||
});
|
||
```
|
||
|
||
### Why `updateUserPassword` is an `action`, not a `mutation`
|
||
|
||
`@convex-dev/auth`'s `modifyAccountCredentials` function makes internal HTTP calls
|
||
under the hood. Convex mutations cannot make external/internal HTTP calls — only
|
||
actions can. That's why any password change operation must be an `action`:
|
||
|
||
```typescript
|
||
export const updateUserPassword = action({
|
||
args: { currentPassword: v.string(), newPassword: v.string() },
|
||
handler: async (ctx, { currentPassword, newPassword }) => {
|
||
const userId = await getAuthUserId(ctx);
|
||
if (!userId) throw new ConvexError('Not authenticated.');
|
||
// retrieveAccount and modifyAccountCredentials both require action context
|
||
const verified = await retrieveAccount(ctx, {
|
||
provider: 'password',
|
||
account: { id: user.email, secret: currentPassword },
|
||
});
|
||
if (!verified) throw new ConvexError('Current password is incorrect.');
|
||
await modifyAccountCredentials(ctx, {
|
||
provider: 'password',
|
||
account: { id: user.email, secret: newPassword },
|
||
});
|
||
},
|
||
});
|
||
```
|
||
|
||
Similarly, any auth function that needs to call into Convex Auth internals
|
||
(`retrieveAccount`, `modifyAccountCredentials`, `createAccount`, etc.) must be an
|
||
action.
|
||
|
||
### Authentication setup
|
||
|
||
```typescript
|
||
// convex/auth.ts
|
||
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
|
||
providers: [Authentik({ allowDangerousEmailAccountLinking: true }), Password],
|
||
});
|
||
```
|
||
|
||
Two providers are configured:
|
||
|
||
1. **Authentik** — OAuth SSO. The `allowDangerousEmailAccountLinking: true` flag
|
||
allows users who previously registered with the Password provider to link their
|
||
Authentik account if it shares the same email address.
|
||
|
||
2. **Password** — custom provider in `convex/custom/auth/providers/password.ts`.
|
||
Uses `UseSendOTP` for email verification during sign-up and `UseSendOTPPasswordReset`
|
||
for password reset flows.
|
||
|
||
### The UseSend email provider
|
||
|
||
`convex/custom/auth/providers/usesend.ts` is the custom email OTP provider. It reads
|
||
configuration from environment variables:
|
||
|
||
- `USESEND_API_KEY` — the API key for your UseSend instance
|
||
- `USESEND_URL` — the base URL of your self-hosted UseSend instance
|
||
- `USESEND_FROM_EMAIL` — the from address for emails (e.g. `My App <noreply@example.com>`)
|
||
|
||
The provider creates two exports used in `password.ts`:
|
||
|
||
- `UseSendOTP` — 20-minute OTP for sign-up email verification
|
||
- `UseSendOTPPasswordReset` — 1-hour OTP for password reset
|
||
|
||
Email templates are currently inline HTML strings in `usesend.ts`. The
|
||
`@react-email/components` and `react-email` packages are in `package.json` as planned
|
||
dependencies — React Email templates will eventually replace the inline HTML.
|
||
|
||
### File uploads
|
||
|
||
`convex/files.ts` provides two functions:
|
||
|
||
```typescript
|
||
// Get a signed upload URL (call this first)
|
||
const uploadUrl = await generateUploadUrl();
|
||
|
||
// POST your file to the URL, get back a storage ID
|
||
// Then store the ID:
|
||
await updateUser({ image: storageId });
|
||
|
||
// Get a temporary signed URL to display the image:
|
||
const imageUrl = await getImageUrl({ storageId });
|
||
```
|
||
|
||
The profile avatar upload in `apps/next/src/components/layout/auth/profile/avatar-upload.tsx`
|
||
demonstrates the complete flow: crop the image client-side → get an upload URL →
|
||
POST to Convex storage → save the storage ID to the user record.
|
||
|
||
### Cron jobs
|
||
|
||
`convex/crons.ts` is the place for scheduled jobs. The file currently contains
|
||
commented-out examples. When you need to add a cron job, add it to this file following
|
||
the pattern in the comments.
|
||
|
||
### Generating JWT keys for Convex Auth
|
||
|
||
When setting up a new deployment, you need to generate RS256 JWT keys for Convex Auth
|
||
to work:
|
||
|
||
```bash
|
||
# From packages/backend/
|
||
bun run scripts/generateKeys.mjs
|
||
```
|
||
|
||
This outputs `JWT_PRIVATE_KEY` and `JWKS` values that must be synced to Convex:
|
||
|
||
```bash
|
||
bun with-env npx convex env set JWT_PRIVATE_KEY "..."
|
||
bun with-env npx convex env set JWKS "..."
|
||
```
|
||
|
||
---
|
||
|
||
## 8. Next.js App Deep Dive
|
||
|
||
### 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 |
|
||
|
||
### 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):
|
||
|
||
| 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 |
|
||
|
||
The `/profile` route is 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',
|
||
'/dashboard',
|
||
'/settings',
|
||
]);
|
||
```
|
||
|
||
### 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
|
||
initial token passing. It has no visible UI.
|
||
|
||
**2. `ConvexClientProvider`** — a `'use client'` component in
|
||
`components/providers/ConvexClientProvider.tsx`. This wraps `ConvexAuthNextjsProvider`
|
||
and initializes the `ConvexReactClient`. All client-side reactive queries and
|
||
mutations flow through this.
|
||
|
||
```tsx
|
||
// app/layout.tsx (server)
|
||
<ConvexAuthNextjsServerProvider>
|
||
<html lang='en'>
|
||
<body>
|
||
<ConvexClientProvider>{children}</ConvexClientProvider>
|
||
</body>
|
||
</html>
|
||
</ConvexAuthNextjsServerProvider>
|
||
```
|
||
|
||
Removing either provider breaks auth. The server provider is needed for SSR token
|
||
hydration; the client provider is needed for reactive updates.
|
||
|
||
### SSR data preloading — the preferred pattern for new pages
|
||
|
||
For any page that needs Convex data, use `preloadQuery` on the server and
|
||
`usePreloadedQuery` on the client. This avoids a loading flash and gives you
|
||
SSR-rendered content.
|
||
|
||
**Server component (the page file):**
|
||
|
||
```tsx
|
||
// app/(auth)/some-page/page.tsx
|
||
import { SomeComponent } from '@/components/some-component';
|
||
import { preloadQuery } from 'convex/nextjs';
|
||
|
||
import { api } from '@gib/backend/convex/_generated/api.js';
|
||
|
||
const SomePage = async () => {
|
||
const preloadedData = await preloadQuery(api.someModule.someQuery);
|
||
|
||
return <SomeComponent preloadedData={preloadedData} />;
|
||
};
|
||
|
||
export default SomePage;
|
||
```
|
||
|
||
**Client component (the interactive part):**
|
||
|
||
```tsx
|
||
// components/some-component.tsx
|
||
'use client';
|
||
|
||
import type { Preloaded } from 'convex/react';
|
||
import { usePreloadedQuery } from 'convex/react';
|
||
|
||
import type { api } from '@gib/backend/convex/_generated/api.js';
|
||
|
||
interface Props {
|
||
preloadedData: Preloaded<typeof api.someModule.someQuery>;
|
||
}
|
||
|
||
export const SomeComponent = ({ preloadedData }: Props) => {
|
||
const data = usePreloadedQuery(preloadedData);
|
||
// data is immediately available with SSR value, then stays reactive
|
||
return <div>{data.name}</div>;
|
||
};
|
||
```
|
||
|
||
This is the pattern used on the `/profile` page and should be used for all new
|
||
data-fetching pages.
|
||
|
||
### Auth-conditional UI — always use a loading skeleton
|
||
|
||
Any component that renders differently based on whether the user is authenticated
|
||
**must** show a loading skeleton while auth resolves. Never render a "signed in" state
|
||
optimistically or flash from "not logged in" to "logged in".
|
||
|
||
The `AvatarDropdown` component in
|
||
`src/components/layout/header/controls/AvatarDropdown.tsx` is the canonical example:
|
||
|
||
```tsx
|
||
'use client';
|
||
|
||
import { useAuthActions } from '@convex-dev/auth/react';
|
||
import { useConvexAuth } from 'convex/react';
|
||
|
||
export const AvatarDropdown = () => {
|
||
const { isLoading, isAuthenticated } = useConvexAuth();
|
||
|
||
if (isLoading) {
|
||
// Always show a skeleton while auth state is resolving
|
||
return <Skeleton className='h-9 w-9 rounded-full' />;
|
||
}
|
||
|
||
if (!isAuthenticated) {
|
||
return <SignInButton />;
|
||
}
|
||
|
||
return <UserAvatarWithDropdown />;
|
||
};
|
||
```
|
||
|
||
### `src/proxy.ts` — route protection and IP banning
|
||
|
||
This file is the Next.js proxy/middleware. It does two things in sequence:
|
||
|
||
1. **IP banning** — `banSuspiciousIPs(request)` runs first. It checks the incoming
|
||
request against known malicious patterns (path traversal, PHP probes, etc.) and
|
||
rate-limits suspicious IPs. If a ban is triggered, a 403 is returned immediately
|
||
before auth runs.
|
||
|
||
2. **Auth routing** — `convexAuthNextjsMiddleware` handles authentication:
|
||
- Unauthenticated users hitting `/profile` are redirected to `/sign-in`
|
||
- Authenticated users hitting `/sign-in` are redirected to `/`
|
||
|
||
Session cookies are configured with a 30-day max age.
|
||
|
||
### The IP banning system (`src/lib/proxy/ban-sus-ips.ts`)
|
||
|
||
This is an in-memory security layer. Key behaviors:
|
||
|
||
- Tracks request counts per IP using an in-memory Map
|
||
- 10 requests matching malicious patterns within 1 minute → 30-minute ban
|
||
- 10 404 responses within 2 minutes → ban
|
||
- Detects and bans suspicious HTTP methods (TRACE, PUT, DELETE, PATCH)
|
||
- The ban response message is deliberately direct — consider softening it for
|
||
professional/client-facing deployments
|
||
- **Bans reset on server restart** (in-memory). This is a known limitation; a future
|
||
enhancement would persist bans to Convex or Redis.
|
||
|
||
Agents can extend the suspicious patterns by adding to the arrays at the top of the
|
||
file if new attack vectors are observed.
|
||
|
||
IP detection order: `x-forwarded-for` → `x-real-ip` → `cf-connecting-ip` → host.
|
||
This handles the nginx-proxy-manager reverse proxy setup correctly.
|
||
|
||
### `next.config.js` — key behaviors
|
||
|
||
- **`output: 'standalone'`** — generates a self-contained build for Docker deployment
|
||
- **`typescript.ignoreBuildErrors: true`** — suppresses TypeScript errors during build
|
||
(shadcn components can produce spurious errors)
|
||
- **`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
|
||
- **`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`
|
||
|
||
### Fonts
|
||
|
||
Fonts are configured in `apps/next/src/app/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)
|
||
|
||
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`.
|
||
|
||
### Analytics and monitoring
|
||
|
||
**Plausible** is injected via `next-plausible` and proxied through the Next.js app
|
||
itself via `withPlausibleProxy` in `next.config.js`. This means analytics requests go
|
||
to `/` (the Next.js server) first, which then forwards them to the Plausible instance.
|
||
Ad blockers that block `plausible.io` directly cannot block this setup.
|
||
|
||
**Sentry** is configured with `tracesSampleRate: 1.0` (100% trace sampling) on both
|
||
client and server. This is appropriate for development and low-traffic deployments. For
|
||
high-traffic production sites, reduce this to `0.1`–`0.2` in
|
||
`instrumentation-client.ts` and `sentry.server.config.ts`.
|
||
|
||
---
|
||
|
||
## 9. UI Package (`@gib/ui`)
|
||
|
||
### The single-import rule
|
||
|
||
**Everything** from the UI package is importable from the single `@gib/ui` entry
|
||
point. Never import from specific component file paths:
|
||
|
||
```typescript
|
||
// ✅ Correct — single import from @gib/ui
|
||
import { Button, Card, CardContent, cn, useIsMobile } from '@gib/ui';
|
||
// ❌ Wrong — never import from subpaths like this
|
||
import { Button } from '@gib/ui/src/button';
|
||
```
|
||
|
||
This includes utility functions (`cn`, `ccn`), all components, and all hooks.
|
||
|
||
### Available components
|
||
|
||
The UI package contains the complete shadcn/ui component set (new-york style) plus
|
||
custom additions. Key custom components:
|
||
|
||
- **`BasedAvatar`** — shows user initials or a user icon when no image is set; use
|
||
this instead of the base `Avatar` for user avatars
|
||
- **`BasedProgress`** — progress bar variant
|
||
- **`ImageCrop`** — image cropping UI built on `react-image-crop`; used in the profile
|
||
avatar upload flow
|
||
- **`SubmitButton`** — button with built-in loading state for form submissions
|
||
- **`StatusMessage`** — standardized success/error message display
|
||
- **`Spinner`** — loading spinner
|
||
|
||
Full component list: Accordion, Alert, AlertDialog, AspectRatio, Avatar, Badge,
|
||
BasedAvatar, BasedProgress, Breadcrumb, Button, ButtonGroup, Calendar, Card, Carousel,
|
||
Chart, Checkbox, Collapsible, Combobox, Command, ContextMenu, Dialog, Drawer,
|
||
DropdownMenu, Empty, Field, Form, HoverCard, ImageCrop, Input, InputGroup, InputOTP,
|
||
Item, Kbd, Label, NavigationMenu, NativeSelect, Pagination, Popover, Progress,
|
||
RadioGroup, Resizable, ScrollArea, Select, Separator, Sheet, Sidebar, Skeleton, Slider,
|
||
Sonner (Toaster), Spinner, StatusMessage, SubmitButton, Switch, Table, Tabs, Textarea,
|
||
ThemeProvider, ThemeToggle, Toggle, ToggleGroup, Tooltip.
|
||
|
||
### Available hooks (also from `@gib/ui`)
|
||
|
||
- `useIsMobile` — returns `true` when viewport width is below 768px
|
||
- `useOnClickOutside` — fires a callback when a click occurs outside a ref element
|
||
|
||
### Adding a new component via `bun ui-add`
|
||
|
||
```bash
|
||
# From the repo root
|
||
bun ui-add
|
||
```
|
||
|
||
This runs `bunx shadcn@latest add` interactively. After adding a new component:
|
||
|
||
1. Any internal imports the component makes (to other shadcn components, `cn`, hooks)
|
||
**must be updated** to come from `@gib/ui` instead of relative paths
|
||
2. Export the new component(s) from `packages/ui/src/index.tsx`
|
||
|
||
If you add a new hook, add it to `packages/ui/src/hooks/` and export it from both
|
||
`packages/ui/src/hooks/index.tsx` and the main `packages/ui/src/index.tsx`.
|
||
|
||
### `components.json` note
|
||
|
||
The shadcn `components.json` file specifies `style: "new-york"` and `baseColor: "zinc"`.
|
||
The `zinc` base color is irrelevant — the actual visual theme is entirely determined by
|
||
`tools/tailwind/theme.css` which overrides all color tokens with OKLCH values. The
|
||
`zinc` setting only affects how `bunx shadcn` generates new component defaults; the
|
||
runtime appearance is controlled by the theme.
|
||
|
||
---
|
||
|
||
## 10. Tools — Shared Configuration Packages
|
||
|
||
### `@gib/eslint-config`
|
||
|
||
Three exported configs used across the monorepo:
|
||
|
||
- **`baseConfig`** — TypeScript-aware ESLint with `recommended`, `recommendedTypeChecked`,
|
||
and `stylisticTypeChecked` rules. Used by all packages.
|
||
- **`reactConfig`** — adds `eslint-plugin-react` and `react-hooks` rules.
|
||
- **`nextjsConfig`** — adds `@next/eslint-plugin-next` rules including core-web-vitals.
|
||
- **`restrictEnvAccess`** — blocks direct `process.env` access outside `env.ts`.
|
||
|
||
Key enforced rules:
|
||
|
||
- `@typescript-eslint/consistent-type-imports` — type imports must be separate (`import type`)
|
||
- `@typescript-eslint/no-non-null-assertion` — non-null assertions (`!`) are errors
|
||
- `@typescript-eslint/no-unused-vars` — unused vars are warnings; prefix with `_` to suppress
|
||
- `import/consistent-type-specifier-style` — prefer top-level `import type` over inline `type`
|
||
|
||
Note: the `consistent-type-imports` enforcement is a known item to potentially relax
|
||
in the future (see Known Issues).
|
||
|
||
### `@gib/prettier-config`
|
||
|
||
Key settings:
|
||
|
||
- `singleQuote: true`, `jsxSingleQuote: true` — single quotes throughout
|
||
- `trailingComma: 'all'` — trailing commas on all multi-line structures
|
||
- `printWidth: 80`, `tabWidth: 2`
|
||
- `tailwindFunctions: ['cn', 'cva']` — Tailwind class sorting in these functions
|
||
|
||
Import order (enforced automatically by `@ianvs/prettier-plugin-sort-imports`):
|
||
|
||
```
|
||
1. Type imports (all)
|
||
2. react / react-native
|
||
3. next / expo
|
||
4. Third-party packages
|
||
5. @gib/* type imports, then @gib/* value imports
|
||
6. Local type imports, then local relative imports
|
||
```
|
||
|
||
You do not need to manually order imports — Prettier handles it on format. Run
|
||
`bun format:fix` to apply.
|
||
|
||
### `@gib/tailwind-config`
|
||
|
||
- **`theme.css`** — the complete OKLCH-based design system. Light mode: warm cream
|
||
background with reddish-brown primary. Dark mode: warm dark background with
|
||
orange-warm primary. Do not modify without explicit instruction. When the theme
|
||
needs to change, the entire file is replaced with a new theme generated from a
|
||
shadcn theme generator.
|
||
- **`postcss-config.js`** — re-exports `{ plugins: { '@tailwindcss/postcss': {} } }`.
|
||
Used by all apps via `postcss.config.js`.
|
||
|
||
### `@gib/tsconfig`
|
||
|
||
- **`base.json`** — strict TypeScript config for all packages. Notable settings:
|
||
`noUncheckedIndexedAccess: true` (array/object access returns `T | undefined`),
|
||
`moduleResolution: "Bundler"`, `module: "Preserve"`, `isolatedModules: true`.
|
||
Uses `tsBuildInfoFile` for incremental caching in `.cache/`.
|
||
- **`compiled-package.json`** — extends base with declaration emit settings for
|
||
packages that need to output `.d.ts` files.
|
||
|
||
---
|
||
|
||
## 11. Docker & Deployment
|
||
|
||
### Container overview
|
||
|
||
Three Docker services are defined in `docker/compose.yml`:
|
||
|
||
**`convex-backend`** (`ghcr.io/get-convex/convex-backend`)
|
||
|
||
- The self-hosted Convex runtime
|
||
- Data persists in `docker/data/` (gitignored, must be backed up)
|
||
- Health check via `curl http://localhost:3210/version`
|
||
- Watchtower-enabled for automatic image updates
|
||
- `stop_signal: SIGINT` + 10s grace period for clean shutdown
|
||
|
||
**`convex-dashboard`** (`ghcr.io/get-convex/convex-dashboard`)
|
||
|
||
- Admin UI for the Convex backend
|
||
- Starts only after `convex-backend` is healthy
|
||
|
||
**`next-app`** (built from `docker/Dockerfile`)
|
||
|
||
- Multi-stage build: `oven/bun:alpine` builder → `node:20-alpine` runner
|
||
- Standalone Next.js output (`CMD ["node", "apps/next/server.js"]`)
|
||
- Reverse-proxied by nginx — ports are not directly exposed
|
||
|
||
All services share the external `nginx-bridge` Docker network.
|
||
|
||
### Typical production deployment workflow
|
||
|
||
The Convex backend and dashboard are long-running and rarely need to be rebuilt.
|
||
The typical workflow when you've made changes to the Next.js app:
|
||
|
||
```bash
|
||
# On the production server (via SSH)
|
||
cd docker/
|
||
|
||
# Build the Next.js app with current source
|
||
sudo docker compose build next-app
|
||
|
||
# Once build succeeds, bring up the new container
|
||
sudo docker compose up -d next-app
|
||
```
|
||
|
||
If you need to start everything from scratch:
|
||
|
||
```bash
|
||
sudo docker compose up -d convex-backend convex-dashboard
|
||
# Wait for backend to be healthy, then:
|
||
sudo docker compose up -d next-app
|
||
```
|
||
|
||
### The `.dockerignore` situation (known issue)
|
||
|
||
There are two `.dockerignore` files:
|
||
|
||
- `/.dockerignore` (root) — has `.env` commented out, meaning the `.env` file IS
|
||
included in the Docker build context
|
||
- `/docker/.dockerignore` — explicitly excludes `.env`
|
||
|
||
The Docker build context is set to the repo root in `compose.yml`. The root
|
||
`.dockerignore` is the one that applies. Because `.env` is not excluded, it gets sent
|
||
to the builder and environment variables are available at build time. This is how
|
||
`SENTRY_AUTH_TOKEN` and other build-time vars reach the Next.js build step.
|
||
|
||
This is a known imperfect approach but it works. Do not "fix" it without understanding
|
||
the full implications — removing `.env` from the build context would break Sentry
|
||
source map uploads and `NEXT_PUBLIC_*` variable baking into the client bundle.
|
||
|
||
### Convex data persistence
|
||
|
||
`docker/data/` is mounted as a Docker volume and contains all Convex database data.
|
||
This directory is gitignored. Back it up before any server migrations, restarts, or
|
||
image updates.
|
||
|
||
### `generate_convex_admin_key`
|
||
|
||
A bash script in `docker/` that generates the admin key from a running Convex backend:
|
||
|
||
```bash
|
||
cd docker/
|
||
./generate_convex_admin_key
|
||
```
|
||
|
||
This runs the key generation script inside the `convex-backend` container and prints
|
||
the `CONVEX_SELF_HOSTED_ADMIN_KEY` value to stdout.
|
||
|
||
---
|
||
|
||
## 12. Code Style Reference
|
||
|
||
### Functions — always `const` arrow syntax
|
||
|
||
```typescript
|
||
// ✅ Correct — use const for all functions
|
||
const fetchUser = async (userId: string) => {
|
||
return await db.get(userId);
|
||
};
|
||
|
||
const MyComponent = ({ name }: { name: string }) => {
|
||
return <div>{name}</div>;
|
||
};
|
||
|
||
// ❌ Wrong — function declarations
|
||
function fetchUser(userId: string) { ... }
|
||
function MyComponent({ name }: { name: string }) { ... }
|
||
```
|
||
|
||
### Naming conventions
|
||
|
||
| Thing | Convention | Example |
|
||
| --------------------------- | ---------------------- | ------------------- |
|
||
| React components | `PascalCase` | `UserProfile.tsx` |
|
||
| Non-component files | `kebab-case.ts` | `ban-sus-ips.ts` |
|
||
| Functions and variables | `camelCase` | `const getUserById` |
|
||
| Constants | `SCREAMING_SNAKE_CASE` | `PASSWORD_MIN` |
|
||
| TypeScript types/interfaces | `PascalCase` | `type UserDoc` |
|
||
| Unused vars/params | prefix with `_` | `const _unused` |
|
||
|
||
### Imports
|
||
|
||
- Use `import type` for type-only imports (enforced by ESLint)
|
||
- Import order is handled automatically by Prettier on format — don't manually sort
|
||
- Always use `.js` extension for Convex-generated file imports
|
||
- All UI components and utilities come from `@gib/ui`
|
||
- All env vars come from `@/env`
|
||
|
||
### Error handling
|
||
|
||
```typescript
|
||
// Backend — throw ConvexError with a user-readable message
|
||
import { ConvexError } from 'convex/values';
|
||
|
||
throw new ConvexError('User not found.');
|
||
|
||
// Frontend — catch ConvexError and display with toast
|
||
try {
|
||
await someConvexMutation(args);
|
||
} catch (e) {
|
||
if (e instanceof ConvexError) {
|
||
toast.error(e.message);
|
||
}
|
||
}
|
||
```
|
||
|
||
### TypeScript
|
||
|
||
- Strict mode is on everywhere (`strict: true`, `noUncheckedIndexedAccess: true`)
|
||
- Array/object index access returns `T | undefined` — always handle the undefined case
|
||
- Avoid `any` — use `unknown` with type guards or proper generics
|
||
- Non-null assertions (`!`) are ESLint errors — prove your types instead
|
||
|
||
### Template customization points
|
||
|
||
When using this repo as a template for a **new project** (not working on the existing
|
||
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.
|
||
- 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
|
||
|
||
---
|
||
|
||
## 13. Adding New Features — Checklists
|
||
|
||
### Adding a new field to the users table
|
||
|
||
1. Add the field to `users` in `packages/backend/convex/schema.ts`
|
||
2. Add or update the relevant `mutation` in `packages/backend/convex/auth.ts`
|
||
3. Run `bun dev:backend` to let `convex dev` regenerate `_generated/` types
|
||
4. Add the corresponding UI in the profile page or wherever the field is managed
|
||
5. Update `packages/backend/types/auth.ts` if the field has shared validation constants
|
||
|
||
### Adding a new Convex function
|
||
|
||
1. Write the function in the appropriate `.ts` file in `packages/backend/convex/`
|
||
2. Run `bun dev:backend` — `convex dev --until-success` will push it and regenerate
|
||
`_generated/api.ts` with the new export
|
||
3. Import using `api.moduleName.functionName` with the `.js` extension:
|
||
```typescript
|
||
import { api } from '@gib/backend/convex/_generated/api.js';
|
||
```
|
||
|
||
### Adding a new page
|
||
|
||
1. Create `apps/next/src/app/<route>/page.tsx`
|
||
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>`:
|
||
```typescript
|
||
export const metadata = { title: 'Page Name' };
|
||
```
|
||
5. Run `bun typecheck` to validate
|
||
|
||
### Adding a new environment variable
|
||
|
||
See the 4-step checklist in [Section 5](#5-environment-variables--complete-reference).
|
||
|
||
### Adding a new UI component via shadcn
|
||
|
||
1. Run `bun ui-add` from the repo root
|
||
2. Select the component(s) to add
|
||
3. Update any internal imports in the new files to use `@gib/ui` instead of relative paths
|
||
4. Add exports to `packages/ui/src/index.tsx`
|
||
5. Run `bun typecheck` to validate
|
||
|
||
---
|
||
|
||
## 14. Expo App — Known Issues, Do Not Touch
|
||
|
||
The `apps/expo/` directory exists as a placeholder for future mobile development.
|
||
It is currently non-functional — it still contains the original T3 Turbo template
|
||
code and has not been migrated to use Convex or `@convex-dev/auth`.
|
||
|
||
**Do not work on the Expo app unless explicitly asked.** If you are asked to work on it,
|
||
be aware that the following are all broken and will need to be addressed:
|
||
|
||
| File | Issue |
|
||
| ----------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||
| `src/utils/api.tsx` | Imports `AppRouter` from `@acme/api` — package does not exist. Uses tRPC, which is not used anywhere in this repo. |
|
||
| `src/utils/auth.ts` | Uses `better-auth` / `@better-auth/expo` instead of `@convex-dev/auth` |
|
||
| `src/app/index.tsx` | Old T3 Turbo template code using tRPC mutations and better-auth session patterns |
|
||
| `src/app/post/[id].tsx` | Same — tRPC and better-auth patterns throughout |
|
||
| `postcss.config.js` | References `@acme/tailwind-config` — package does not exist |
|
||
| `src/styles.css` | Imports from `@acme/tailwind-config` — package does not exist |
|
||
| `eslint.config.mts` | Imports from `@acme/eslint-config` — package does not exist |
|
||
| `eas.json` | Specifies `pnpm 9.15.4` as the package manager — should be bun |
|
||
|
||
When the time comes to properly implement the Expo app, the right approach is to start
|
||
fresh with Convex + `@convex-dev/auth` patterns (mirroring how the Next.js app works),
|
||
not to try to incrementally fix the broken T3 Turbo code.
|
||
|
||
---
|
||
|
||
## 15. Known Issues & Technical Debt
|
||
|
||
A comprehensive list of known issues, organized by priority. Add new issues here as
|
||
they're discovered.
|
||
|
||
---
|
||
|
||
**1. Catalog update regression** ⚠️ High priority
|
||
|
||
Running `bun update` inside individual package directories replaces `catalog:`
|
||
references with hard-coded version strings, breaking the single-source-of-truth system.
|
||
The correct update workflow (edit root `package.json` catalog → `bun install` →
|
||
`bun lint:ws`) needs to be better enforced/documented. Investigate whether Bun has a
|
||
native fix for this or if a custom update script is needed.
|
||
|
||
---
|
||
|
||
**2. Password validation mismatch** ⚠️ Medium priority
|
||
|
||
`packages/backend/types/auth.ts` exports `PASSWORD_REGEX` which requires special
|
||
characters. `packages/backend/convex/custom/auth/providers/password.ts::validatePassword()`
|
||
does NOT check for special characters. These two are inconsistent. The frontend sign-in
|
||
form does enforce special characters via its own Zod schema, but the backend validation
|
||
is weaker than the type definition implies. Either align `validatePassword()` to match
|
||
`PASSWORD_REGEX`, or update `PASSWORD_REGEX` to not require special characters.
|
||
|
||
---
|
||
|
||
**3. `import type` enforcement removed** ✅ Fixed
|
||
|
||
The `@typescript-eslint/consistent-type-imports` and `import/consistent-type-specifier-style`
|
||
rules that forced separate `import type` statements have been removed from
|
||
`tools/eslint/base.ts`. Type imports can now be mixed inline or on separate lines —
|
||
whatever reads most naturally in context.
|
||
|
||
---
|
||
|
||
**4. `const` arrow function lint rule added** ✅ Fixed
|
||
|
||
`eslint-plugin-prefer-arrow-functions` has been added to `tools/eslint/base.ts` and the
|
||
`prefer-arrow-functions/prefer-arrow-functions` rule is now enforced as a warning. All
|
||
function declarations across the codebase have been auto-converted to `const` arrow
|
||
function syntax.
|
||
|
||
---
|
||
|
||
**5. Turbo generator uses pnpm** ✅ Fixed in this session
|
||
|
||
`turbo/generators/config.ts` previously called `pnpm i` and `pnpm prettier`. This has
|
||
been fixed to use `bun install` and `bun prettier`.
|
||
|
||
---
|
||
|
||
**6. Root `.dockerignore` includes `.env` in build context** ℹ️ Known, works intentionally
|
||
|
||
The root `.dockerignore` has `.env` commented out, meaning the `.env` file is sent to
|
||
the Docker build context. This is how build-time env vars (Sentry token, `NEXT_PUBLIC_*`)
|
||
reach the Next.js build step. It's a known imperfect approach but fully functional.
|
||
A proper solution would use Docker build args (`ARG`) or multi-stage secrets, but this
|
||
requires careful restructuring to avoid breaking Sentry source map uploads.
|
||
|
||
---
|
||
|
||
**7. In-memory IP banning resets on restart** ℹ️ Future enhancement
|
||
|
||
`src/lib/proxy/ban-sus-ips.ts` stores bans in a JavaScript `Map`. Bans reset whenever
|
||
the Next.js server restarts (container restart, redeploy, etc.). A more robust solution
|
||
would persist bans to Convex (using a `bans` table with TTL-based cleanup via a cron
|
||
job) or to Redis.
|
||
|
||
---
|
||
|
||
**8. `apps/next/src/lib/metadata.ts` has hardcoded branding** ⚠️ Template concern
|
||
|
||
When using this as a template for a new project, `metadata.ts` must be updated:
|
||
the title template (`'%s | Convex Monorepo'`) and any other project-specific strings.
|
||
Same for `next.config.js` `images.remotePatterns` (currently `*.gbrown.org`).
|
||
|
||
---
|
||
|
||
**9. No CI/CD** ℹ️ Future enhancement
|
||
|
||
There is no `.github/workflows/` directory. All deployment is done manually via SSH.
|
||
A future enhancement would add GitHub Actions (or Gitea Actions, since this repo is
|
||
on Gitea) for automated lint and typecheck on pull requests.
|
||
|
||
---
|
||
|
||
**10. Scripts organization** ℹ️ Future exploration
|
||
|
||
Currently:
|
||
|
||
- `packages/backend/scripts/generateKeys.mjs` — generates JWT keys (run manually)
|
||
- `docker/generate_convex_admin_key` — bash script to get the admin key from Docker
|
||
|
||
These are separate workflows that both need to be run once when setting up a new
|
||
deployment. Explore combining these into a unified setup script — potentially one that
|
||
generates the keys AND automatically syncs them to Convex in one step, possibly
|
||
moving everything to `docker/scripts/`.
|
||
|
||
---
|
||
|
||
**11. Expo `eas.json` specifies pnpm** ⚠️ Low priority (Expo is WIP anyway)
|
||
|
||
`apps/expo/eas.json` specifies `pnpm 9.15.4` as the package manager. Should specify
|
||
bun. This is low priority since the Expo app is not in active development.
|
||
|
||
---
|
||
|
||
**12. React Email templates not yet implemented** ℹ️ Future enhancement
|
||
|
||
`@react-email/components` and `react-email` are in `packages/backend/package.json` as
|
||
planned dependencies. The current `usesend.ts` uses inline HTML strings for email
|
||
templates. These will be replaced with proper React Email templates in a future update.
|
||
|
||
---
|
||
|
||
## 16. Quick Command Reference
|
||
|
||
```bash
|
||
# ── Development ───────────────────────────────────────────────────────────────
|
||
bun dev # Start all apps (Next.js + Expo + Convex backend)
|
||
bun dev:next # Start Next.js + Convex backend (most common)
|
||
bun dev:backend # Start Convex backend only
|
||
bun dev:expo # Start Expo + Convex backend
|
||
bun dev:expo:tunnel # Expo with tunnel for physical device testing
|
||
|
||
# ── Quality Checks ────────────────────────────────────────────────────────────
|
||
bun typecheck # ← PREFERRED — TypeScript checking, fast
|
||
bun lint # ESLint all packages
|
||
bun lint:fix # ESLint with auto-fix
|
||
bun lint:ws # Check workspace consistency with sherif
|
||
bun format # Check Prettier formatting
|
||
bun format:fix # Apply Prettier formatting
|
||
|
||
# ── Build (production only) ───────────────────────────────────────────────────
|
||
bun build # Build all packages
|
||
|
||
# ── Utilities ─────────────────────────────────────────────────────────────────
|
||
bun ui-add # Add shadcn/ui components interactively
|
||
bun clean # Clean all node_modules (git clean -xdf)
|
||
bun clean:ws # Clean workspace caches only
|
||
|
||
# ── Single Package (Turborepo filters) ────────────────────────────────────────
|
||
bun turbo run typecheck -F @gib/next # Typecheck Next.js app only
|
||
bun turbo run lint -F @gib/backend # Lint backend only
|
||
bun turbo run dev -F @gib/next # Dev Next.js only (no Convex)
|
||
|
||
# ── Scaffolding ───────────────────────────────────────────────────────────────
|
||
bun turbo gen init # Scaffold a new package
|
||
|
||
# ── Convex CLI (run from packages/backend/) ───────────────────────────────────
|
||
bun dev # Runs convex dev (push functions + watch)
|
||
bun setup # Runs convex dev --until-success (push once then exit)
|
||
bun with-env npx convex env set VAR "value" # Sync env var to Convex deployment
|
||
|
||
# ── Docker (run from docker/) ─────────────────────────────────────────────────
|
||
sudo docker compose up -d convex-backend convex-dashboard # Start Convex
|
||
sudo docker compose build next-app # Build Next.js image
|
||
sudo docker compose up -d next-app # Deploy Next.js
|
||
sudo docker compose logs -f # Stream all logs
|
||
./generate_convex_admin_key # Get admin key
|
||
```
|