Campaign Companion for Dungeon Masters
Your campaign, without the amnesia. Persistent, reviewable memory for NPCs, factions and consequences.
Campaign preparation becomes harder as context accumulates. A DM must track NPCs, factions, consequences, open arcs, and forgotten details while preparing a coherent next session. There is no good tool that connects what happened to what comes next.
Lazy Lands helps a DM:
- Capture campaign context from free text.
- Record what happened after each session.
- Review and validate AI-suggested memories.
- Generate a coherent next-session proposal using only accepted memories.
The AI is the Scribe. It proposes, never decides. The DM always has the last word.
Full flow: PRODUCT.md section 4.
- The DM signs up or logs in.
- Creates a campaign from free text.
- Reviews AI-extracted NPCs, factions and world state before saving.
- Records what happened after a played session.
- Reviews and accepts/edits/dismisses AI-suggested memories.
- Asks Lazy Lands to prepare the next session.
- Reviews and copies or exports the generated session proposal.
| Layer | Technology |
|---|---|
| Frontend | Next.js 16 (App Router), React, TypeScript, TailwindCSS |
| UI | shadcn/ui, Lucide, React Hook Form, Zod |
| Backend | FastAPI, Python 3.12, Pydantic, pydantic-settings |
| Package mgr | uv (backend), pnpm + Turborepo (monorepo) |
| Database | Supabase (PostgreSQL + Auth + Row Level Security) |
| AI | LLM Provider abstraction (Gemini, Groq, Mistral or Cerebras; fake in dev) |
| Testing | Vitest + React Testing Library, Playwright, pytest |
| Quality | ESLint, Prettier, Ruff, mypy, Husky, lint-staged |
| Deployment | Vercel (frontend), Railway (backend) |
Backend follows a Modular Monolith with nested Clean/Hexagonal layers per module (see ADR-05). Feature modules live under services/api/app/modules/ and encapsulate their own domain/, application/, infrastructure/, routes, schemas, and prompts. Transversal concerns live in the shared/ kernel.
Full architecture reference: docs/04-architecture.md.
Try the complete campaign flow without creating an account at lazy-lands.com/demo. The guided tour runs entirely in the browser with seeded, in-memory data: it does not require credentials and does not affect production campaigns.
The web application and API send CSP, anti-framing, nosniff, referrer, and
permissions-policy headers. The API generates a server-side X-Request-ID for
safe correlation. HSTS is intentionally configured at the TLS ingress or hosting
platform, not by the application: enabling it before HTTPS is enforced can lock
users out of an HTTP development or misconfigured deployment.
CI audits Node dependencies from pnpm-lock.yaml and the installed Python
environment with pip-audit==2.10.0. Adding pip-audit to the API dependency
group would add 304 transitive lockfile lines, exceeding the bounded security
work unit; uvx --from keeps the audit tool version explicit while uv sync --locked remains the reproducible application dependency install.
lazy-lands/
|-- apps/web/ # Next.js frontend (App Router)
| |-- app/ # Routes and layouts
| |-- components/ # Shared UI components
| |-- lib/ # Utilities (Supabase, etc.)
| |-- tests/ # Vitest unit tests + Playwright E2E
|-- services/api/ # FastAPI backend
| |-- app/main.py
| |-- app/shared/ # Config, security, errors, logging, dependencies, shared adapters
| |-- app/shared/llm/ # LLM provider port and fake implementation
| |-- app/modules/ # Health + feature modules (campaigns, sessions, memory, generation)
| |-- tests/ # pytest test suite
|-- supabase/ # Migrations, config, seed
|-- docs/ # SDD technical documentation
|-- .github/workflows/ # CI pipeline
|-- PRODUCT.md # Product source of truth
|-- DESIGN.md # Design system and tokens
|-- AGENTS.md # AI agent instructions
- Node.js 22+ and pnpm 11+
- Python 3.12 and uv
- Docker Desktop + WSL2 (required for supabase start)
git clone https://github.com/danilopgon/lazy-lands.git
cd lazy-lands
pnpm installcp .env.example .env
# Fill in the values from your Supabase project dashboardFor frontend-only local development, also copy the frontend values into
apps/web/.env.local.
pnpm supabase start
# Outputs local URLs and keys -- copy them into .envNote: Supabase requires Docker Desktop with WSL2 on Windows. Alternative: connect to a remote Supabase project.
pnpm --filter web dev
# http://localhost:3000cd services/api
uv run uvicorn app.main:app --reload
# http://localhost:8000pnpm devCopy .env.example to .env and fill in your values.
| Variable | Description | Default |
|---|---|---|
| NEXT_PUBLIC_SUPABASE_URL | Supabase project URL for the web app | |
| NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY | Supabase publishable key for the web app | |
| NEXT_PUBLIC_APP_URL | Public frontend URL for auth redirects | http://localhost:3000 |
| NEXT_PUBLIC_API_URL | Backend API URL | http://localhost:8000 |
| APP_ENV | Backend environment | development |
| API_CORS_ORIGINS | Allowed origins; accepts a comma-separated list or JSON array | http://localhost:3000 |
| SUPABASE_URL | Supabase URL for the backend | |
| SUPABASE_PUBLISHABLE_KEY | Supabase publishable key for the backend | |
| SUPABASE_SERVICE_ROLE_KEY | Supabase service role key for backend data access | |
| LLM_PROVIDER | fake, gemini, groq, mistral, or cerebras |
fake |
| GEMINI_API_KEY | Required when Gemini is the primary or fallback provider | |
| GROQ_API_KEY | Required when Groq is the primary or fallback provider | |
| MISTRAL_API_KEY | Required when Mistral is the primary or fallback provider | |
| CEREBRAS_API_KEY | Required when Cerebras is the primary or fallback provider | |
| LLM_FALLBACKS | Optional comma- or space-separated fallback providers | |
| AI_GENERATION_RATE_LIMIT | Maximum AI-generation requests in the configured window | 5 |
| AI_GENERATION_RATE_WINDOW_SECONDS | Duration of the AI-generation rate-limit window, in seconds | 60 |
# Start local Supabase stack (requires Docker Desktop + WSL2)
pnpm supabase start
# Copy output into .env:
# API URL -> NEXT_PUBLIC_SUPABASE_URL and SUPABASE_URL
# publishable key -> NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY and SUPABASE_PUBLISHABLE_KEY
# service_role -> SUPABASE_SERVICE_ROLE_KEYDocker Desktop must be running before pnpm supabase start.
If startup fails with failed to read signing keys, generate them with:
pnpm setup:keysThis creates supabase/signing_keys.json (gitignored) with the correct format.
See supabase/README.md for full setup instructions.
-
Start Supabase with Docker Desktop running:
pnpm supabase start. -
Copy the local URL and publishable key into
apps/web/.env.local:NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321 NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=<local publishable key> NEXT_PUBLIC_APP_URL=http://localhost:3000
-
Restart the frontend dev server after changing env vars.
-
Request a password reset from the local web app.
-
Open Mailpit at http://127.0.0.1:54324 to inspect auth emails.
The Supabase API gateway at http://127.0.0.1:54321 is not the inbox. If you
open the wrong URL or path and see {"message":"no Route matched with those values"},
use the Mailpit URL above.
pnpm --filter web dev # development server
pnpm --filter web build # production build
pnpm --filter web start # serve production buildcd services/api
uv sync
uv run uvicorn app.main:app --reload # dev server (port 8000)
uv run uvicorn app.main:app --host 0.0.0.0 # productionHealth check: GET http://localhost:8000/health
See services/api/README.md for backend docs.
Note: Not verified locally -- WSL2 + Docker Desktop required.
cp .env.example .env
pnpm supabase start # Supabase separately
docker compose up # web (3000) + api (8000)| Script | Command | Description |
|---|---|---|
| dev | pnpm dev | Start all dev servers |
| build | pnpm build | Build all packages |
| lint | pnpm lint | Lint all packages |
| typecheck | pnpm typecheck | TypeScript check all packages |
| test | pnpm test | Run all unit tests |
| format | pnpm format | Format all files with Prettier |
| format:check | pnpm format:check | Check formatting |
| Script | Command | Description |
|---|---|---|
| dev | pnpm --filter web dev | Start dev server |
| build | pnpm --filter web build | Build for production |
| lint | pnpm --filter web lint | ESLint |
| typecheck | pnpm --filter web typecheck | TypeScript check |
| test | pnpm --filter web test | Vitest unit tests |
| test:e2e | pnpm --filter web test:e2e | Playwright E2E |
| Script | Command | Description |
|---|---|---|
| Run | uv run uvicorn app.main:app | FastAPI dev server |
| Test | uv run pytest | pytest suite |
| Lint | uv run ruff check app/ tests/ | Ruff linter |
| Format | uv run ruff format --check app/ | Ruff formatter |
| Type check | uv run mypy app/ | mypy (non-blocking) |
# Frontend unit tests (Vitest)
pnpm --filter web test
# Frontend E2E (Playwright -- requires dev server)
pnpm --filter web test:e2e
# Backend tests (pytest)
cd services/api && uv run pytestpnpm format:check
pnpm lint
pnpm typecheck
cd services/api && uv run ruff check app/ tests/
cd services/api && uv run mypy app/Pre-commit hooks (Husky + lint-staged) run Prettier on staged files automatically.
- Connect the GitHub repository to Vercel.
- Set apps/web as root directory.
- Add all required
NEXT_PUBLIC_*environment variables. - Set NEXT_PUBLIC_API_URL to the production backend URL.
Deployment URL: lazy-lands.com
- Build with services/api/Dockerfile.
- Set backend env vars (APP_ENV=production, Supabase keys, LLM keys).
- Set API_CORS_ORIGINS to the Vercel frontend URL.
- Health check: GET /health.
Note: Dockerfiles are structurally correct but not verified locally (WSL2 + Docker required).
- The backend abstracts the LLM behind an LlmProvider port.
- In dev/test: FakeLlmProvider returns deterministic JSON without API calls.
- In production: a configured Gemini, Groq, Mistral, or Cerebras provider calls its model.
- All LLM outputs are validated with Pydantic before storage or return.
- Prompts are versioned inside their owning feature module when implemented.
| Resource | URL |
|---|---|
| Production application | lazy-lands.com |
| Public demo | lazy-lands.com/demo |
| Slides (spanish) | Presentation PDF |
| Video | Demo |
| Document | Purpose |
|---|---|
| PRODUCT.md | Product principles, flow, entity model |
| DESIGN.md | Design system, tokens, components, motion |
| docs/README.md | Documentation index and reading guide |
| AGENTS.md | Instructions for AI coding agents |
The MVP includes PDF export; its generated-session export deliberately excludes private notes. The following opportunities remain deferred:
- Richer session logging, including private notes and additional entity-change fields.
- Timeline and advanced filtering for larger campaigns.
- A conversational Scribe that still proposes rather than changing canon automatically.
- RAG, embeddings, and vector search across campaign history.
- Advanced relationship graphs and memory compilation.
- Dark theme and theme settings.
Billing, shared campaigns, a mobile app, and Obsidian sync are not currently planned. See docs/11-backlog.md for rationale and implementation constraints.
