t-convex-nextjs-saas/DEVELOPMENT.md
nxtkofi 1c2adc1f1e chore(deploy): simplify docker-compose and add Forgejo CI/CD
- Remove Convex from docker-compose (runs separately)
- Add .forgejo/workflows/ci.yml with auto-deploy to Coolify
- Update DEVELOPMENT.md with dev/prod architecture diagram
- Rename branch from develop to dev in all workflows
2026-05-17 18:58:17 +02:00

437 lines
15 KiB
Markdown

# Development Guide
Personal cheat sheet for bootstrapping and deploying projects from this template.
## Local Development Setup
### 1. Clone & Install
```bash
git clone <your-forgejo-repo> my-project
cd my-project
pnpm install
```
### 2. Environment Variables
Copy `.env.example` to `.env.local` and fill in:
```bash
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
```bash
pnpm dev --webpack
```
> ⚠️ **Never use Turbopack** — Next.js 16.2.1 has a CPU spike bug. Always `--webpack`.
### 4. Convex Setup (Local)
```bash
npx convex dev
```
This connects to your self-hosted Convex instance. Ensure CLI version matches your backend image version.
## Architecture Overview
```
┌─────────────────────────────────────────────────────────────┐
│ LOCAL DEV │
│ pnpm dev --webpack → npx convex dev → DEV CONVEX DB │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ FORGEJO / GIT │
│ main branch ──► PROD DEPLOY │
│ dev branch ──► DEV DEPLOY │
└─────────────────────────────────────────────────────────────┘
┌───────────────┴───────────────┐
▼ ▼
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ COOLIFY (PROD) │ │ COOLIFY (DEV) │
│ Next.js Docker Container │ │ Next.js Docker Container │
│ (frontend) │ │ (frontend) │
└─────────────────────────────┘ └─────────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ CONVEX (PROD DB) │ │ CONVEX (DEV DB) │
│ Self-hosted on Coolify │ │ Self-hosted on Coolify │
│ or Convex Cloud │ │ or Convex Cloud │
└─────────────────────────────┘ └─────────────────────────────┘
```
### Convex Backend
Convex runs **separately** from the Next.js frontend. You need two instances:
- **Dev**: for local development + dev environment testing
- **Prod**: for production
On Coolify, create a new service for each Convex backend using the official docker-compose:
```yaml
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:-}'
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](https://github.com/get-convex/convex-backend/blob/main/self-hosted/docker/docker-compose.yml) and adapt env vars for Coolify.
### Generate Admin Key
SSH into the server and run:
```bash
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:
1. Backend API: `https://convex-backend.mentat.ovh:3210`
2. Actions proxy: `https://backend-site-xxx.mentat.ovh:3211`
### 4. Better Auth Env Vars
Set these on your Convex backend via CLI:
```bash
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`.
1. Get your API key from [resend.com](https://resend.com)
2. Verify your domain in Resend and create a sender email (e.g. `noreply@yourdomain.com`)
3. Set Convex environment variables:
```bash
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.ts` at runtime. Never commit them.
## Adding a New Project from Template
### Option A: Manual Clone (Private Forgejo)
```bash
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
```bash
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:
```bash
npx convex --version
# Compare with backend image tag in Coolify
```
Upgrade whichever is lower.
### Auth redirect loop
- Ensure `callbackURL` starts with `/`
- Ensure `sign-in` and `sign-up` pages are **not** protected by `isAuthenticated()`
- Check `NEXT_PUBLIC_SITE_URL` matches your actual URL
### Locale not switching
- Check `src/proxy.ts` matcher includes the route
- Check browser has `NEXT_LOCALE` cookie
- Ensure both `messages/en.json` and `messages/pl.json` exist and are valid JSON
### Build fails with Turbopack
You forgot `--webpack`:
```bash
# 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.json` name
- [ ] Update `README.md` title and description
- [ ] Fill `.env.local`
- [ ] Deploy Convex backend on Coolify
- [ ] Set `BETTER_AUTH_SECRET` and `SITE_URL` on Convex
- [ ] Set `RESEND_API_KEY` and `RESEND_FROM_EMAIL` on Convex
- [ ] Update `src/app/layout.tsx` metadata
- [ ] Remove/replace placeholder content in `src/app/[locale]/page.tsx`
- [ ] Add project-specific routes to `src/lib/routes.ts`
- [ ] Run `pnpm lint` and `pnpm build` to 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 metadata
- `public/pwa/icon.svg` — template placeholder icon
- `public/pwa/maskable-icon.svg` — template placeholder maskable icon
- `public/pwa/apple-touch-icon.svg` — template placeholder Apple touch icon
- `src/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 `S` initials 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
```bash
# Build locally
docker build -t my-app .
# Run locally
docker run -p 3000:3000 --env-file .env.local my-app
```
### Docker Compose
Frontend-only deployment:
```bash
# Build and start Next.js app
docker compose up --build
```
Services:
| Service | Port | Description |
|---------|------|-------------|
| `app` | 3000 | Next.js application |
> **Note**: Convex backend runs separately. See [Coolify Deployment](#coolify-deployment) for Convex setup.
### Server Components in Docker
Next.js 16 App Router with Server Components (RSC) works identically in Docker:
- **Build time**: `next build` generates static pages + server bundles
- **Runtime**: `next start` serves both static and server-rendered content
- **No special config needed** — `output: 'standalone'` in `next.config.ts` handles 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_modules` in final image
### Environment Variables
When running in Docker, pass env vars via:
1. `.env` file in the same directory as `docker-compose.yml`
2. `environment` section in `docker-compose.yml`
3. `-e` flags with `docker run`
Required variables:
```bash
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
#### 1. Setup Two Environments
Create two services in Coolify:
| Service | Branch | Convex DB | Domain |
|---------|--------|-----------|--------|
| `my-app-prod` | `main` | Prod | `app.example.com` |
| `my-app-dev` | `dev` | Dev | `dev-app.example.com` |
#### 2. Configure Webhooks (Auto-Deploy)
In each Coolify service, copy the **Deploy Webhook URL** and add it to Forgejo Secrets:
```bash
# Forgejo → Repo Settings → Secrets
COOLIFY_PROD_WEBHOOK=https://coolify.example.com/webhooks/...
COOLIFY_DEV_WEBHOOK=https://coolify.example.com/webhooks/...
```
The `.forgejo/workflows/ci.yml` automatically triggers deploy on push:
- `main` branch → Prod webhook
- `dev` branch → Dev webhook
#### 3. Set Environment Variables
In each Coolify service, set env vars:
**Prod**:
```bash
NEXT_PUBLIC_SITE_URL=https://app.example.com
NEXT_PUBLIC_CONVEX_URL=https://convex-prod.example.com
NEXT_PUBLIC_CONVEX_SITE_URL=https://convex-site-prod.example.com
CONVEX_SELF_HOSTED_URL=https://convex-prod.example.com
CONVEX_SELF_HOSTED_ADMIN_KEY=prod-admin-key
```
**Dev**:
```bash
NEXT_PUBLIC_SITE_URL=https://dev-app.example.com
NEXT_PUBLIC_CONVEX_URL=https://convex-dev.example.com
NEXT_PUBLIC_CONVEX_SITE_URL=https://convex-site-dev.example.com
CONVEX_SELF_HOSTED_URL=https://convex-dev.example.com
CONVEX_SELF_HOSTED_ADMIN_KEY=dev-admin-key
```
#### 4. Local Development Connects to Dev
Your `.env.local` should point to the **dev** Convex:
```bash
CONVEX_SELF_HOSTED_URL='https://convex-dev.example.com'
CONVEX_SELF_HOSTED_ADMIN_KEY='dev-admin-key'
NEXT_PUBLIC_SITE_URL=http://localhost:3000
NEXT_PUBLIC_CONVEX_URL=https://convex-dev.example.com
NEXT_PUBLIC_CONVEX_SITE_URL=https://convex-site-dev.example.com
```
### Health Check
Verify the app is running:
```bash
curl http://localhost:3000
```