11 KiB
Development Guide
Personal cheat sheet for bootstrapping and deploying projects from this template.
Local Development Setup
1. Clone & Install
git clone <your-forgejo-repo> my-project
cd my-project
pnpm install
2. Environment Variables
Copy .env.example to .env.local and fill in:
CONVEX_SELF_HOSTED_URL='https://convex-backend.mentat.ovh'
CONVEX_SELF_HOSTED_ADMIN_KEY='self-hosted-convex|...'
NEXT_PUBLIC_SITE_URL=http://localhost:3000
NEXT_PUBLIC_CONVEX_URL=https://convex-backend.mentat.ovh
NEXT_PUBLIC_CONVEX_SITE_URL=https://backend-site-xxx.mentat.ovh
Never commit
.env.local— it's in.gitignore.
3. Run Dev Server
pnpm dev --webpack
⚠️ Never use Turbopack — Next.js 16.2.1 has a CPU spike bug. Always
--webpack.
4. Convex Setup (Local)
npx convex dev
This connects to your self-hosted Convex instance. Ensure CLI version matches your backend image version.
Coolify Deployment (Convex Self-Hosted)
1. Deploy Convex Backend
On Coolify, create a new service using this docker-compose:
services:
backend:
image: 'ghcr.io/get-convex/convex-backend:latest'
stop_grace_period: 10s
stop_signal: SIGINT
ports:
- '${PORT:-3210}:3210'
- '${SITE_PROXY_PORT:-3211}:3211'
volumes:
- 'data:/convex/data'
environment:
INSTANCE_NAME: '${INSTANCE_NAME}'
INSTANCE_SECRET: '${INSTANCE_SECRET}'
CONVEX_CLOUD_ORIGIN: '${SERVICE_URL_BACKEND}'
CONVEX_SITE_ORIGIN: '${SERVICE_URL_BACKEND_SITE}'
DO_NOT_REQUIRE_SSL: '${DO_NOT_REQUIRE_SSL:-true}'
DATABASE_URL: '${DATABASE_URL:-}'
# ... (see README.md for full config)
healthcheck:
test: ['CMD-SHELL', 'curl -f http://localhost:3210/version']
interval: 5s
start_period: 10s
timeout: 5s
retries: 10
dashboard:
image: 'ghcr.io/get-convex/convex-dashboard:latest'
stop_grace_period: 10s
stop_signal: SIGINT
ports:
- '${DASHBOARD_PORT:-6791}:6791'
environment:
PORT: '6791'
NEXT_PUBLIC_DEPLOYMENT_URL: '${SERVICE_URL_BACKEND}'
depends_on:
backend:
condition: service_healthy
volumes:
data: null
Best practice: Copy the official self-hosted config and adapt env vars for Coolify.
2. Generate Admin Key
SSH into the server and run:
docker exec -it <backend-container-name> ./generate_admin_key.sh
Save the key. You'll use it to log into the Convex dashboard.
3. Configure Domains
Add 2 domains in Coolify for the backend:
- Backend API:
https://convex-backend.mentat.ovh:3210 - Actions proxy:
https://backend-site-xxx.mentat.ovh:3211
4. Better Auth Env Vars
Set these on your Convex backend via CLI:
pnpm dlx convex env set BETTER_AUTH_SECRET=$(openssl rand -base64 32)
pnpm dlx convex env set SITE_URL=http://localhost:3000
For production, SITE_URL should be your deployed frontend URL.
5. Resend Email Setup
Email is handled by Resend. The API key is server-side only and lives on Convex, not in .env.local.
- Get your API key from resend.com
- Verify your domain in Resend and create a sender email (e.g.
noreply@yourdomain.com) - Set Convex environment variables:
pnpm dlx convex env set RESEND_API_KEY=re_xxxxxxxxxxxxx
pnpm dlx convex env set RESEND_FROM_EMAIL=noreply@yourdomain.com
These are read by
convex/lib/resend.tsat runtime. Never commit them.
Adding a New Project from Template
Option A: Manual Clone (Private Forgejo)
git clone <forgejo-url>/convex-next-saas.git new-project
cd new-project
# Remove .git and re-init
git remote remove origin
git init
git remote add origin <new-project-url>
Option B: Using the Init Script
node bin/init-template.mjs new-project
This will:
- Copy the template to
new-project/ - Replace personal/project-specific strings with placeholders
- Initialize a fresh git repo
- Run
pnpm install
What gets replaced:
| Original | Replaced With |
|---|---|
convex-next-saas |
<project-name> |
SaaS Template |
<project-name> |
Create SaaS in a day! |
<project-name> — built with convex-next-saas |
https://convex-backend.mentat.ovh |
https://your-convex-backend.example.com |
https://convex-backend.mentat.ovh:3210 |
https://your-convex-backend.example.com:3210 |
https://backend-site-olnjg91x5ervt6j6owwgnlha.mentat.ovh |
https://your-convex-site.example.com |
https://backend-site-xxx.mentat.ovh:3211 |
https://your-convex-site.example.com:3211 |
self-hosted-convex|01eea0ecf04bed0f70b73021564229f7f08eecff003f7294e5f9279faa4c19ffd5c501b0ac |
self-hosted-convex|YOUR_ADMIN_KEY |
/home/nxtkofi/.config/tmuxinator/ |
~/.config/tmuxinator/ |
~/vaults/mentat/ |
~/workspace/ |
Note: The script performs a simple string replacement across all text files. Review the output if you have customizations that might collide with these patterns.
Common Issues
missing field functions on npx convex dev
Your CLI and backend image versions differ. Check:
npx convex --version
# Compare with backend image tag in Coolify
Upgrade whichever is lower.
Auth redirect loop
- Ensure
callbackURLstarts with/ - Ensure
sign-inandsign-uppages are not protected byisAuthenticated() - Check
NEXT_PUBLIC_SITE_URLmatches your actual URL
Locale not switching
- Check
src/proxy.tsmatcher includes the route - Check browser has
NEXT_LOCALEcookie - Ensure both
messages/en.jsonandmessages/pl.jsonexist and are valid JSON
Build fails with Turbopack
You forgot --webpack:
# Wrong
pnpm build
# Right
# build script in package.json already does next build
# dev only:
pnpm dev --webpack
Project Checklist
When starting a new project:
- Clone template / run init script
- Update
package.jsonname - Update
README.mdtitle and description - Fill
.env.local - Deploy Convex backend on Coolify
- Set
BETTER_AUTH_SECRETandSITE_URLon Convex - Set
RESEND_API_KEYandRESEND_FROM_EMAILon Convex - Update
src/app/layout.tsxmetadata - Remove/replace placeholder content in
src/app/[locale]/page.tsx - Add project-specific routes to
src/lib/routes.ts - Run
pnpm lintandpnpm buildto verify
PWA Lite
The template includes lightweight PWA installability out of the box. It is intentionally minimal and does not include offline caching, push notifications, or app-store wrappers.
What is included
src/app/manifest.ts— Web App Manifest with installability metadatapublic/pwa/icon.svg— template placeholder iconpublic/pwa/maskable-icon.svg— template placeholder maskable iconpublic/pwa/apple-touch-icon.svg— template placeholder Apple touch iconsrc/app/layout.tsx— PWA-relevant metadata (applicationName,appleWebApp,icons)
What is NOT included
- Service worker or offline caching
- Push notifications
- Google Play TWA / Bubblewrap wrapper
- Apple App Store native wrapper
Before shipping, customize these
| Field | File |
|---|---|
name, short_name, description |
src/app/manifest.ts |
theme_color, background_color |
src/app/manifest.ts |
start_url, scope |
src/app/manifest.ts |
| Icon files | public/pwa/ |
applicationName, appleWebApp.title |
src/app/layout.tsx |
Replace the placeholder SVG icons with real branded assets before production. The template uses neutral
Sinitials as a placeholder only.
Locale behavior
The default manifest uses start_url: '/'. With localePrefix: 'as-needed' in src/i18n/routing.ts, Next.js/next-intl resolves the locale automatically when the app opens.
Auth / cache note
Do not add aggressive caching for authenticated SaaS pages (dashboard, settings) unless you are deliberately designing offline behavior. The template intentionally skips the service worker to avoid accidental auth data leaks.
Store distribution
- Google Play: requires a native/TWA wrapper such as Bubblewrap. The manifest alone is not enough.
- Apple App Store: usually rejects simple repackaged websites without native value. A PWA wrapper alone is not sufficient.
Docker Deployment
The template includes a Dockerfile and docker-compose.yml for containerized deployment.
Dockerfile
Multi-stage build optimized for production:
- deps: Installs dependencies
- builder: Builds the Next.js app
- runner: Minimal image with only production artifacts
# Build locally
docker build -t my-app .
# Run locally
docker run -p 3000:3000 --env-file .env.local my-app
Docker Compose
Full-stack deployment with Convex backend:
# Start everything (Next.js + Convex backend + Convex dashboard)
docker compose up
Services:
| Service | Port | Description |
|---|---|---|
app |
3000 | Next.js application |
backend |
3210 | Convex backend API |
dashboard |
6791 | Convex admin dashboard |
Server Components in Docker
Next.js 16 App Router with Server Components (RSC) works identically in Docker:
- Build time:
next buildgenerates static pages + server bundles - Runtime:
next startserves both static and server-rendered content - No special config needed —
output: 'standalone'innext.config.tshandles everything
The Dockerfile uses output: 'standalone' which creates a self-contained server bundle. This means:
- Smaller image size (only production files)
- Faster startup
- No need for
node_modulesin final image
Environment Variables
When running in Docker, pass env vars via:
.envfile in the same directory asdocker-compose.ymlenvironmentsection indocker-compose.yml-eflags withdocker run
Required variables:
NEXT_PUBLIC_SITE_URL=https://your-domain.com
NEXT_PUBLIC_CONVEX_URL=https://your-convex-backend.example.com
NEXT_PUBLIC_CONVEX_SITE_URL=https://your-convex-site.example.com
CONVEX_SELF_HOSTED_URL=https://your-convex-backend.example.com
CONVEX_SELF_HOSTED_ADMIN_KEY=your-admin-key
Coolify Deployment
- Push code to your Forgejo repo
- In Coolify, create a new service from your repo
- Select
docker-compose.ymlas the deployment method - Set environment variables in Coolify UI
- Deploy
Note
: The
appservice depends onbackend(Convex). Make sure your Convex backend is accessible from the Coolify server.
Health Check
The Next.js app exposes a health endpoint at /api/health (add if needed) or you can check if the app is running:
curl http://localhost:3000