Files
convex-monorepo/AGENTS.md

1412 lines
57 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 is one `.dockerignore` file at the repo root: `/.dockerignore`. Docker always
reads the `.dockerignore` from the root of the build context, which is set to `../`
(the repo root) in `compose.yml` — so the root file is the only one that ever applies.
The `.env` file is intentionally included in the build context (the `.env` lines are
commented out in `/.dockerignore`). This is how `SENTRY_AUTH_TOKEN` and `NEXT_PUBLIC_*`
variables reach the Next.js build step inside the container.
Do not un-comment those lines 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.
**Note on `docker/.env`:** This file is entirely separate and serves a different
purpose. It is read by Docker Compose itself (not the build) to interpolate the
`${VARIABLE}` placeholders in `compose.yml` — things like container names, network
names, and Convex origin URLs. It is not the same as the root `/.env` and the two
are not interchangeable.
### 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 — Scaffold Only, Not Production-Ready
The `apps/expo/` directory exists as a placeholder for future mobile development.
The broken T3 Turbo template code has been cleaned up — all `@acme/*` references,
tRPC, and `better-auth` have been removed and replaced with the correct Convex patterns.
The app will now load, but it is still a bare scaffold with no real features.
### Current state
The app is wired up with the correct providers and patterns:
- **`src/app/_layout.tsx`** — `ConvexAuthProvider` wraps the root, initialized with
a `ConvexReactClient` pointed at the self-hosted backend
- **`src/utils/convex.ts`** — Convex client setup, derives the backend URL from the
Expo dev server host in development; reads from `app.config.ts` `extra.convexUrl`
in production
- **`src/app/index.tsx`** — A minimal home screen that reads auth state via
`useConvexAuth()` and queries the current user via `api.auth.getUser`
- **`src/app/post/[id].tsx`** — A placeholder detail screen (route structure kept intact)
- **`src/utils/session-store.ts`** — `expo-secure-store` helpers for token storage
- **`src/utils/base-url.ts`** — Dev server localhost detection
### What still needs to be built
- Sign-in / sign-up screens (mirror the Next.js `(auth)/sign-in` flow, adapted for RN)
- Authenticated navigation / route protection (check `isAuthenticated` in layout)
- Any actual app screens and Convex queries for your specific project
### Production URL configuration
For production builds, set `convexUrl` in `app.config.ts` `extra`:
```typescript
extra: {
convexUrl: 'https://api.convex.example.com',
},
```
The client in `src/utils/convex.ts` will prefer this over the dev-server-derived URL.
---
## 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. 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.
---
**3. 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.
---
**4. `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`).
---
**5. 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.
---
**6. 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/`.
---
**7. 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
```