Optional modules

Payments, AI chat, background jobs, uploads, and maps. Each one strippable at scaffold time, each one a flag away.

Genesis is a maximal template: every module a SaaS might need is included, and genesis-start strips the ones you do not want at scaffold time. Modules kept in the project stay dormant until their flag is enabled, and every flag validates its prerequisites at boot.

Payments (Polar)

ENABLE_POLAR=true
POLAR_ENV=sandbox | production
POLAR_ACCESS_TOKEN_SANDBOX=... (or POLAR_ACCESS_TOKEN_PROD)

Integrates Polar through the Better Auth plugin: customers are created on sign-up, with checkout and customer portal flows wired. Webhook handling activates when POLAR_WEBHOOK_SECRET_* is set. Production requires POLAR_ENV=production and a production token.

AI chat

ENABLE_AI_CHAT=true
OPENAI_API_KEY=...
AI_MODEL=gpt-4o-mini

A streaming chat endpoint at /api/chat for authenticated users, with history persisted per user and thread. Guardrails ship in the baseline: message count and length caps, and a per-user rate limit.

Background jobs (BullMQ)

ENABLE_JOBS=true
REDIS_URL=redis://...

Queue infrastructure on BullMQ with Redis. A worker service template lives in services/_template/ for jobs that should run outside the web process; see the multi-service doc in the repo for the pattern.

Uploads (UploadThing)

VITE_ENABLE_UPLOADS=true
UPLOADTHING_TOKEN=...

Two upload routes ship configured: images (4MB, up to 4 files) and PDF documents (16MB, single file). Uploads require an authenticated session and are recorded in the Upload model with key, URL, size, and type.

Maps (Geoapify)

MAPS_PROVIDER=geoapify
VITE_MAPS_PROVIDER=geoapify   # must match — boot fails if it doesn't
GEOAPIFY_API_KEY=...
MAP_CACHE_DIR=.cache/maps

Getting a key: sign up at geoapify.com, create a project, copy its API key. Free tier is 3,000 credits/day, allows commercial use, and needs no card.

Static map thumbnails at /api/map plus forward geocoding. MapThumb renders a plain <img>, never an embedded map engine — browsers cap active WebGL contexts at around eight and evict the oldest, so a grid of live maps blanks itself out. Every failure path (module off, no coordinates, provider down, broken image) renders the fallback you pass, so a card without a map degrades to whatever it showed before.

Images are rendered once per location and cached, which is what keeps this on the free tier permanently rather than temporarily: the allowance is spent on distinct places, not page views. That only works because Geoapify's terms explicitly permit caching, storing and redistributing rendered maps. Any replacement provider has to allow the same — Mapbox, for example, permits only 30-day device-side caching, which would turn this into a per-view cost. Check that before adding one to src/domain/maps/providers/.

Geocoding deliberately has no table of its own. Persist the coordinates on your own rows and never ask again:

import { geocode } from '~/domain/maps';

const located = await geocode(listing.address, 'gb');
if (located) {
  await db.listing.update({
    where: { id: listing.id },
    data: { latitude: located.lat, longitude: located.lng },
  });
}

In production VITE_MAPS_PROVIDER is a build variable, not a runtime one — it is inlined into the client bundle (Coolify: mark it "Build Variable"). Set it only at runtime and the server will serve maps the UI never asks for. MAP_CACHE_DIR wants a mounted volume; without one the cache refills after each deploy, which costs one render per location and breaks nothing.

Social auth

GitHub and Google OAuth activate when their client credentials are present (GITHUB_CLIENT_ID/SECRET, GOOGLE_CLIENT_ID/SECRET), with matching VITE_ENABLE_*_AUTH flags showing the buttons. Account linking between providers is enabled automatically.

The stripping model

If you scaffold without a module, its code, schema models, and env keys are removed: no dead weight. If you keep a module disabled, enabling it later is setting its env vars. The flags fail loudly when prerequisites are missing rather than silently degrading.