Move to infisical. Create local dev environment. Add ci gates. Modernize repo
Build and Push Next App / quality (push) Successful in 1m8s
Build and Push Next App / build-next (push) Successful in 2m59s

This commit is contained in:
Gabriel Brown
2026-06-21 14:04:02 -05:00
parent 86e2fdc82e
commit a12bf6071b
79 changed files with 1612 additions and 42168 deletions
+74 -477
View File
@@ -1,495 +1,92 @@
# Convex Turbo Monorepo
A production-ready Turborepo starter with Next.js, Expo, self-hosted Convex, and
embedded Payload CMS for live-editable marketing content. Built with TypeScript,
Tailwind CSS, and modern tooling.
A reusable Bun/Turborepo template with Next.js 16, Expo, self-hosted Convex,
Payload CMS, shared UI/config packages, Vitest, and Docker deployment.
---
## Local setup
## What's Inside?
Requirements: Bun 1.3.10, Docker or Podman, Node 22, and the Infisical CLI.
### Apps & Packages
- **`apps/next`** - Next.js 16 web application with App Router
- **`apps/expo`** - Expo 54 mobile application _(in progress)_
- **`@gib/backend`** - Self-hosted Convex backend with authentication
- **`@gib/ui`** - Shared shadcn/ui component library
- **`@gib/eslint-config`** - ESLint configuration
- **`@gib/prettier-config`** - Prettier configuration with import sorting
- **`@gib/tailwind-config`** - Tailwind CSS v4 configuration
- **`@gib/tsconfig`** - Shared TypeScript configurations
### Tech Stack
- **Framework:** Next.js 16 (App Router) + Expo 54
- **Backend:** Convex (self-hosted)
- **Auth:** @convex-dev/auth with Authentik OAuth & Password providers
- **CMS:** Payload CMS embedded inside the Next.js app
- **Styling:** Tailwind CSS v4 + shadcn/ui
- **Language:** TypeScript (strict mode)
- **Package Manager:** Bun
- **Monorepo:** Turborepo
- **Deployment:** Docker (standalone)
---
## Getting Started
This is a self-hosted template. The full setup requires a server (home server or VPS)
to host the Convex backend and dashboard, and a reverse proxy (nginx-proxy-manager is
recommended) to expose them over HTTPS. The Next.js app can run locally in dev mode
once the Convex containers are reachable.
### Prerequisites
- [Bun](https://bun.sh) (v1.2+)
- [Docker](https://www.docker.com/) & Docker Compose (for self-hosted Convex)
- Node.js 22+ (for compatibility)
- A running nginx-proxy-manager instance (or similar reverse proxy) to expose Convex over HTTPS
---
### Step 1 — Clone & Install
```bash
git clone https://git.gbrown.org/gib/convex-monorepo
cd convex-monorepo
bun install
```sh
bun install --frozen-lockfile
infisical login
infisical init
bun db:up
bun dev:next
```
If you're using this as a template for a new project, remove the existing remote and
add your own:
The committed `.infisical.json` links this repository to its own Infisical
project. Local commands read `dev` by default and never fall back to `.env`
files. Select staging with `INFISICAL_ENV=staging bun dev:next`.
```bash
git remote remove origin
git remote add origin https://your-git-host.com/your/new-repo.git
Local services:
- Next.js: `http://localhost:3000`
- Convex API: `http://localhost:3210`
- Convex dashboard: `http://localhost:6791`
- Payload Postgres: `localhost:5432`
Next and Expo run on the host. Payload uses the local Postgres database. Convex
uses a separate self-hosted data volume and does not use Postgres by default.
The commented `POSTGRES_URL` in Compose is an opt-in example for cloned projects.
```sh
bun db:down # stop; preserve Payload and Convex data
bun db:down:wipe # remove both volumes, generated admin key, and seed marker
bun db:sync:payload # refresh/apply the local Payload seed snapshot from staging
```
---
`db:sync:payload` reads the staging `PAYLOAD_DB_URL`, saves a private PostgreSQL
snapshot under `.local/`, and replaces only the localhost Payload database. It
never writes to staging or Convex. Normal `bun db:up` never contacts staging;
it only restores `.local/payload-staging.dump` when the local database marker is
missing or stale. The marker is `.local/payload-seed-state.env`, not a database
table, so Payload schema push will not try to drop it. Stop the Next development
server first so it does not reconnect while the database is being replaced. The
snapshot includes Payload users and password hashes, allowing the same admin
credentials locally; protect it as production-derived data and delete it when no
longer needed.
### Step 2 — Configure the Single Root Environment File
Physical devices cannot resolve their own `localhost`; override the public
Convex URL with the development host's LAN address when testing Expo on-device.
```bash
cp .env.example .env
## Environment model
- Local `dev` and `staging`: Infisical.
- Generated local state: `.local/<environment>.generated.env`.
- CI/CD: Gitea `DOTENV_PROD`, materialized only as a temporary runner file.
- Docker compilation: explicit Compose build args; `.env*` stays outside the
image context.
Run `sh scripts/with-env dev -- <command>` for an environment-aware command or
`sh scripts/export-env dev` to materialize a temporary merged dotenv stream.
Do not commit or maintain root `.env` files.
## Development and quality
```sh
bun dev:next
bun dev:expo
bun lint:ws
bun format
bun lint
bun typecheck
bun test:unit
bun test:integration
bun test:component
SKIP_E2E=1 bun run ci:check
```
The root `/.env` is the single required env file for this repo. It is used for:
`bun test:e2e` starts the isolated local stack and currently performs generic
stack smoke checks. It skips in CI and when `SKIP_E2E=1` is set.
- app/package runtime env vars
- Next.js build-time env vars
- Payload env vars
- Docker Compose interpolation
- helper scripts in `scripts/`
Fill it in with your values. The Docker-related variables in the same file control the
Convex containers and Next.js container — you'll need to choose:
- `INSTANCE_NAME` — a unique name for your Convex instance
- `INSTANCE_SECRET` — a secret string (generate something random)
- `CONVEX_CLOUD_ORIGIN` — the public HTTPS URL for your Convex backend API (e.g. `https://api.convex.example.com`)
- `CONVEX_SITE_ORIGIN` — the public HTTPS URL for Convex Auth HTTP routes (e.g. `https://api.convex.example.com`)
- `NEXT_PUBLIC_DEPLOYMENT_URL` — the URL for the Convex dashboard (e.g. `https://dashboard.convex.example.com`)
- `NEXT_CONTAINER_NAME`, `BACKEND_CONTAINER_NAME`, and `DASHBOARD_CONTAINER_NAME` — the Docker service container names
Do not create or rely on a separate `docker/.env`.
---
### Step 3 — Start the Convex Containers
```bash
./scripts/docker-compose up -d backend dashboard
```
Wait a moment for `convex-backend` to pass its health check, then verify both
containers are running:
```bash
./scripts/docker-compose ps
```
Reverse-proxy the two Convex services through nginx-proxy-manager (or your preferred
proxy) to the URLs you chose in Step 2. Both must be reachable over HTTPS before you
can proceed.
---
### Step 4 — Generate the Convex Admin Key
With the backend container running, generate the admin key:
```bash
./scripts/generate-convex-admin-key
```
Copy the printed key — you'll need it as `CONVEX_SELF_HOSTED_ADMIN_KEY` in the root
`.env` file.
---
### Step 5 — Finish Configuring Root Environment Variables
Fill out all values in root `/.env`:
```bash
# Next.js
NODE_ENV=development
SENTRY_AUTH_TOKEN= # From your self-hosted Sentry
PAYLOAD_SECRET= # openssl rand -hex 32
PAYLOAD_DB_URL=postgresql://user:password@host:5432/db_name
NEXT_PUBLIC_SITE_URL=https://example.com
NEXT_PUBLIC_CONVEX_URL=https://api.convex.example.com
NEXT_PUBLIC_PLAUSIBLE_URL=https://plausible.example.com
NEXT_PUBLIC_SENTRY_DSN=
NEXT_PUBLIC_SENTRY_URL=https://sentry.example.com
NEXT_PUBLIC_SENTRY_ORG=sentry
NEXT_PUBLIC_SENTRY_PROJECT_NAME=my-project
# Convex
CONVEX_SELF_HOSTED_URL=https://api.convex.example.com
CONVEX_SELF_HOSTED_ADMIN_KEY= # From Step 4
CONVEX_SITE_URL=http://localhost:3000 # Always localhost:3000 for local dev
# Auth (synced to Convex in Step 6)
USESEND_API_KEY=
USESEND_URL=https://usesend.example.com
USESEND_FROM_EMAIL=My App <noreply@example.com>
AUTH_AUTHENTIK_ID=
AUTH_AUTHENTIK_SECRET=
AUTH_AUTHENTIK_ISSUER=https://auth.example.com/application/o/my-app/
# Docker Compose
NETWORK=nginx-bridge
NEXT_CONTAINER_NAME=next-app
BACKEND_CONTAINER_NAME=convex-backend
DASHBOARD_CONTAINER_NAME=convex-dashboard
INSTANCE_NAME=convex
INSTANCE_SECRET=
CONVEX_CLOUD_ORIGIN=https://api.convex.example.com
CONVEX_SITE_ORIGIN=https://convex.convex.example.com
NEXT_PUBLIC_DEPLOYMENT_URL=https://dashboard.convex.example.com
POSTGRES_URL=postgresql://user:password@host:5432/convex
```
---
### Step 6 — Generate JWT Keys & Sync Environment Variables to Convex
Generate the RS256 JWT keypair needed for Convex Auth:
```bash
cd packages/backend
bun run scripts/generateKeys.mjs
```
This prints `JWT_PRIVATE_KEY` and `JWKS` values. Sync them to your Convex deployment
along with all other backend environment variables:
```bash
# From packages/backend/
bun with-env npx convex env set JWT_PRIVATE_KEY "your-private-key"
bun with-env npx convex env set JWKS "your-jwks"
bun with-env npx convex env set AUTH_AUTHENTIK_ID "your-client-id"
bun with-env npx convex env set AUTH_AUTHENTIK_SECRET "your-client-secret"
bun with-env npx convex env set AUTH_AUTHENTIK_ISSUER "your-issuer-url"
bun with-env npx convex env set USESEND_API_KEY "your-api-key"
bun with-env npx convex env set USESEND_URL "https://usesend.example.com"
bun with-env npx convex env set USESEND_FROM_EMAIL "My App <noreply@example.com>"
```
**For production auth to work**, you must also update `CONVEX_SITE_URL` in the Convex
Dashboard to your production Next.js URL. Go to
`https://dashboard.convex.example.com` → Settings → Environment Variables and set:
```
CONVEX_SITE_URL = https://example.com
```
The root `.env` value of `http://localhost:3000` is correct for local dev and should
not be changed — only update it in the Dashboard for production.
---
### Step 7 — Start the Development Server
```bash
# From project root
bun dev:next # Next.js app + Convex backend (most common)
# or
bun dev # All apps (Next.js + Expo + Backend)
```
**App URLs:**
- **Next.js:** http://localhost:3000
- **Convex Dashboard:** https://dashboard.convex.example.com
### Docker Helper Scripts
This repo includes helper scripts so you do not have to keep passing `--env-file` and
`-f` manually to Docker Compose.
Useful commands:
```bash
./scripts/docker-compose ps
./scripts/docker-compose up -d backend dashboard
./scripts/docker-compose up -d next
./scripts/build-next-app
./scripts/update-next-app
./scripts/update-convex
./scripts/generate-convex-admin-key
```
`./scripts/docker-compose` also accepts short service aliases like `next`, `backend`,
and `dashboard` and maps them to the container names from root `/.env`.
---
## Development Commands
```bash
# Development
bun dev # Run all apps
bun dev:next # Next.js + Convex backend
bun dev:expo # Expo + Convex backend
bun dev:backend # Convex backend only
# Quality Checks
bun typecheck # Type checking (recommended for testing)
bun lint # Lint all packages
bun lint:fix # Lint and auto-fix
bun format # Check formatting
bun format:fix # Fix formatting
# Build
bun build # Build all packages (production)
# Utilities
bun ui-add # Add shadcn/ui components
bun clean # Clean node_modules
bun clean:ws # Clean workspace caches
```
### Single Package Commands
Use Turborepo filters to target specific packages:
```bash
bun turbo run dev -F @gib/next # Run Next.js only
bun turbo run typecheck -F @gib/backend # Typecheck backend
bun turbo run lint -F @gib/ui # Lint UI package
```
---
## Project Structure
```
convex-monorepo/
├── apps/
│ ├── next/ # Next.js web app
│ │ ├── src/
│ │ │ ├── app/ # App Router pages
│ │ │ ├── components/ # React components
│ │ │ └── lib/ # Utilities
│ │ └── package.json
│ └── expo/ # Expo mobile app (WIP)
├── packages/
│ ├── backend/ # Convex backend
│ │ ├── convex/ # Convex functions (synced to deployment)
│ │ ├── scripts/ # Utilities (generateKeys.mjs)
│ │ └── types/ # Shared types
│ └── ui/ # shadcn/ui components
│ └── src/ # Component source files
├── tools/ # Shared configurations
│ ├── eslint/
│ ├── prettier/
│ ├── tailwind/
│ └── typescript/
├── docker/ # Self-hosted deployment
│ ├── compose.yml
│ ├── Dockerfile
│ └── data/ # Convex data volume (gitignored in real use)
├── scripts/ # Docker/deployment helper scripts using root .env
│ ├── docker-compose
│ ├── build-next-app
│ ├── update-next-app
│ └── update-convex
├── turbo.json # Turborepo configuration
└── package.json # Root workspace & catalogs
```
---
## Features
### Authentication
- **OAuth:** Authentik SSO integration
- **Password:** Custom password auth with email verification
- **OTP:** Email verification via self-hosted UseSend
- **Session Management:** Secure cookie-based sessions (30-day max age)
### Next.js App
- **App Router:** Next.js 16 with React Server Components
- **Editable Marketing Content:** Payload-backed landing page with live preview
- **Data Preloading:** SSR data fetching with `preloadQuery` + `usePreloadedQuery`
- **Middleware:** Route protection & IP-based security (`src/proxy.ts`)
- **Styling:** Tailwind CSS v4 with dark mode (OKLCH-based theme)
- **Analytics:** Plausible (privacy-focused, proxied through Next.js)
- **Monitoring:** Sentry error tracking & performance
### Backend
- **Real-time:** Convex reactive queries
- **File Storage:** Built-in file upload/storage
- **Auth:** Multi-provider authentication
- **Type-safe:** Full TypeScript with generated types
- **Self-hosted:** Complete control over your data
### Developer Experience
- **Monorepo:** Turborepo for efficient builds and caching
- **Type Safety:** Strict TypeScript throughout
- **Code Quality:** ESLint + Prettier with auto-fix
- **Hot Reload:** Fast refresh for all packages
- **Catalog Deps:** Centralized dependency version management
---
Shared dependency versions belong in root catalogs. Edit the root catalog, run
`bun install`, then `bun lint:ws`; do not run `bun update` inside a workspace.
## Deployment
### Production Deployment (Docker)
Once the Convex containers are running (they only need to be started once), deploying
a new version of the Next.js app is a one-command workflow:
```bash
./scripts/build-next-app
```
To start all services from scratch:
```bash
./scripts/docker-compose up -d backend dashboard
# Wait for backend health check to pass, then:
./scripts/docker-compose up -d next
```
**Services:**
- `next-app` — Next.js standalone build
- `convex-backend` — Convex backend (port 3210)
- `convex-dashboard` — Admin dashboard (port 6791)
**Network:** Uses `nginx-bridge` Docker network (reverse proxy via nginx-proxy-manager).
### Production Checklist
- [ ] Fill out root `/.env` with all app, Payload, and Docker Compose values
- [ ] Start `convex-backend` and `convex-dashboard` containers
- [ ] Generate and set `CONVEX_SELF_HOSTED_ADMIN_KEY` via `./scripts/generate-convex-admin-key`
- [ ] Reverse-proxy both Convex services via nginx-proxy-manager with SSL
- [ ] Generate JWT keys and sync all env vars to Convex (`bun with-env npx convex env set ...`)
- [ ] Update `CONVEX_SITE_URL` in the Convex Dashboard to your production Next.js URL
- [ ] Build and start the `next-app` container
- [ ] Back up `docker/data/` regularly (contains all Convex database data)
---
## Documentation
- **[AGENTS.md](./AGENTS.md)** — Comprehensive guide for AI agents & developers
- **[docs/payload-cms.md](./docs/payload-cms.md)** — How Payload is wired up and how to extend it in this template
- **[Convex Docs](https://docs.convex.dev)** — Official Convex documentation
- **[Turborepo Docs](https://turbo.build/repo/docs)** — Turborepo documentation
- **[Next.js Docs](https://nextjs.org/docs)** — Next.js documentation
---
## Troubleshooting
### Backend typecheck shows TypeScript help message
This is expected behavior. The backend package follows Convex's structure with only
`convex/tsconfig.json` (no root tsconfig). Running `bun typecheck` from the repo root
will show TypeScript's help text for `@gib/backend` — this is not an error.
### Imports from Convex require `.js` extension
The project uses ESM (`"type": "module"`), which requires explicit file extensions:
```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';
```
### Docker containers won't start
1. Check Docker logs: `./scripts/docker-compose logs`
2. Verify environment variables in root `/.env`
3. Ensure the `nginx-bridge` network exists: `sudo docker network create nginx-bridge`
4. Check that the required ports (3210, 6791) are not already in use
### Only one `.env` file matters
Use the root `/.env` for everything in this repo, including Docker helper scripts and
Docker Compose interpolation. Do not create or rely on a separate `docker/.env`.
### Auth doesn't work in production
Make sure `CONVEX_SITE_URL` is set to your production Next.js URL in the **Convex
Dashboard** (not just in the root `.env` file). The root `.env` should always contain
`http://localhost:3000`; the Dashboard must have your production URL.
### Catalog updates break workspace
After updating dependencies, if you see `sherif` errors on `bun install`:
1. Never use `bun update` inside individual package directories
2. Edit the version in root `package.json` catalog section instead
3. Run `bun install` from the root
4. Verify with `bun lint:ws`
---
## Contributing
This is a personal monorepo template. Feel free to fork and adapt for your needs.
### Code Style
- Single quotes, trailing commas
- 80 character line width
- ESLint + Prettier enforced
- `const fn = () => {}` over `function fn()` (strong preference)
- Import order: Types → React → Next → Third-party → @gib → Local
Run `bun lint:fix` and `bun format:fix` before committing.
---
## License
MIT
---
## Acknowledgments
Built with inspiration from:
- [T3 Turbo](https://github.com/t3-oss/create-t3-turbo)
- [Convex](https://convex.dev)
- [shadcn/ui](https://ui.shadcn.com)
- [Turborepo](https://turbo.build)
Production Compose retains the self-hosted Convex backend/dashboard and accepts
an external Payload `PAYLOAD_DB_URL`. Its commented Postgres service remains an
optional Payload database. Gitea runs the quality gate first, builds the Next
image from a temporary Gitea-secret env file, then pushes SHA and `latest` tags.
CI never installs or invokes Infisical.