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 migratezero 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'