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

15 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.

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:

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 and adapt env vars for Coolify.

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:

  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:

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
  2. Verify your domain in Resend and create a sender email (e.g. noreply@yourdomain.com)
  3. 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.ts at 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 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:

# 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
# 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:

# 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 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 neededoutput: '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:

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:

# 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:

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:

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:

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:

curl http://localhost:3000