14 KiB
Convex Turbo Monorepo
A production-ready Turborepo starter with Next.js, Expo, and self-hosted Convex backend. Built with TypeScript, Tailwind CSS, and modern tooling.
What's Inside?
Apps & Packages
apps/next- Next.js 16 web application with App Routerapps/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
- 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 (v1.2+)
- Docker & 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
git clone https://git.gbrown.org/gib/convex-monorepo
cd convex-monorepo
bun install
If you're using this as a template for a new project, remove the existing remote and add your own:
git remote remove origin
git remote add origin https://your-git-host.com/your/new-repo.git
Step 2 — Configure the Docker Environment
The docker/ directory contains everything needed to run the Convex backend and the
Next.js app in production.
cd docker/
cp .env.example .env
Edit docker/.env and fill in your values. The variables below the Next.js app section
control the Convex containers — you'll need to choose:
INSTANCE_NAME— a unique name for your Convex instanceINSTANCE_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)
Step 3 — Start the Convex Containers
cd docker/
sudo docker compose up -d convex-backend convex-dashboard
Wait a moment for convex-backend to pass its health check, then verify both
containers are running:
sudo 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:
cd docker/
./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 — Configure Root Environment Variables
Create the root .env file:
# From the repo root
cp .env.example .env
Fill out all values in /.env:
# Next.js
NODE_ENV=development
SENTRY_AUTH_TOKEN= # From your self-hosted Sentry
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/
Step 6 — Generate JWT Keys & Sync Environment Variables to Convex
Generate the RS256 JWT keypair needed for Convex Auth:
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:
# 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
# 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
Development Commands
# 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:
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
│ └── .env.example
│
├── 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
- 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
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 two-command workflow:
# SSH onto your server, then:
cd docker/
# Build the new image
sudo docker compose build next-app
# Deploy it
sudo docker compose up -d next-app
To start all services from scratch:
cd docker/
sudo docker compose up -d convex-backend convex-dashboard
# Wait for backend health check to pass, then:
sudo docker compose up -d next-app
Services:
next-app— Next.js standalone buildconvex-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
docker/.envwith your domain names and secrets - Start
convex-backendandconvex-dashboardcontainers - Generate and set
CONVEX_SELF_HOSTED_ADMIN_KEYvia./generate_convex_admin_key - Reverse-proxy both Convex services via nginx-proxy-manager with SSL
- Fill out root
/.envwith all environment variables - Generate JWT keys and sync all env vars to Convex (
bun with-env npx convex env set ...) - Update
CONVEX_SITE_URLin the Convex Dashboard to your production Next.js URL - Build and start the
next-appcontainer - Back up
docker/data/regularly (contains all Convex database data)
Documentation
- AGENTS.md — Comprehensive guide for AI agents & developers
- Convex Docs — Official Convex documentation
- Turborepo Docs — Turborepo documentation
- Next.js 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:
// ✅ 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
- Check Docker logs:
sudo docker compose logs - Verify environment variables in
docker/.env - Ensure the
nginx-bridgenetwork exists:sudo docker network create nginx-bridge - Check that the required ports (3210, 6791) are not already in use
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:
- Never use
bun updateinside individual package directories - Edit the version in root
package.jsoncatalog section instead - Run
bun installfrom the root - 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 = () => {}overfunction 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: