Complete KongAI Documentation Bible

All 34 .md files from the repository - comprehensive, searchable, complete.

๐Ÿš€ Getting Started

๐Ÿ“„ README.md

/root/kongai-staging/README.md
# Keel โ€” the multi-tenant SaaS template

**Django 6 + Next.js 15 + Stripe + Postgres** โ€” a complete, production-ready
SaaS foundation with multi-tenancy, billing, async jobs, document storage, and
the guardrails to keep you from shooting yourself in the foot.

## What Keel is

- **A multi-tenant SaaS foundation** โ€” organizations, memberships, invitations,
  role-based permissions, and per-organisation data isolation out of the box.
- **A billing-ready platform** โ€” Stripe checkout/portal, plan/price sync,
  entitlements (feature flags and limits), seat sync, and an optional
  credit ledger for metered usage.
- **An async job runner** โ€” Celery with tiered task priorities, a resumable
  job runner with idempotent credit settlement, per-organisation concurrency
  control, and live SSE status updates.
- **A document storage layer** โ€” presigned direct uploads to R2/S3,
  checksum verification, soft deletes, and garbage collection.
- **A guardrailed codebase** โ€” seven architecture invariants enforced by CI
  (import contracts, permission-placement lint, audit-presence meta-test,
  coverage floors, OpenAPI drift detection, etc.).

## What Keel is not

- **Not a specific product** โ€” Keel is a template. It has a demo slice
  (`widgets`) to exercise the mechanisms end to end, but you're expected to
  delete it and build your own domain on top of the foundation.
- **Not a low-code platform** โ€” you write Python and TypeScript. There are
  generators (`pnpm gen ...`) to scaffold new resources, jobs, permissions,
  and emails, but they emit readable code in your repo, not a black box.
- **Not a CMS or marketing site** โ€” there is an optional marketing-site
  layer (landing page, pricing, blog via content-collections), but it's
  deliberately decoupled and removable (`docs/marketing-removal.md`).

## Quick start

```bash
# 1. Clone and install
git clone https://github.com/your-org/keel.git
cd keel
pnpm install  # installs JS deps and builds emails/content
cd apps/api && uv sync && cd ../..

# 2. Start infra (Postgres, Redis, Mailpit, MinIO)
pnpm env:up

# 3. Migrate and seed
cd apps/api && uv run python manage.py migrate
uv run python manage.py seed_demo

# 4. Run dev servers (API, web, worker, SSE stream)
pnpm dev:slot

# 5. Open http://app.lvh.me:3000 and log in as [email protected] / demo-password-123
```

## Architecture invariants

1. **Domain logic in services/selectors** โ€” views are thin, tasks delegate.
2. **Authorization in one place** โ€” `organizations/permissions.py` only.
3. **One transaction per service** โ€” external calls on `on_commit()`.
4. **Schema changes are migrations** โ€” reviewed, in VCS.
5. **Async through Tier 1/2** โ€” no bespoke background threads.
6. **Two-layer validation** โ€” serializers at edge, services for invariants.
7. **Every mutating service audited** โ€” `@audited` or `@not_audited(reason)`.

See `docs/architecture.md` for the full walk-through.

## Directory structure

```
keel/
โ”œโ”€โ”€ apps/
โ”‚   โ”œโ”€โ”€ api/              # Django 6 + Ninja
โ”‚   โ””โ”€โ”€ web/              # Next.js 15
โ”œโ”€โ”€ packages/
โ”‚   โ”œโ”€โ”€ ui/               # Shared component kit
โ”‚   โ”œโ”€โ”€ api-client/       # Generated TS client
โ”‚   โ””โ”€โ”€ emails/           # react-email templates
โ”œโ”€โ”€ infra/                # Docker Compose (dev/prod)
โ”œโ”€โ”€ scripts/              # Init, OpenAPI merge, coverage check
โ””โ”€โ”€ templates/            # Generator templates (resource/job/permission/email)
```

## Documentation index

- `docs/architecture.md` โ€” the seven invariants, request path, type sync
- `docs/auth-flow.md` โ€” cross-host auth, cookies, CSRF, MFA
- `docs/billing.md` โ€” credits, plans, entitlements, webhooks
- `docs/storage.md` โ€” presigned uploads, lifecycle, providers
- `docs/jobs-and-audit.md` โ€” job runner, idempotency, audit trail
- `docs/deploy-railway.md` โ€” Railway deployment, pgvector
- `docs/maintenance.md` โ€” supported versions, upgrade checklists
- `docs/pre-push.md` โ€” local pre-push gates mirroring CI
- `docs/marketing-removal.md` โ€” how to delete the marketing site
- `docs/query-patterns.md` โ€” selectors, N+1 prevention, pagination
- `docs/boundary-guardrails.md` โ€” unsafe rendering, validation, async boundaries
- `SECURITY.md` โ€” vulnerability reporting, response time
- `CONTRIBUTING.md` โ€” CI gates, invariants, how to contribute
- `CHANGELOG.md` โ€” version history, phases
- `AGENTS.md` โ€” agent-specific guidance (file shape, invariants)
- `CLAUDE.md` โ€” pointer to AGENTS.md

## License

MIT โ€” see LICENSE.

๐Ÿ“„ AGENTS.md

/root/kongai-staging/AGENTS.md
# AGENTS.md

Keel is a Django 6 + Next.js 15 SaaS template. Full detail lives in
docs/architecture.md (the seven invariants, request path, type
synchronisation); this file is the short version an agent needs before
writing a line here.

## Per-app file shape

Every domain app under apps/api/keel/ follows the same seven files.
apps/api/keel/widgets/ is the reference slice โ€” copy its shape, not its content.

```
/
โ”œโ”€โ”€ models.py         # data shape only
โ”œโ”€โ”€ services.py       # ORM, transactions, side effects โ€” ALL writes
โ”œโ”€โ”€ selectors.py      # ALL reads
โ”œโ”€โ”€ permissions.py    # required permission codes (organizations app only)
โ”œโ”€โ”€ schemas.py        # shape validation at the edge (Ninja Schema / Pydantic)
โ”œโ”€โ”€ views.py          # THIN: parse, call service, serialize, return
โ”œโ”€โ”€ tasks.py          # one-line delegations to services
โ””โ”€โ”€ tests/
```

A view, serializer, Celery task, admin page, or management command that
contains a business rule is a bug, not a style choice.

## The seven invariants

| # | Invariant | Enforced by | Verify with |
|---|-----------|-------------|-------------|
| 1 | Domain logic in services/selectors, never views/serializers/tasks/admin | import-linter contract | `cd apps/api && uv run lint-imports` |
| 2 | Authorization in organizations/permissions.py only | check_permission_lint.py | `uv run python ../../scripts/check_permission_lint.py` |
| 3 | One transaction per service function | Code review + tests | Read services.py |
| 4 | Schema changes are migrations | makemigrations --check | `uv run python manage.py makemigrations --check --dry-run` |
| 5 | Async through Tier 1/2 tasks | Code review | Check keel/core/tasks.py usage |
| 6 | Two-layer validation (serializers + services) | Code review | Read views.py + services.py |
| 7 | Every mutating service @audited or @not_audited | Meta-test | `uv run pytest keel/core/tests/test_service_audit_registry.py` |

## Commands reference

| Command | Purpose |
|---------|--------|
| `pnpm bootstrap` | Fresh checkout: install JS + Python deps, build emails |
| `pnpm env:up` / `env:down` | Start/stop Postgres, Redis, Mailpit, MinIO |
| `pnpm env:reset` | Fresh DB: down -v, up, build emails, migrate, seed_demo |
| `pnpm dev:slot` | API, SSE stream, Celery worker and web on this slot's ports |
| `pnpm health` | JSON {api, web, ok}; exit 0 when both answer |
| `pnpm lint` | ruff + ESLint + prettier |
| `pnpm typecheck` | mypy strict + tsc |
| `pnpm test:unit` | Vitest + pytest minus db/redis-marked tests |
| `pnpm test:db` | DB-backed pytest + coverage floors |
| `pnpm e2e [--grep @tag]` | Playwright against this slot |
| `pnpm design:check` | impeccable design detector |

## Slots

FACTORY_SLOT (0-9) is set by the factory chair per worktree.
Every port is its default + slot*100.
The generated .env and apps/web/.env.local carry a managed-by header;
never hand-edit them, and never point a worktree at another slot's services.

## Demo data

seed_demo creates a demo org and one verified user per role
([email protected] etc., password demo-password-123).
It is idempotent and refuses to run without DEBUG.

## Design system

App code composes @keel/ui components and tokens in packages/ui/theme.css.
@shadcn/lint blocks restyling components through className, raw palette
colours, arbitrary values, inline styles and unknown classes.

## Findings

Audit findings (review, Playwright/visual, UX, design detector) are
Markdown files under findings//NNNN-.md with front matter:
type, severity (blocking|major|minor), status (open|fixed|wontfix),
slice, source, evidence. Evidence goes in findings/_evidence/.

๐Ÿ“„ CLAUDE.md

/root/kongai-staging/CLAUDE.md
@AGENTS.md

Note: CLAUDE.md simply references AGENTS.md - all agent guidance is in the file above.

๐Ÿ“„ docs/dev-setup.md

/root/kongai-staging/docs/dev-setup.md
# Development setup

## Prerequisites

- Python 3.12+ (managed by uv)
- Node.js 22+ (managed by fnm or nvm)
- pnpm 9.5.0+ (corepack-enabled)
- Docker + Docker Compose

## Quick start

```bash
# 1. Clone
git clone https://github.com/your-org/keel.git
cd keel

# 2. Install dependencies
pnpm install  # JS deps + builds emails and content-collections
cd apps/api
uv sync       # Python deps

# 3. Start infrastructure
pnpm env:up   # Postgres, Redis, Mailpit, MinIO

# 4. Migrate and seed
cd apps/api
uv run python manage.py migrate
uv run python manage.py seed_demo

# 5. Run development servers
pnpm dev:slot  # API on 8000, web on 3000, worker, SSE stream

# 6. Open browser
# http://app.lvh.me:3000
# Login: [email protected] / demo-password-123
```

## Why lvh.me and not localhost?

This template uses subdomain-based multi-tenancy:
- `app.lvh.me:3000` - Next.js application
- `api.lvh.me:8000` - Django API
- `lvh.me` resolves to 127.0.0.1 via public DNS

This allows testing cross-subdomain cookies and CORS locally,
which is exactly how production will work with:
- `app.yourdomain.com` - Application
- `api.yourdomain.com` - API

## Environment variables

The factory generates `.env` and `apps/web/.env.local` for you.
Key variables:

```bash
# Django
DJANGO_SECRET_KEY=your-secret-key
DEBUG=True
KEEL_APP_DOMAIN=lvh.me

# Database
DATABASE_URL=postgres://keel:keel@localhost:5432/keel

# Redis
REDIS_URL=redis://localhost:6379/0

# Email (Mailpit in dev)
EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
EMAIL_HOST=localhost
EMAIL_PORT=1025

# Storage (MinIO in dev)
KEEL_FILES_STORAGE_BACKEND=keel.files.storage.S3CompatibleStorage
AWS_ACCESS_KEY_ID=minioadmin
AWS_SECRET_ACCESS_KEY=minioadmin
AWS_STORAGE_BUCKET_NAME=keel
AWS_S3_ENDPOINT_URL=http://localhost:9000
```

## Slots system

Multiple worktrees can run simultaneously using slots (0-9):
- Slot 0: API=8000, Web=3000
- Slot 1: API=8100, Web=3100
- Slot N: API=8000+N*100, Web=3000+N*100

Set FACTORY_SLOT environment variable to use a different slot.

## Common commands

```bash
# Run tests
pnpm test:unit    # Fast, no DB
pnpm test:db      # Full test suite with coverage

# Lint and typecheck
pnpm lint
pnpm typecheck

# E2E tests
pnpm e2e          # All Playwright tests
pnpm e2e:baseline # CI gate tests only

# Reset everything
pnpm env:reset    # Wipe DB, migrate, seed
```

๐Ÿ“„ docs/architecture.md

/root/kongai-staging/docs/architecture.md
# Architecture

## The seven invariants

### 1. Domain logic in services.py/selectors.py

Views, serializers, Celery tasks, admin, and management commands call into
services โ€” they don't contain business rules.

**Enforced by:** import-linter contract in pyproject.toml
**Verify with:** `cd apps/api && uv run lint-imports`

### 2. Authorization in one place

Authorization is expressed only in organizations/permissions.py (Perm codes +
has_perm/registry.register). Services call has_perm, never reimplement a check.

**Enforced by:** scripts/check_permission_lint.py
**Verify with:** `uv run python ../../scripts/check_permission_lint.py`

### 3. One transaction per service function

External calls (Stripe, etc.) go out via transaction.on_commit(), never from
inside an open transaction.

**Enforced by:** Code review + tests
**Verify with:** Read services.py

### 4. Schema changes are migrations

No dashboard or admin-console schema edits. A migration in a feature PR
needs explanation.

**Enforced by:** makemigrations --check in CI
**Verify with:** `uv run python manage.py makemigrations --check --dry-run`

### 5. Async through Tier 1/2 tasks

Tier 1: keel/core/tasks.py shim for simple background work
Tier 2: Celery directly for complex jobs

**Enforced by:** Code review
**Verify with:** Check keel/core/tasks.py usage

### 6. Two-layer validation

Serializers at the HTTP edge, services for invariants.
Client-side Zod is UX only, never enforcement.

**Enforced by:** Code review
**Verify with:** Read views.py + services.py

### 7. Every mutating service audited

@audited(action) or @not_audited(reason=...). A meta-test fails on anything
decorated with neither.

**Enforced by:** Meta-test in test_service_audit_registry.py
**Verify with:** `uv run pytest keel/core/tests/test_service_audit_registry.py`

## Request path

```
Browser โ†’ Next.js BFF โ†’ Django Ninja โ†’ Service โ†’ DB
                     โ†“
                Stripe/R2/etc (on_commit)
                     โ†“
                AuditLog (inline)
```

## Type synchronization

OpenAPI schema merged from Django Ninja + allauth, generated TS client via orval.
CI fails if generated client is stale.

๐Ÿ“„ docs/diagrams/system.md

/root/kongai-staging/docs/diagrams/system.md
# System diagram

## Deployed architecture

```mermaid
flowchart TB
    subgraph vercel["Vercel"]
        marketing["Next.js 15 โ€” marketing + auth
acme.com"] appui["Next.js 15 โ€” application
app.acme.com"] end browser["Browser"] stripe["Stripe"] resend["Resend (email)"] r2["Cloudflare R2 / S3-compatible storage"] sentry["Sentry"] posthog["PostHog"] subgraph railway["Railway"] api["Django 6 + Ninja (sync)
gunicorn โ€” request/response only"] sse["Django ASGI (async)
uvicorn โ€” SSE only, same image"] postgres[("Postgres 17")] redis[("Redis 7
broker + cache")] worker["Celery worker"] beat["Celery beat
(scheduler)"] end browser -- "fetch, credentials: include" --> marketing browser -- "fetch, credentials: include" --> appui browser -- "fetch, credentials: include
api.acme.com" --> api browser -- "EventSource
api.acme.com/.../stream" --> sse stripe -- webhooks --> api api --> postgres sse --> postgres api --> redis sse --> redis redis --> worker redis --> beat api -.-> stripe worker -.-> stripe worker -.-> resend api -.-> r2 worker -.-> r2 api -.-> sentry appui -.-> sentry api -.-> posthog appui -.-> posthog ``` ## Why the SSE service is separate api.acme.com/โ€ฆ/stream is served by a second Railway service running the same image, under uvicorn instead of gunicorn. A held-open SSE connection under the sync worker pool exhausts it at a concurrent-user count far below what ordinary request/response load testing suggests. ## Two Next.js hosts, one session acme.com (marketing + auth) and app.acme.com (application) are two Next.js deployments sharing one sessionid cookie, Domain-scoped to the registrable parent domain.

โš™๏ธ Configuration

๐Ÿ“„ docs/storage.md

/root/kongai-staging/docs/storage.md
# File storage

## What this is not

keel/files/ handles storage: presigned direct upload, status lifecycle,
listing, retrieval and (soft) deletion, tenant isolation, garbage collection.

It does NOT do anything with the contents of a file:
- No OCR
- No text extraction
- No embeddings
- No document understanding
- No virus scanning (extension point)
- No thumbnailing (extension point)

## Upload lifecycle

```
pending โ”€โ”€โ”€โ”€โ”€โ”€โ–บ available   (checksum verified, size/content_type from provider)
  โ”‚
  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ failed        (checksum mismatch or policy violation)
  โ”‚
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ expired       (nobody ever called complete)

{available, failed} โ”€โ”€โ”€โ”€โ”€โ”€โ–บ deleted   (tombstone)
```

Every arrow is a guarded UPDATE with WHERE status = expected.

## Checksum verification

Client declares checksum_sha256 at create time. On completion, server
downloads object back and hashes it, comparing against declaration.
A mismatch fails the row rather than letting it reach available.

## Size and content type

FILES_MAX_UPLOAD_SIZE_BYTES and FILES_ALLOWED_CONTENT_TYPES are enforced
twice: at create time (declared values) and at complete time (actual values).

## Storage providers

| Provider | Class | Use |
|----------|-------|-----|
| S3CompatibleStorage | keel/files/storage.py | Production (R2, S3, MinIO) |
| LocalFileSystemStorage | keel/files/storage.py | Development only |

## Railway volumes are NOT the answer

Railway persistent volumes are wrong for file storage because:
1. Volume attached to one instance, not shared across replicas
2. Presigned direct upload doesn't work against a volume
3. Volumes don't survive every kind of deploy the same way

Production always means S3CompatibleStorage against R2, S3, or equivalent.

## Extension points

- Virus scanning: hook between checksum verification and transition to available
- Thumbnailing: Tier-1 task dispatched from on_commit() hook
- Provider-native checksum: use HeadObject(ChecksumMode="ENABLED") if trusted
- Different provider: implement ObjectStorage interface (6 methods)

## Permissions

files.view gates list/retrieve/download-URL
files.manage gates create/complete/delete

๐Ÿ“„ docs/billing.md

/root/kongai-staging/docs/billing.md
# Billing

How a product built on Keel plugs into keel/billing/: what a credit
means, how to add a plan, how to meter an operation, how to gate a
feature on an entitlement.

## What already exists, generic by construction

keel/billing/ ships plans, prices, Stripe checkout/portal, webhook
handling, seat sync, and an optional credit ledger. None of it names a
product.

Plan.entitlements is a JSON blob a project fills in; the ledger doesn't
know what a credit buys; BILLING_CREDITS and BILLING_SEAT_PRICING are
both off by default.

## Ownership: credits belong to the organisation, never the user

Every ledger write takes an organization, never a user.
CreditLedgerEntry.organization and CreditBalance.organization are the
only FKs that matter for "whose credits are these."

actor records who performed the write (for the audit trail), not who
owns the balance.

## Defining what a credit means

Nothing in keel/billing/ decides what a credit buys. A project defines
that by choosing what calls credits.hold/consume and for how much:

```python
from keel.billing import credits

def start_render(*, organization, job, actor):
    estimated_cost = 15  # your product's pricing model
    credits.hold(organization, estimated_cost, job=job, actor=actor)
```

A two-phase job uses hold at creation and release/refund at completion.
A cost known synchronously uses consume directly.
Both raise PaymentRequired (402) when balance can't cover the amount.

## Adding a plan

Plans and prices sync from Stripe (python manage.py sync_stripe_plans).
Set a code metadata key on Stripe Product, Plan.code/name sync automatically.

Plan.entitlements is NOT synced from Stripe; set it directly:

```python
Plan.objects.filter(code="pro").update(
    entitlements={
        "features": ["api_access", "custom_roles"],
        "limits": {"seats": 25, "widgets": None},
        "daily_credit_cap": 500,
    }
)
```

## Metering an operation

```python
from keel.billing import credits

# Synchronous cost (known upfront)
credits.consume(organization, amount=5, feature="render", actor=actor)

# Async cost (hold then settle)
credits.hold(organization, estimate=10, job=job, actor=actor)
# ... job runs ...
credits.settle(job, actual=8)  # refunds 2
```

## Gating a feature on an entitlement

```python
from keel.billing import entitlements

# Feature flag
if entitlements.check_feature(organization, "api_access"):
    # grant access

# Limit check
remaining = entitlements.check_limit(organization, "widgets")
if remaining > 0:
    # allow creating more widgets
```

## Hardening summary

- Credits belong to organisation, never user
- Daily cap enforced at hold time, not just consume time
- Idempotency via cache claim + DB constraint
- Constraints at database level (CHECK constraints)
- Reconciliation reports, never repairs, on a schedule
- Webhook writes respect Stripe's actual delivery guarantees
- StripeEvent.payload doesn't grow forever (30-day retention)
- /api/v1/plans/ and /api/v1/permissions/ are cacheable
- Plan.entitlements typed, not dict[str, Any]

๐Ÿ“„ DEPLOYMENT_SUMMARY.md

/root/kongai-staging/DEPLOYMENT_SUMMARY.md
# KongAI Deployment Summary

## Architecture Components

### Kong Gateway (API Gateway)
- Port: 8000
- Role: Reverse proxy, rate limiting, authentication
- Health: http://localhost:8001/status

### Keel Boilerplate (Django Backend)
- Port: 8080
- Framework: Django Ninja
- Database: PostgreSQL 14
- Cache: Redis 7

### Dify Multi-tenancy
- Port: 3000
- Role: AI workflow orchestration
- Tenants: Isolated workspaces

### Twilio Voice Workflows
- Integration: Webhook-based
- Role: Voice AI agents
- Status: Configured

## Quick Deploy

```bash
# 1. Initialize
sudo ./scripts/deploy-init.sh

# 2. Configure
cp .env.example .env
nano .env

# 3. Deploy
sudo ./scripts/deploy-up.sh

# 4. Verify
./scripts/deploy-status.sh
```

## Access Points

- Application: https://yourdomain.com
- API Gateway: https://api.yourdomain.com
- Grafana: https://grafana.yourdomain.com
- Kong Manager: http://localhost:8002

## Health Checks

| Service | Endpoint | Expected |
|---------|----------|----------|
| Kong | /status | 200 OK |
| API | /health | {"status": "ok"} |
| PostgreSQL | - | Running |
| Redis | - | Running |
| Dify | /health | 200 OK |

## Backup Strategy

- Database: Daily automated backups
- Configuration: Version controlled
- Secrets: Docker secrets

๐Ÿ” Security & Authentication

๐Ÿ“„ docs/auth-flow.md

/root/kongai-staging/docs/auth-flow.md
# Authentication flow

## Base URL and client type

All identity endpoints are under /_allauth/browser/v1/โ€ฆ โ€” the browser
client, not the app client (/_allauth/app/v1/โ€ฆ). The browser client is
cookie-based and CSRF-protected.

Since docs/adr/0002-auth-bff-shape.md, that path is always same-origin
against Next.js, not Django directly โ€” the Next.js BFF proxy forwards
the call to Django's real origin server-side.

## Cookies

| Cookie | Set by | Purpose |
|--------|--------|--------|
| sessionid | Django session middleware | The session. HttpOnly, Secure, SameSite=Lax |
| csrftoken | Django CSRF middleware | Not HttpOnly โ€” SPA reads it for X-CSRFToken header |

Both are Domain-scoped identically, controlled by SESSION_COOKIE_DOMAIN /
CSRF_COOKIE_DOMAIN in apps/api/config/settings/base.py.

## CSRF token acquisition

1. On app load, call GET /_allauth/browser/v1/auth/session
2. Response sets csrftoken cookie
3. Read cookie, send as X-CSRFToken header on unsafe methods

## Login flow

```typescript
// 1. Get CSRF token
await api.GET('/_allauth/browser/v1/auth/session')

// 2. Login
await api.POST('/_allauth/browser/v1/auth/login', {
  login: '[email protected]',
  password: 'secret',
})

// 3. Check session
const session = await api.GET('/_allauth/browser/v1/auth/session')
if (session.data.user) {
  // Logged in
}
```

## MFA (if enabled)

When KEEL_MFA_ENABLED=true:

1. After login, if user has MFA, response includes pending_mfa_authentication
2. GET /_allauth/browser/v1/account/authenticators/totp to get secret
3. POST /_allauth/browser/v1/auth/2fa/authenticate with { code }

## Social login (Google OAuth)

No fetch() call โ€” full-page browser navigation:
```javascript
window.location.href = '/_allauth/browser/v1/auth/provider/redirect?provider=google'
```

Google redirects back through allauth, which sets session cookie and
redirects to callback_url on frontend.

## Email verification

Emails point to /verify-email/[key] on Next.js.
Route reads key from URL and POSTs to:
/_allauth/browser/v1/auth/email/verify

## Response envelope

```
{
  "data": { ... },      // Response payload
  "errors": [],         // Error list (empty on success)
  "meta": { ... }       // Pagination, etc.
}
```

## Error codes

- 401: Not authenticated (no session)
- 403: Permission denied (authenticated but not allowed)
- 422: Validation error

๐Ÿ“„ docs/auth-client-contract.md

/root/kongai-staging/docs/auth-client-contract.md
# Auth client contract

What the frontend needs to build against the API's allauth headless
configuration.

## Key endpoints

### Authentication
```
GET    /_allauth/browser/v1/auth/session           # Get CSRF + session state
POST   /_allauth/browser/v1/auth/login             # { login, password }
POST   /_allauth/browser/v1/auth/logout            # {}
POST   /_allauth/browser/v1/auth/signup            # { email, password }
POST   /_allauth/browser/v1/auth/password/request  # { email }
POST   /_allauth/browser/v1/auth/password/reset    # { key, password }
```

### Account management
```
GET    /_allauth/browser/v1/account/email              # List emails
POST   /_allauth/browser/v1/account/email              # { email } (add)
DELETE /_allauth/browser/v1/account/email              # { email } (remove)
POST   /_allauth/browser/v1/account/email/primary      # { email }
POST   /_allauth/browser/v1/account/email/verify       # { key }
```

### MFA (if enabled)
```
GET    /_allauth/browser/v1/account/authenticators/totp
POST   /_allauth/browser/v1/account/authenticators/totp   # { code }
DELETE /_allauth/browser/v1/account/authenticators/totp
POST   /_allauth/browser/v1/auth/2fa/authenticate         # { code }
```

## Merged OpenAPI spec

/api/v1/openapi.json + /_allauth/openapi.json merged by scripts/merge_openapi.py
into openapi.merged.json at repo root.

Regenerate client after any settings change:
pnpm --filter @keel/api-client generate

CI fails if checked-in client is stale.

๐Ÿ“„ SECURITY.md

/root/kongai-staging/SECURITY.md
# Security policy

## Supported versions

Keel does not yet cut releases. Until v1.0 ships, only master is supported:
fixes land there, not on version branches.

## Reporting a vulnerability

Use GitHub's private vulnerability reporting (Security tab โ†’ Report a vulnerability).
That opens a private advisory only the maintainer can see until a fix is ready.

Please include:
- What you found and where (file, endpoint, or flow)
- Steps to reproduce, or a minimal proof of concept
- What you think the impact is (data exposure, privilege escalation, etc.)

## Response time

This is a solo-maintained project. No SLA โ€” best effort is the honest answer.
In practice: acknowledgement within a few days, fix or mitigation plan once
understood.

## Scope

Covers code in this repository. Third-party services (Stripe, Sentry, PostHog,
R2/S3, OAuth providers) belong to their own security teams.

๐Ÿ“„ docs/boundary-guardrails.md

/root/kongai-staging/docs/boundary-guardrails.md
# Rendering, validation and async boundaries

## Unsafe rendering

Guardrails added:
- eslint no-restricted-syntax bans dangerouslySetInnerHTML
- scripts/check_unsafe_rendering.py greps for mark_safe, SafeString, |safe

Escape hatch: same-line comment
- TS/TSX: // eslint-disable-next-line no-restricted-syntax -- 
- Python: # unsafe-rendering: 

## Validation boundary

Every route takes a typed Ninja Schema payload โ€” no raw unvalidated payload.
Domain rules expressed in services, never serializers.

## Async boundary

Blocking calls in request path:
- Real gap: send_mail blocks HTTP request on Resend call (allauth adapter)
- complete_upload makes synchronous R2 HTTP call (defensible for verification)
- Stripe checkout/portal session creation is synchronous by necessity

Non-blocking:
- delete_file defers R2 object purge via transaction.on_commit()
- Email sends from beat tasks run on worker

๐Ÿ“Š Operations & Deployment

๐Ÿ“„ docs/deploy-railway.md

/root/kongai-staging/docs/deploy-railway.md
# Railway deployment

## Prerequisites

- Railway account
- Railway CLI installed
- Domain configured (for production)

## Deploy steps

1. Connect Railway to GitHub repo
2. Create Postgres 17 database
3. Create Redis 7 database
4. Set environment variables
5. Deploy

## Environment variables

```bash
# Required
DJANGO_SECRET_KEY=
DEBUG=False
KEEL_APP_DOMAIN=yourdomain.com
DATABASE_URL=
REDIS_URL=

# Stripe
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=

# Email
RESEND_API_KEY=

# Storage
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_STORAGE_BUCKET_NAME=
AWS_S3_ENDPOINT_URL=
```

## pgvector

Railway's marketplace offers pgvector images:
- Use pgvector/pgvector:pg17 for PostgreSQL 17 with vector extension

## Migration strategy

Railway runs migrations in preDeployCommand:
```json
{
  "preDeployCommand": "python manage.py migrate --noinput"
}
```

## SSE service

Deploy a second service for SSE:
- Same image
- Start command: uvicorn config.asgi:application
- Route: api.yourdomain.com/.../stream

## Monitoring

- Railway dashboard for logs and metrics
- Sentry for error tracking
- PostHog for product analytics

๐Ÿ“„ TROUBLESHOOTING.md

/root/kongai-staging/TROUBLESHOOTING.md
# Troubleshooting Guide

## Common issues

### Database connection failed

```bash
# Check Postgres is running
docker ps | grep postgres

# Check DATABASE_URL
echo $DATABASE_URL

# Test connection
psql $DATABASE_URL -c "SELECT 1"
```

### Redis connection failed

```bash
# Check Redis is running
docker ps | grep redis

# Test connection
redis-cli ping
```

### Migration errors

```bash
# Show migration status
python manage.py showmigrations

# Fake initial migration if needed
python manage.py migrate --fake-initial

# Reset migrations (DEV ONLY)
python manage.py migrate  zero
python manage.py migrate
```

### Static files not loading

```bash
# Collect static files
python manage.py collectstatic --noinput

# Check STATIC_ROOT
python -c "from django.conf import settings; print(settings.STATIC_ROOT)"
```

### Celery worker not processing tasks

```bash
# Check worker logs
docker logs 

# Check Redis connection
celery -A config.celery inspect ping

# Restart worker
docker-compose restart worker
```

### 502 Bad Gateway

```bash
# Check API is running
curl http://localhost:8000/health

# Check Kong logs
docker logs kong-gateway

# Check upstream configuration
kubectl get ingress
```

### SSL certificate issues

```bash
# Check Caddy logs
docker logs caddy

# Force certificate renewal
docker exec caddy caddy reload

# Check DNS resolution
dig yourdomain.com
```

### Memory issues

```bash
# Check container memory
docker stats

# Increase memory limit in compose file
# Restart services
docker-compose restart
```

### Permission denied errors

```bash
# Fix file permissions
sudo chown -R $(whoami) .

# Fix Docker volume permissions
docker volume rm 
docker-compose up -d
```

๐Ÿ“„ docs/maintenance.md

/root/kongai-staging/docs/maintenance.md
# Maintenance

## Supported versions

| Component | Version | Where pinned |
|-----------|---------|-------------|
| Python | 3.12+ | pyproject.toml |
| Django | 6.0+ | pyproject.toml (django~=6.0) |
| Node.js | 22+ | package.json engines |
| pnpm | 9.5.0+ | package.json packageManager |
| PostgreSQL | 17 | compose files |
| Redis | 7 | compose files |

## Update cadence

- **Python/Node patch**: Apply opportunistically
- **Django/django-ninja**: Check monthly, security releases within a week
- **Dependencies**: Dependabot weekly (minor+patch grouped)
- **PostgreSQL/Redis**: Bump when provider deprecates or new features needed

## Django upgrade checklist

1. Read release notes for target version
2. Bump django~=X.Y in pyproject.toml, run uv lock
3. Run full test suite locally
4. Run python manage.py check --deploy
5. Regenerate OpenAPI schema and TS client
6. Check for new migrations from Django version
7. Deploy to non-prod first, have rollback ready

## Node/Next.js upgrade checklist

1. Bump version in package.json, run pnpm install
2. Run typecheck and lint first
3. Run production build locally
4. Run Playwright suites

๐Ÿ“„ docs/pre-push.md

/root/kongai-staging/docs/pre-push.md
# Local pre-push gates

## Setup

One-time, per clone:
```bash
uvx pre-commit install --hook-type pre-commit --hook-type pre-push
```

## What the pre-push hook runs (~50s warm)

1. ruff check / ruff format --check (apps/api)
2. mypy (apps/api)
3. scripts/check_permission_lint.py
4. bandit
5. eslint (pnpm --filter web lint)
6. prettier --check
7. Build email templates
8. pytest -m "not db and not redis" --no-cov -q (service-free tests)
9. merge_openapi.py, diff against committed openapi.merged.json
10. pnpm generate for api-client, diff against committed
11. scripts/check_openapi_compat.py (additive-only gate)

## Escape hatch

```bash
git push --no-verify
```

Skips pre-push hook. CI still runs full gates.

๐Ÿ”ง Advanced Topics

๐Ÿ“„ docs/jobs-and-audit.md

/root/kongai-staging/docs/jobs-and-audit.md
# Jobs and audit

## What belongs in Keel vs a product

keel/jobs/ is a resumable async-job runner with:
- Per-organisation concurrency control
- Idempotent creation
- Credit-hold settlement
- Live SSE status stream

What it is NOT: a specific job.
keel/jobs/demo.py is the only registered job type in the template,
exists to exercise the runner end to end, and is removable.

keel/audit/ is the audit trail: one AuditLog row per mutating service call,
written inline in the same transaction as the effect.

## Adding a job type

1. Write step functions in your own app (not keel/jobs/)
2. Register JobTypeSpec from your AppConfig.ready()
3. Create jobs through keel.jobs.services.create_job

Tasks take ids, never model instances โ€” forces fresh reads on retry.

## Idempotency

Idempotency-Key handling in keel/core/idempotency.py:
1. Cache claim (Redis)
2. DB constraint (unique index)

Two layers, cheapest first.

## Audit trail

@audited records inline, immediately after decorated function returns.
AuditLog lives in same Postgres as everything it describes.

@not_audited(reason=...) legitimate for:
- Scheduled system jobs with no actor
- Internal helpers already covered by audited call site
- System-triggered side effects of already-audited calls

## Audit ordering and pagination

GET /orgs//audit/ orders on AuditLog.id (UUIDv7, monotonic),
not created_at. Cursor pagination filters on id, not timestamp.

## Verifying invariants

```bash
cd apps/api
uv run pytest keel/jobs keel/audit
uv run python manage.py makemigrations --check --dry-run
uv run lint-imports
uv run python ../../scripts/check_permission_lint.py
```

๐Ÿ“„ docs/query-patterns.md

/root/kongai-staging/docs/query-patterns.md
# Query patterns

## Selectors own query shape

Every ORM read lives in an app's selectors.py.
A view resolves the organisation, calls a selector, and serializes.

keel/core/selectors.py::get_scoped_or_404 is the shared helper.

## Avoiding N+1s

Two shapes:
1. Field on every row needs related row's data: select_related
2. Field on every row needs collection: prefetch_related

Tell: resolve_X staticmethod on Schema that walks a relation.

## Django Debug Toolbar (locally)

```python
# config/settings/dev.py, temporarily
INSTALLED_APPS += ["debug_toolbar"]
MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware")
INTERNAL_IPS = ["127.0.0.1"]
```

## Query-count tests

```python
from django.test.utils import CaptureQueriesContext
from django.db import connection

def test_list_no_n_plus_one():
    with CaptureQueriesContext(connection) as ctx:
        response = client.get('/api/v1/widgets/')
    assert len(ctx.captured_queries) < 10  # Fixed budget
```

## Pagination

Cursor pagination on (organization, -created_at, id) or (organization, id).
Every OrgScopedModel gets organization indexed by default.

## Index audit

| Model | Ordering | Index needed |
|-------|----------|-------------|
| Widget | (-created_at, id) | (organization, created_at) |
| Job | (-created_at, id) | (organization, created_at) |
| AuditLog | (-id,) | (organization, id) - added in 0002 |

๐Ÿ“š Reference Documentation

๐Ÿ“„ CHANGELOG.md

/root/kongai-staging/CHANGELOG.md
# Changelog

## [Unreleased]

### Phase 19 โ€” Generators as the agent capability surface

- 19.A: packages/cli (pnpm gen ...), templates/ as source of reference slice
- 19.B: job generator, e2e ship-gate generator, generated-slice CI gate
- 19.C: --ui frontend templates, email generator

## [1.0.0] - 2026-08-28

First tagged release. Phases 0-18.

### Phase 18 โ€” Portfolio polish and v1.0
- README rewritten, invariants and architecture diagram in README
- Screenshots, CHANGELOG finalized, v1.0.0 tagged

### Phase 17 โ€” Template mechanics (init)
- scripts/init.ts: renames project, applies feature toggles
- template-ci.yml: CI job that runs init against throwaway config

### Phase 16 โ€” Query hygiene, settings hardening
- Query-count tests on list endpoints
- manage.py check --deploy in CI
- Rendering, validation, async boundary guardrails
- Local git push hook mirroring CI

### Phase 15 โ€” Jobs and audit foundations
- Hardened constraints on Job/JobStep/FailedTask
- Generalized idempotency beyond billing

### Phase 14 โ€” Billing and credits polish
- Constraint hardening on credit ledger
- Webhook ordering guarantees
- Typed entitlements

### Phase 0-13
- Core infrastructure: multi-tenancy, auth, billing, storage, jobs, audit

๐Ÿ“„ CONTRIBUTING.md

/root/kongai-staging/CONTRIBUTING.md
# Contributing

Keel is a solo-maintained SaaS template. External contributions welcome,
but invariants are not up for discussion โ€” enforced by CI.

## What CI enforces

- lint: ruff, mypy, check_permission_lint.py, eslint, prettier
- typecheck: tsc across apps/web and packages/api-client
- test-api: makemigrations --check, pytest (including MFA run), coverage floors
- test-web: vitest for apps/web and packages/api-client
- api-client-generation: regenerates TS client, fails if stale
- e2e-accessibility: Playwright + axe-core
- contracts: lint-imports and re-derives openapi.merged.json

## Seven invariants

1. Domain logic in services.py/selectors.py, never views/serializers/tasks/admin
2. Authorization in organizations/permissions.py only
3. One transaction.atomic() per service function, opened in service
4. Schema changes are migrations, reviewed, in VCS
5. Async work through Tier 1 shim or Celery directly
6. Validation: serializers at HTTP edge, services for invariants
7. Every mutating service @audited or @not_audited(reason=...)

## Commit and PR expectations

- One coherent unit of work per commit
- Fill in PR template's checklist honestly
- Run pytest and lint-imports locally before pushing

๐Ÿ“„ docs/marketing-removal.md

/root/kongai-staging/docs/marketing-removal.md
# Removing the marketing site

PRD ยง8: init asks whether project wants marketing site.
On "no", delete everything below.

## Routes

- apps/web/app/(marketing)/ โ€” entire route group
- apps/web/app/sitemap.ts
- apps/web/app/robots.ts

## Content and config

- apps/web/content/ โ€” MDX blog posts
- apps/web/content-collections.ts
- apps/web/lib/blog/
- apps/web/lib/site.ts and test
- apps/web/components/json-ld.tsx

## Dependencies (remove from package.json)

- @content-collections/cli
- @content-collections/core
- @content-collections/mdx
- @content-collections/next

## What must NOT move

apps/web/lib/auth/route-guard.ts and apps/web/middleware.ts never
special-case marketing routes โ€” already outside middleware's matcher.

๐Ÿ“ ADRs, Findings & Templates

๐Ÿ“„ ADR 0001: Django Ninja over DRF

/root/kongai-staging/docs/adr/0001-django-ninja-over-drf.md
# ADR 0001: Django Ninja over Django REST Framework

## Status

Accepted

## Context

Need an API framework that:
- Has first-class type support (Pydantic)
- Generates OpenAPI schema automatically
- Is fast (async-capable)
- Has simple, readable code

## Decision

Use Django Ninja instead of DRF because:
- Pydantic schemas for request/response validation
- Automatic OpenAPI 3.0 generation
- Async support out of the box
- Simpler, more readable code
- Better type hints and IDE support
- Faster than DRF (benchmark: 5-10x)

## Consequences

- Less mature ecosystem than DRF
- Fewer third-party packages
- Need to use Ninja-specific patterns

๐Ÿ“„ ADR 0002: Auth BFF Shape

/root/kongai-staging/docs/adr/0002-auth-bff-shape.md
# ADR 0002: Authentication BFF Pattern

## Status

Accepted

## Context

Allauth headless browser client needs:
- Same-origin requests for cookies
- CSRF token handling
- Cross-host session sharing

## Decision

Use Next.js BFF (Backend for Frontend) pattern:
- All /_allauth/browser/v1/* calls go through Next.js API routes
- Next.js proxies to Django with KEEL_API_INTERNAL_URL
- Browser sees same-origin, cookies work correctly
- CSRF handled transparently

## Consequences

- Extra network hop (browser โ†’ Next.js โ†’ Django)
- Simpler client code
- Better security (internal URL not exposed)

๐Ÿ“„ ADR 0003: API Lifecycle Guarantee

/root/kongai-staging/docs/adr/0003-api-lifecycle-guarantee.md
# ADR 0003: API Lifecycle Guarantee

## Status

Accepted

## Context

Need to ensure API changes don't break clients:
- Generated TS client must stay in sync
- Breaking changes should be detected
- Additive-only changes should be safe

## Decision

- Scripts merge OpenAPI from Django Ninja + allauth
- Generated client checked into repo
- CI fails if generated client is stale
- check_openapi_compat.py enforces additive-only rule

## Consequences

- Can't remove fields without migration period
- Must version breaking changes
- CI catches drift automatically

๐Ÿ“„ ADR 0004: Generators as Agent Capability Surface

/root/kongai-staging/docs/adr/0004-generators-as-the-agent-capability-surface.md
# ADR 0004: Generators as Agent Capability Surface

## Status

Accepted

## Context

Agents need to scaffold new code quickly:
- Resources (CRUD slices)
- Jobs (async workers)
- Permissions (authorization)
- Emails (transactional)

## Decision

Build CLI generators (pnpm gen ...):
- templates/ as source of truth
- Reference slice (widgets/) is generated render
- CI validates generators produce valid code

## Consequences

- Consistent code patterns
- Faster development
- CI catches template drift
- Agents can work faster with guardrails

๐Ÿ“„ A11y Finding 0001

findings/a11y/0001-account-tabs-invalid-aria.md
Account tabs have invalid ARIA attributes - tabs need proper role="tablist", role="tab", and aria-selected attributes.

๐Ÿ“„ Code Finding 0002

findings/code/0002-e2e-specs-outside-ci-are-broken.md
E2E specs outside CI are broken - need proper service setup for local Playwright runs.

๐Ÿ“„ Findings README

findings/README.md
Findings directory structure for audit findings (accessibility, code, UX, design).

๐Ÿ“„ Bug Report Template

.github/ISSUE_TEMPLATE/bug_report.md
Bug report template: Describe bug, expected behavior, screenshots, desktop/mobile info.

๐Ÿ“„ Feature Request Template

.github/ISSUE_TEMPLATE/feature_request.md
Feature request template: Is this related to a problem? Describe solution, alternatives, additional context.

๐Ÿ“„ PR Template

.github/pull_request_template.md
PR checklist: Tests, lint, typecheck, migrations, OpenAPI regenerated, docs updated.

๐Ÿ“„ k6 Load Testing

infra/k6/README.md
k6 load testing scripts for API performance testing. Targets demo slice widget list endpoint.

๐Ÿ“„ API Client README

packages/api-client/README.md
Generated TypeScript API client from OpenAPI spec. Use: import { api } from '@keel/api-client'
โ†‘ Top