{"id":34683194,"url":"https://github.com/mjtechguy/django-boilerplate","last_synced_at":"2026-05-25T17:43:23.434Z","repository":{"id":328441736,"uuid":"1111474686","full_name":"mjtechguy/django-boilerplate","owner":"mjtechguy","description":null,"archived":false,"fork":false,"pushed_at":"2026-01-05T05:25:28.000Z","size":8760,"stargazers_count":0,"open_issues_count":10,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-25T17:42:52.899Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mjtechguy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-07T02:18:56.000Z","updated_at":"2026-01-05T05:25:47.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/mjtechguy/django-boilerplate","commit_stats":null,"previous_names":["mjtechguy/django-boilerplate"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/mjtechguy/django-boilerplate","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mjtechguy%2Fdjango-boilerplate","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mjtechguy%2Fdjango-boilerplate/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mjtechguy%2Fdjango-boilerplate/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mjtechguy%2Fdjango-boilerplate/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mjtechguy","download_url":"https://codeload.github.com/mjtechguy/django-boilerplate/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mjtechguy%2Fdjango-boilerplate/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33486787,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-25T14:31:05.219Z","status":"ssl_error","status_checked_at":"2026-05-25T14:31:02.878Z","response_time":57,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-12-24T21:35:24.216Z","updated_at":"2026-05-25T17:43:23.422Z","avatar_url":"https://github.com/mjtechguy.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Django + DRF + Keycloak + Cerbos Boilerplate (High-Scale RBAC/ABAC)\n\nProduction-ready, multi-tenant SaaS boilerplate with enterprise security features. Includes RBAC/ABAC authorization (Cerbos), hybrid authentication (Keycloak + Local), Stripe billing (B2B \u0026 B2C), React admin console, and comprehensive audit logging.\n\n## Table of Contents\n\n- [Features](#features)\n- [Service Topology](#service-topology)\n- [Quickstart](#quickstart)\n- [Environment Variables](#environment-variables)\n- [Secrets Handling](#secrets-handling)\n- [API Reference](#api-reference)\n- [Authentication](#authentication)\n- [Authentication Audiences](#authentication-audiences)\n- [Stripe Billing](#stripe-billing)\n- [Frontend Admin Console](#frontend-admin-console)\n- [Audit Trail](#audit-trail)\n- [Account Lockout Notifications](#account-lockout-notifications)\n- [Custom Webhooks](#custom-webhooks)\n- [Storage Configuration](#storage-configuration)\n- [Runbooks](#runbooks)\n- [Architecture Overview](#architecture-overview)\n- [Testing](#testing)\n\n## Features\n\n### Security \u0026 Authentication\n- **Hybrid Auth**: Keycloak OIDC + Local JWT (RS256) authentication\n- **RBAC/ABAC**: Cerbos policy decision point with Redis-cached decisions\n- **Argon2 Passwords**: Industry-standard password hashing\n- **MFA Support**: Multi-factor authentication via Keycloak\n- **Account Lockout**: Brute-force protection with django-axes\n- **Lockout Notifications**: Email alerts to users and admins on account lockouts\n- **Mass Attack Detection**: Automated admin alerts for credential stuffing attempts\n- **Rate Limiting**: Global and per-tenant throttling with tier-based quotas\n\n### Multi-Tenancy\n- **Organizations**: Multi-org data isolation\n- **Teams**: Org-scoped team management\n- **Memberships**: Flexible role assignments (org_roles, team_roles)\n- **License Tiers**: Per-org feature gating (free, starter, pro, enterprise)\n\n### Billing (Stripe)\n- **B2B Org Billing**: Organization-level subscriptions\n- **B2C User Billing**: Individual user subscriptions\n- **Webhook Handling**: Automatic tier updates on subscription events\n- **Billing Portal**: Self-service subscription management\n\n### Admin Console (React)\n- **Organizations Management**: CRUD with license management\n- **Teams Management**: Create/edit teams with member management\n- **Users Management**: Invite, create, deactivate users\n- **Audit Log Viewer**: Search, filter, export audit logs\n- **System Monitoring**: Celery health, queue stats, metrics\n\n### Observability\n- **Structured Logging**: JSON logs with structlog\n- **Audit Trail**: Tamper-evident logs with hash chain verification\n- **Prometheus Metrics**: `/api/v1/monitoring/metrics`\n- **Sentry Integration**: Error tracking and tracing\n- **Health Probes**: Kubernetes-ready liveness/readiness\n\n## Service Topology\n\nDevelopment stack via Docker Compose:\n\n| Service | Description | Port |\n|---------|-------------|------|\n| web | Django/DRF API (stateless) | 8000 |\n| frontend | React Admin Console (Vite) | 5173 |\n| celery | Celery workers (async tasks) | - |\n| celery-beat | Celery scheduler | - |\n| cerbos | Policy Decision Point (RBAC/ABAC) | 3592, 3593 |\n| keycloak | Identity Provider (OIDC) | 8080 |\n| postgres | Primary database | 5432 |\n| rabbitmq | Celery broker (durable queues + DLQ) | 5672, 15672 |\n| redis | Cache, rate limits, locks | 6379 |\n| mailpit | Email testing (dev only) | 8025, 1025 |\n| stripe-mock | Stripe API mock (dev only) | 12111 |\n\n```mermaid\nflowchart LR\n  FE[React Admin] --\u003e|OIDC/Local| KC[Keycloak]\n  FE --\u003e|Bearer JWT| WEB[Django API]\n  WEB --\u003e|gRPC allow/deny| CER[Cerbos]\n  WEB --\u003e|SQL| PG[(Postgres)]\n  WEB --\u003e|cache/rate| RDS[(Redis)]\n  WEB --\u003e|Stripe API| STRIPE[Stripe]\n  CEL[Celery Worker] --\u003e|tasks| RMQ[(RabbitMQ)]\n  BEAT[Celery Beat] --\u003e RMQ\n```\n\n## Quickstart\n\n### Prerequisites\n\n- Python 3.13+\n- Node.js 20+ (for frontend)\n- Docker and Docker Compose\n- `uv` (recommended) or `pip`\n\n### 1. Clone and Setup\n\n```bash\ngit clone \u003crepo-url\u003e\ncd django-boilerplate\n\n# Backend: Create virtual environment\npython -m venv .venv\nsource .venv/bin/activate\npip install -r requirements.txt -r requirements-dev.txt\n\n# Frontend: Install dependencies\ncd frontend \u0026\u0026 pnpm install \u0026\u0026 cd ..\n```\n\n### 2. Configure Environment\n\n```bash\ncp .env.example .env\n# Edit .env with your values (defaults work for local dev)\n```\n\n### 3. Start Services\n\n```bash\n# Start all services\ndocker compose -f compose/docker-compose.yml up -d\n\n# Wait for services to be healthy (~30s for Keycloak)\ndocker compose -f compose/docker-compose.yml ps\n\n# Run database migrations\ndocker compose -f compose/docker-compose.yml exec -w /app/backend web python manage.py migrate\n```\n\n### 4. Seed Data (Development)\n\nAdd test data to your database:\n\n```bash\n# Seed Django database with test org, team, users\ndocker compose -f compose/docker-compose.yml exec -w /app/backend web python manage.py shell \u003c test-seed/seed.py\n\n# Seed Keycloak with test users (run from host with venv activated)\npython test-seed/keycloak_seed.py\n```\n\n### 5. Verify Setup\n\nConfirm all services are working:\n\n```bash\n# Check all services are healthy\ndocker compose -f compose/docker-compose.yml ps\n\n# Get a test token (outputs tokens for all test users)\npython test-seed/keycloak_tokens.py\n\n# Test an API call\nTOKEN=\"\u003cpaste access token from above\u003e\"\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:8000/api/v1/auth/me\n```\n\n### 7. Access Services\n\n| Service | URL | Credentials |\n|---------|-----|-------------|\n| API | http://localhost:8000 | - |\n| Admin Console | http://localhost:5173 | See below |\n| Keycloak Admin | http://localhost:8080 | admin / admin |\n| RabbitMQ | http://localhost:15672 | guest / guest |\n| Mailpit | http://localhost:8025 | - |\n\n### 8. Create Admin User (Local Auth)\n\n```bash\n# Register via API\ncurl -X POST http://localhost:8000/api/v1/auth/register \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"email\": \"admin@example.com\", \"password\": \"SecurePass123!\", \"first_name\": \"Admin\", \"last_name\": \"User\"}'\n\n# Or use Django management command\ndocker compose -f compose/docker-compose.yml exec -w /app/backend web \\\n  python manage.py createsuperuser\n```\n\n## Environment Variables\n\n### Core Django\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `DJANGO_SECRET_KEY` | Secret key (change in production!) | `changeme` |\n| `DJANGO_DEBUG` | Enable debug mode | `true` |\n| `DJANGO_ALLOWED_HOSTS` | Comma-separated allowed hosts | `localhost,127.0.0.1` |\n| `DJANGO_SETTINGS_MODULE` | Settings module | `config.settings.local` |\n| `FRONTEND_URL` | Frontend URL for redirects | `http://localhost:5173` |\n\n### Database (PostgreSQL)\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `POSTGRES_DB` | Database name | `app` |\n| `POSTGRES_USER` | Database user | `app` |\n| `POSTGRES_PASSWORD` | Database password | `changeme` |\n| `POSTGRES_HOST` | Database host | `postgres` |\n| `POSTGRES_PORT` | Database port | `5432` |\n\n### Redis \u0026 RabbitMQ\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `REDIS_HOST` | Redis host | `redis` |\n| `REDIS_PORT` | Redis port | `6379` |\n| `RABBITMQ_HOST` | RabbitMQ host | `rabbitmq` |\n| `RABBITMQ_USER` | RabbitMQ user | `guest` |\n| `RABBITMQ_PASSWORD` | RabbitMQ password | `guest` |\n\n### Authentication (Keycloak)\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `KEYCLOAK_SERVER_URL` | Keycloak server URL | `http://keycloak:8080` |\n| `KEYCLOAK_REALM` | Keycloak realm | `app` |\n| `KEYCLOAK_CLIENT_ID` | Keycloak client ID | `api` |\n| `KEYCLOAK_AUDIENCE` | Expected JWT audience | `api` |\n\n### Local Authentication\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `LOCAL_AUTH_ENABLED` | Enable local auth | `true` |\n| `LOCAL_AUTH_ACCESS_TOKEN_TTL` | Access token TTL (seconds) | `900` |\n| `LOCAL_AUTH_REFRESH_TOKEN_TTL` | Refresh token TTL (seconds) | `604800` |\n| `LOCAL_AUTH_MAX_FAILED_ATTEMPTS` | Max failed logins before lockout | `5` |\n| `LOCAL_AUTH_LOCKOUT_DURATION` | Lockout duration (seconds) | `1800` |\n| `EMAIL_VERIFICATION_REQUIRED` | Require email verification | `true` |\n\n### Authorization (Cerbos)\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `CERBOS_URL` | Cerbos server URL | `http://cerbos:3592` |\n| `CERBOS_DECISION_CACHE_TTL` | Decision cache TTL (seconds) | `30` |\n\n### Stripe Billing\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `STRIPE_ENABLED` | Enable Stripe integration | `false` |\n| `STRIPE_SECRET_KEY` | Stripe secret key | `` |\n| `STRIPE_PUBLISHABLE_KEY` | Stripe publishable key | `` |\n| `STRIPE_WEBHOOK_SECRET` | Stripe webhook signing secret | `` |\n| `STRIPE_PRICE_STARTER` | Price ID for starter tier | `price_starter` |\n| `STRIPE_PRICE_PRO` | Price ID for pro tier | `price_pro` |\n| `STRIPE_PRICE_ENTERPRISE` | Price ID for enterprise tier | `price_enterprise` |\n\n### Email\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `EMAIL_BACKEND` | Django email backend | `console` |\n| `EMAIL_HOST` | SMTP host | `mailpit` |\n| `EMAIL_PORT` | SMTP port | `1025` |\n| `EMAIL_USE_TLS` | Use TLS | `false` |\n| `DEFAULT_FROM_EMAIL` | Default sender email | `noreply@example.com` |\n\n### Security\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `ADMIN_HOSTNAME` | Hostname for Django admin (production) | `` |\n| `AXES_FAILURE_LIMIT` | Login failures before lockout | `5` |\n| `AXES_COOLOFF_TIME` | Lockout duration (hours) | `1` |\n| `LOCKOUT_NOTIFICATION_ENABLED` | Send email on account lockout | `true` |\n| `LOCKOUT_ADMIN_EMAILS` | Admin emails for mass lockout alerts (comma-separated) | `` |\n| `LOCKOUT_MASS_THRESHOLD` | Lockout count to trigger admin alert | `10` |\n| `LOCKOUT_MASS_WINDOW_MINUTES` | Time window for mass lockout detection (minutes) | `5` |\n| `THROTTLE_RATE_ANON` | Anonymous rate limit | `100/hour` |\n| `THROTTLE_RATE_USER` | Authenticated rate limit | `1000/hour` |\n| `CORS_ALLOWED_ORIGINS` | Allowed CORS origins | `http://localhost:5173` |\n\n### Observability\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `LOG_LEVEL` | Logging level | `INFO` |\n| `ENVIRONMENT` | Environment name | `development` |\n| `AUDIT_PII_POLICY` | PII handling: mask, hash, drop | `mask` |\n| `SENTRY_DSN` | Sentry DSN (empty to disable) | `` |\n\n## Secrets Handling\n\n### Development\nIn development, use the `.env` file with demo credentials. The included values are safe for local testing.\n\n### Production\nNever commit real secrets. Use a secret manager:\n\n| Secret Manager | Setup |\n|----------------|-------|\n| AWS Secrets Manager | Store as JSON, load via boto3 |\n| HashiCorp Vault | Use hvac library |\n| GCP Secret Manager | Use google-cloud-secret-manager |\n| Azure Key Vault | Use azure-keyvault-secrets |\n\n### Secrets Inventory\n\nThese variables contain sensitive data and MUST be secured in production:\n\n| Variable | Type | Generation |\n|----------|------|------------|\n| `DJANGO_SECRET_KEY` | String | `python -c \"from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())\"` |\n| `POSTGRES_PASSWORD` | String | Strong random password |\n| `RABBITMQ_PASSWORD` | String | Strong random password |\n| `LOCAL_AUTH_PRIVATE_KEY` | RSA PEM | `python -c \"from api.local_jwt import generate_key_pair; priv, pub = generate_key_pair(); print(priv)\"` |\n| `LOCAL_AUTH_PUBLIC_KEY` | RSA PEM | Generated with private key |\n| `STRIPE_SECRET_KEY` | String | From Stripe dashboard |\n| `STRIPE_WEBHOOK_SECRET` | String | From Stripe webhook settings |\n| `AUDIT_SIGNING_KEY` | Hex | `python -c \"import secrets; print(secrets.token_hex(32))\"` |\n| `FIELD_ENCRYPTION_KEYS` | Fernet | `python -c \"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())\"` |\n\n### Key Rotation\nSee [Runbooks](#runbooks) for key rotation procedures.\n\n## API Reference\n\n### Health \u0026 Monitoring\n\n| Endpoint | Method | Auth | Description |\n|----------|--------|------|-------------|\n| `/healthz` | GET | No | Basic health check |\n| `/api/v1/health/live` | GET | No | Kubernetes liveness probe |\n| `/api/v1/health/ready` | GET | No | Kubernetes readiness probe |\n| `/api/v1/monitoring/overview` | GET | Admin | System overview |\n| `/api/v1/monitoring/server` | GET | Admin | Server metrics |\n| `/api/v1/monitoring/metrics` | GET | No | Prometheus metrics |\n| `/api/v1/monitoring/metrics/json` | GET | No | JSON metrics |\n| `/api/v1/monitoring/celery/health` | GET | No | Celery worker health |\n| `/api/v1/monitoring/celery/stats` | GET | No | Celery statistics |\n| `/api/v1/monitoring/queues` | GET | No | RabbitMQ queue stats |\n| `/api/v1/monitoring/tasks` | GET | No | Registered Celery tasks |\n\n### Local Authentication\n\n| Endpoint | Method | Auth | Description |\n|----------|--------|------|-------------|\n| `/api/v1/auth/register` | POST | No | Register new user |\n| `/api/v1/auth/login` | POST | No | Login (returns JWT tokens) |\n| `/api/v1/auth/logout` | POST | JWT | Logout (revoke refresh token) |\n| `/api/v1/auth/refresh` | POST | No | Refresh access token |\n| `/api/v1/auth/me` | GET | JWT | Get current user profile |\n| `/api/v1/auth/verify-email` | POST | No | Verify email with token |\n| `/api/v1/auth/resend-verification` | POST | No | Resend verification email |\n| `/api/v1/auth/forgot-password` | POST | No | Request password reset |\n| `/api/v1/auth/reset-password` | POST | No | Reset password with token |\n| `/api/v1/auth/change-password` | POST | JWT | Change password (authenticated) |\n\n### Keycloak Authentication\n\n| Endpoint | Method | Auth | Description |\n|----------|--------|------|-------------|\n| `/api/v1/ping` | GET | JWT | Auth verification endpoint |\n| `/api/v1/protected` | GET | JWT | Sample protected endpoint |\n\n### B2B Organization Billing\n\n| Endpoint | Method | Auth | Description |\n|----------|--------|------|-------------|\n| `/api/v1/orgs/{id}/billing` | GET | Org Admin | Get org billing status |\n| `/api/v1/orgs/{id}/billing/checkout` | POST | Org Admin | Create Stripe checkout session |\n| `/api/v1/orgs/{id}/billing/portal` | POST | Org Admin | Create billing portal session |\n| `/api/v1/orgs/{id}/billing/customer` | POST | Org Admin | Create Stripe customer |\n| `/api/v1/billing/plans` | GET | JWT | List available subscription plans |\n\n### B2C User Billing\n\n| Endpoint | Method | Auth | Description |\n|----------|--------|------|-------------|\n| `/api/v1/me/billing` | GET | JWT | Get user billing status |\n| `/api/v1/me/billing/checkout` | POST | JWT | Create checkout session |\n| `/api/v1/me/billing/portal` | POST | JWT | Create billing portal session |\n| `/api/v1/me/billing/customer` | POST | JWT | Create Stripe customer |\n\n### Organization Licensing\n\n| Endpoint | Method | Auth | Description |\n|----------|--------|------|-------------|\n| `/api/v1/orgs/{id}/license` | GET | Org Admin | Get org license info |\n| `/api/v1/orgs/{id}/license` | PUT | Org Admin | Update org license |\n| `/api/v1/stripe/webhook` | POST | Stripe Sig | Stripe webhook handler |\n\n### Platform Admin - Organizations\n\n| Endpoint | Method | Auth | Description |\n|----------|--------|------|-------------|\n| `/api/v1/admin/orgs` | GET | Platform Admin | List all organizations |\n| `/api/v1/admin/orgs` | POST | Platform Admin | Create organization |\n| `/api/v1/admin/orgs/{id}` | GET | Platform Admin | Get organization details |\n| `/api/v1/admin/orgs/{id}` | PUT | Platform Admin | Update organization |\n| `/api/v1/admin/orgs/{id}` | DELETE | Platform Admin | Soft-delete organization |\n\n### Platform Admin - Teams\n\n| Endpoint | Method | Auth | Description |\n|----------|--------|------|-------------|\n| `/api/v1/admin/teams` | GET | Platform Admin | List all teams |\n| `/api/v1/admin/teams` | POST | Platform Admin | Create team |\n| `/api/v1/admin/teams/{id}` | GET | Platform Admin | Get team details |\n| `/api/v1/admin/teams/{id}` | PUT | Platform Admin | Update team |\n| `/api/v1/admin/teams/{id}` | DELETE | Platform Admin | Delete team |\n| `/api/v1/admin/teams/{id}/members` | GET | Platform Admin | List team members |\n| `/api/v1/admin/teams/{id}/members` | POST | Platform Admin | Add team member |\n\n### Platform Admin - Users\n\n| Endpoint | Method | Auth | Description |\n|----------|--------|------|-------------|\n| `/api/v1/admin/users` | GET | Platform Admin | List all users |\n| `/api/v1/admin/users` | POST | Platform Admin | Create user |\n| `/api/v1/admin/users/invite` | POST | Platform Admin | Invite user via email |\n| `/api/v1/admin/users/{id}` | GET | Platform Admin | Get user details |\n| `/api/v1/admin/users/{id}` | PUT | Platform Admin | Update user |\n| `/api/v1/admin/users/{id}` | DELETE | Platform Admin | Deactivate user |\n| `/api/v1/admin/users/{id}/memberships` | GET | Platform Admin | List user memberships |\n| `/api/v1/admin/users/{id}/memberships` | POST | Platform Admin | Add membership |\n| `/api/v1/admin/users/{id}/resend-invite` | POST | Platform Admin | Resend invite email |\n\n### Platform Admin - Memberships\n\n| Endpoint | Method | Auth | Description |\n|----------|--------|------|-------------|\n| `/api/v1/admin/memberships` | GET | Platform Admin | List all memberships |\n| `/api/v1/admin/memberships` | POST | Platform Admin | Create membership |\n| `/api/v1/admin/memberships/{id}` | GET | Platform Admin | Get membership details |\n| `/api/v1/admin/memberships/{id}` | PUT | Platform Admin | Update membership roles |\n| `/api/v1/admin/memberships/{id}` | DELETE | Platform Admin | Delete membership |\n\n### Site Settings\n\n| Endpoint | Method | Auth | Description |\n|----------|--------|------|-------------|\n| `/api/v1/settings/site` | GET | No | Get public site settings (branding) |\n| `/api/v1/admin/settings/site` | GET | Platform Admin | Get admin site settings |\n| `/api/v1/admin/settings/site` | PUT | Platform Admin | Update site settings |\n\n### Audit Logs\n\n| Endpoint | Method | Auth | Description |\n|----------|--------|------|-------------|\n| `/api/v1/audit` | GET | JWT | List audit logs (filtered by access) |\n| `/api/v1/audit/export` | GET | Platform Admin | Export audit logs (CSV/JSON) |\n| `/api/v1/audit/verify` | POST | Platform Admin | Verify single audit entry |\n| `/api/v1/audit/chain-verify` | POST | Platform Admin | Verify hash chain integrity |\n\n### Custom Webhooks\n\n| Endpoint | Method | Auth | Description |\n|----------|--------|------|-------------|\n| `/api/v1/webhooks` | GET | JWT | List webhook endpoints |\n| `/api/v1/webhooks` | POST | JWT | Create webhook endpoint |\n| `/api/v1/webhooks/{id}` | GET | JWT | Get webhook details |\n| `/api/v1/webhooks/{id}` | PUT | JWT | Update webhook |\n| `/api/v1/webhooks/{id}` | DELETE | JWT | Delete webhook |\n| `/api/v1/webhooks/{id}/deliveries` | GET | JWT | List webhook deliveries |\n| `/api/v1/webhooks/{id}/test` | POST | JWT | Send test webhook |\n\n### Impersonation\n\n| Endpoint | Method | Auth | Description |\n|----------|--------|------|-------------|\n| `/api/v1/admin/impersonation/logs` | GET | Platform Admin | List impersonation logs |\n\n### Alerts\n\n| Endpoint | Method | Auth | Description |\n|----------|--------|------|-------------|\n| `/api/v1/admin/alerts` | GET | Platform Admin | Get system alerts |\n\n## Authentication\n\n### Hybrid Authentication\n\nThe boilerplate supports two authentication methods that can work together:\n\n1. **Keycloak OIDC**: Enterprise SSO with MFA support\n2. **Local JWT**: Built-in username/password authentication\n\n### Local Authentication Flow\n\n```mermaid\nsequenceDiagram\n  participant U as User\n  participant API as Django API\n  participant DB as Database\n\n  U-\u003e\u003eAPI: POST /auth/register\n  API-\u003e\u003eDB: Create User + LocalUserProfile\n  API-\u003e\u003eU: Send verification email\n  U-\u003e\u003eAPI: POST /auth/verify-email\n  API-\u003e\u003eDB: Mark email verified\n  U-\u003e\u003eAPI: POST /auth/login\n  API-\u003e\u003eDB: Verify credentials\n  API-\u003e\u003eU: {access_token, refresh_token}\n  U-\u003e\u003eAPI: GET /api/... (Bearer token)\n  API-\u003e\u003eU: Response\n```\n\n### JWT Token Structure\n\n```json\n{\n  \"sub\": \"user_id\",\n  \"email\": \"user@example.com\",\n  \"name\": \"User Name\",\n  \"realm_access\": {\n    \"roles\": [\"user\", \"platform_admin\"]\n  },\n  \"org_id\": \"uuid\",\n  \"team_ids\": [\"uuid1\", \"uuid2\"],\n  \"iat\": 1234567890,\n  \"exp\": 1234568790\n}\n```\n\n### Role Hierarchy\n\n| Role | Scope | Permissions |\n|------|-------|-------------|\n| `platform_admin` | Global | Full system access |\n| `org_admin` | Organization | Manage org teams/users |\n| `team_admin` | Team | Manage team members |\n| `user` | Self | Basic access |\n\n## Authentication Audiences\n\nThe system uses three Keycloak clients for different access levels:\n\n### Keycloak Clients\n\n| Client | Audience | Purpose |\n|--------|----------|---------|\n| `api` | `api` | General API access for all authenticated users |\n| `global-admin` | `global-admin` | Platform administration (super-admin features) |\n| `org-admin` | `org-admin` | Organization-level administration |\n\n### JWT Audience Validation\n\nThe API validates the `aud` (audience) claim in JWTs:\n\n- Requests to `/api/v1/admin/*` endpoints require `global-admin` or `org-admin` audience\n- Requests to `/api/v1/orgs/{id}/*` endpoints validate org membership\n- All other authenticated endpoints accept the `api` audience\n\n### ADMIN_HOSTNAME\n\nThe `ADMIN_HOSTNAME` environment variable enables hostname-based access control:\n\n```bash\n# When set, Django admin and platform admin APIs only accessible from this hostname\nADMIN_HOSTNAME=admin.yourdomain.com\n```\n\nThis provides defense-in-depth by ensuring admin interfaces aren't exposed on public domains.\n\n### Role Assignment\n\n| Role | Source | Scope |\n|------|--------|-------|\n| `platform_admin` | Keycloak realm role | Global platform access |\n| `support_readonly` | Keycloak realm role | Read-only platform access |\n| `org_admin` | Keycloak client role (api) | Organization management |\n| `team_admin` | Keycloak client role (api) | Team management |\n| `org_member`, `team_member` | Database Membership | Data access |\n\n## Stripe Billing\n\n### B2B (Organization) Billing\n\nOrganization-level subscriptions for SaaS teams:\n\n```bash\n# Get billing status\ncurl -H \"Authorization: Bearer $TOKEN\" \\\n  http://localhost:8000/api/v1/orgs/{org_id}/billing\n\n# Create checkout session\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"price_id\": \"price_pro\"}' \\\n  http://localhost:8000/api/v1/orgs/{org_id}/billing/checkout\n```\n\n### B2C (User) Billing\n\nIndividual user subscriptions:\n\n```bash\n# Get user billing status\ncurl -H \"Authorization: Bearer $TOKEN\" \\\n  http://localhost:8000/api/v1/me/billing\n\n# Create checkout session\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"price_id\": \"price_pro\"}' \\\n  http://localhost:8000/api/v1/me/billing/checkout\n```\n\n### License Tiers \u0026 Features\n\n| Tier | Default Features |\n|------|------------------|\n| `free` | 5 users, 1 team, 100 API req/hr |\n| `starter` | 25 users, 5 teams, 1000 API req/hr |\n| `pro` | 100 users, unlimited teams, 10000 API req/hr, webhooks |\n| `enterprise` | Unlimited, custom features, audit export |\n\n### Webhook Events Handled\n\n- `checkout.session.completed` - Subscription activated\n- `customer.subscription.created` - New subscription\n- `customer.subscription.updated` - Plan changed\n- `customer.subscription.deleted` - Subscription cancelled\n- `invoice.payment_failed` - Payment failed\n\n## Frontend Admin Console\n\n### Overview\n\nReact-based admin console built with modern tooling:\n\n- **React 18** with TypeScript\n- **TanStack Router** - Type-safe routing\n- **TanStack Query** - Server state management\n- **TanStack Table** - Data tables with sorting/filtering\n- **shadcn/ui** - Accessible UI components\n- **Tailwind CSS** - Utility-first styling\n\n### Pages\n\n| Route | Description |\n|-------|-------------|\n| `/login` | Login page (local auth) |\n| `/admin/organizations` | Manage organizations |\n| `/admin/teams` | Manage teams |\n| `/admin/users` | Manage users |\n| `/admin/audit` | View audit logs |\n| `/admin/monitoring` | System health dashboard |\n\n### Running the Frontend\n\n```bash\ncd frontend\npnpm install\npnpm dev\n# Open http://localhost:5173\n```\n\n### Building for Production\n\n```bash\ncd frontend\npnpm build\n# Output in dist/\n```\n\n## Audit Trail\n\n### Features\n\n- **Tamper-Evident**: HMAC-SHA256 signatures on each entry\n- **Hash Chain**: Each entry includes hash of previous entry\n- **PII Handling**: Configurable mask/hash/drop for sensitive data\n- **Export**: CSV and JSON export formats\n\n### Audit Entry Structure\n\n```json\n{\n  \"id\": \"uuid\",\n  \"timestamp\": \"2024-01-01T00:00:00Z\",\n  \"action\": \"create\",\n  \"resource_type\": \"User\",\n  \"resource_id\": \"uuid\",\n  \"actor_id\": \"user_uuid\",\n  \"org_id\": \"org_uuid\",\n  \"changes\": {\"field\": {\"old\": \"x\", \"new\": \"y\"}},\n  \"signature\": \"hmac_signature\",\n  \"previous_hash\": \"hash_of_previous_entry\"\n}\n```\n\n### Verification\n\n```bash\n# Verify single entry\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"audit_id\": \"uuid\"}' \\\n  http://localhost:8000/api/v1/audit/verify\n\n# Verify chain integrity\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"org_id\": \"uuid\", \"start_date\": \"2024-01-01\"}' \\\n  http://localhost:8000/api/v1/audit/chain-verify\n```\n\n## Account Lockout Notifications\n\n### Overview\n\nThe system automatically sends email notifications when accounts are locked due to failed login attempts. This feature provides security transparency and enables rapid response to potential account compromise or credential stuffing attacks.\n\n### User Notifications\n\nWhen an account is locked (after exceeding the configured failure limit), the affected user receives an email containing:\n\n- **Lockout details**: Duration, failed attempt count, IP address, and timestamp\n- **Security guidance**: Different advice for legitimate users vs. suspicious activity\n- **Password reset link**: Quick access to reset password if account is compromised\n- **Best practices**: Recommendations for account security\n\nThe notification is sent asynchronously via Celery to avoid blocking the login flow.\n\n### Mass Lockout Detection\n\nThe system tracks lockout events in a time-windowed counter using Redis sorted sets. When the number of lockouts exceeds a configured threshold within a time window, administrators receive an alert email.\n\n**Default thresholds:**\n- **Threshold**: 10 lockouts\n- **Time window**: 5 minutes\n\n**Admin alert includes:**\n- Count of lockouts in the time window\n- List of affected accounts (username, email, lockout time)\n- IP address summary showing attack sources\n- Recommended incident response actions\n- Educational content about credential stuffing attacks\n\n### Configuration\n\n```bash\n# Enable/disable lockout notifications\nLOCKOUT_NOTIFICATION_ENABLED=true\n\n# Admin email recipients for mass lockout alerts (comma-separated)\nLOCKOUT_ADMIN_EMAILS=security@example.com,admin@example.com\n\n# Number of lockouts to trigger admin alert\nLOCKOUT_MASS_THRESHOLD=10\n\n# Time window for mass lockout detection (minutes)\nLOCKOUT_MASS_WINDOW_MINUTES=5\n```\n\n### Behavior\n\n**User lockout triggers:**\n- **django-axes lockouts**: Triggered by the `user_locked_out` signal when AXES_FAILURE_LIMIT is exceeded\n- **Local auth lockouts**: Triggered by `LocalUserProfile.record_login_attempt()` when LOCAL_AUTH_MAX_FAILED_ATTEMPTS is exceeded\n\n**Audit logging:**\n- All lockout events create an audit log entry with `action='account_locked'`\n- Includes metadata: IP address, failure count, lockout duration, unlock time, source (django-axes or local-auth)\n\n**Mass attack detection:**\n- Uses Redis sorted sets for efficient time-windowed counting\n- Automatically cleans up expired lockout events\n- Debounces admin alerts (one alert per time window) to prevent notification spam\n- Tracks lockout source to distinguish between attack patterns\n\n### Disabling Notifications\n\nTo disable lockout notifications entirely:\n\n```bash\nLOCKOUT_NOTIFICATION_ENABLED=false\n```\n\nIndividual users without email addresses will not receive notifications, but the lockout will still occur and be logged in the audit trail.\n\n### Testing Lockout Notifications\n\n```bash\n# Trigger a lockout by failing login attempts\nfor i in {1..6}; do\n  curl -X POST http://localhost:8000/api/v1/auth/login \\\n    -H \"Content-Type: application/json\" \\\n    -d '{\"email\": \"user@example.com\", \"password\": \"wrong_password\"}'\ndone\n\n# Check email in Mailpit (development)\n# Open http://localhost:8025\n\n# Trigger mass lockout detection (requires multiple users)\n# See backend/api/tests/test_lockout_integration.py for examples\n```\n\n## Custom Webhooks\n\n### Overview\n\nSend HTTP callbacks when events occur in the system.\n\n### Supported Events\n\n- `user.created`, `user.updated`, `user.deleted`\n- `org.created`, `org.updated`\n- `team.created`, `team.updated`\n- `membership.created`, `membership.updated`, `membership.deleted`\n\n### Creating a Webhook\n\n```bash\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"url\": \"https://example.com/webhook\",\n    \"events\": [\"user.created\", \"user.updated\"],\n    \"secret\": \"your_signing_secret\"\n  }' \\\n  http://localhost:8000/api/v1/webhooks\n```\n\n### Webhook Payload\n\n```json\n{\n  \"event\": \"user.created\",\n  \"timestamp\": \"2024-01-01T00:00:00Z\",\n  \"data\": {\n    \"id\": \"uuid\",\n    \"email\": \"user@example.com\"\n  }\n}\n```\n\nWebhooks include an `X-Webhook-Signature` header (HMAC-SHA256) for verification.\n\n## Storage Configuration\n\n### Local Storage (Default)\n\nFiles stored in `media/` directory.\n\n### Enabling S3/MinIO in Development\n\nThe Docker Compose stack includes MinIO (S3-compatible storage):\n\n1. Enable S3 in `.env`:\n   ```bash\n   USE_S3=true\n   AWS_ACCESS_KEY_ID=rustfsadmin\n   AWS_SECRET_ACCESS_KEY=rustfsadmin\n   AWS_STORAGE_BUCKET_NAME=app-media\n   AWS_S3_ENDPOINT_URL=http://rustfs:9000\n   ```\n\n2. Create the bucket (first time only):\n   ```bash\n   # Access MinIO console at http://localhost:9001\n   # Login: rustfsadmin / rustfsadmin\n   # Create bucket: app-media\n   ```\n\n3. Restart the web service:\n   ```bash\n   docker compose -f compose/docker-compose.yml restart web\n   ```\n\n### Production S3 Setup\n\n1. Create an S3 bucket with appropriate lifecycle policies\n2. Create an IAM user with these permissions:\n   ```json\n   {\n     \"Version\": \"2012-10-17\",\n     \"Statement\": [{\n       \"Effect\": \"Allow\",\n       \"Action\": [\"s3:PutObject\", \"s3:GetObject\", \"s3:DeleteObject\", \"s3:ListBucket\"],\n       \"Resource\": [\"arn:aws:s3:::your-bucket\", \"arn:aws:s3:::your-bucket/*\"]\n     }]\n   }\n   ```\n3. Set environment variables (omit `AWS_S3_ENDPOINT_URL` for real AWS S3)\n\n### Migrating from Local to S3\n\n```bash\n# Sync existing media files to S3\naws s3 sync media/ s3://your-bucket/media/\n\n# Update .env and restart\nUSE_S3=true\n# ... other S3 vars\ndocker compose restart web\n```\n\n### MinIO (Local S3)\n\nIncluded in Docker Compose:\n- Console: http://localhost:9001 (rustfsadmin/rustfsadmin)\n- API: http://localhost:9000\n\n## Runbooks\n\n### Rotate Django Secret Key\n\n```bash\npython -c \"from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())\"\n# Update DJANGO_SECRET_KEY and restart services\n```\n\n### Clear Redis Caches\n\n```bash\ndocker compose -f compose/docker-compose.yml exec redis redis-cli FLUSHALL\n```\n\n### View Celery Logs\n\n```bash\ndocker compose -f compose/docker-compose.yml logs -f celery\n```\n\n### Debug Failed Tasks (DLQ)\n\n```bash\ncurl http://localhost:8000/api/v1/monitoring/queues | jq '.queues[] | select(.name == \"dlq\")'\n```\n\n### Run Migrations\n\n```bash\ndocker compose -f compose/docker-compose.yml exec -w /app/backend web python manage.py migrate\n```\n\n### Invalidate Cerbos Decision Cache\n\n```bash\n# Cerbos caches decisions in Redis for 30s by default\n# To force re-evaluation, flush the cache:\ndocker compose -f compose/docker-compose.yml exec redis redis-cli KEYS \"cerbos:*\" | xargs docker compose -f compose/docker-compose.yml exec redis redis-cli DEL\n\n# Or restart Cerbos to clear its internal cache:\ndocker compose -f compose/docker-compose.yml restart cerbos\n```\n\n### Restart Individual Services\n\n```bash\n# Restart without losing data\ndocker compose -f compose/docker-compose.yml restart web\ndocker compose -f compose/docker-compose.yml restart celery\n\n# Full recreate (pulls latest config)\ndocker compose -f compose/docker-compose.yml up -d --force-recreate web\n```\n\n### Reset Keycloak Realm\n\n```bash\n# Export current realm (backup)\ndocker compose -f compose/docker-compose.yml exec keycloak \\\n  /opt/keycloak/bin/kc.sh export --dir /tmp/export --realm app\n\n# Re-import realm from file\ndocker compose -f compose/docker-compose.yml restart keycloak\n# Keycloak auto-imports from /opt/keycloak/data/import on startup\n```\n\n### Celery Queue Debugging\n\n```bash\n# View all queues and message counts\ncurl http://localhost:8000/api/v1/monitoring/queues | jq\n\n# View registered tasks\ncurl http://localhost:8000/api/v1/monitoring/tasks | jq\n\n# Inspect Celery worker\ndocker compose -f compose/docker-compose.yml exec celery celery -A config inspect active\ndocker compose -f compose/docker-compose.yml exec celery celery -A config inspect reserved\ndocker compose -f compose/docker-compose.yml exec celery celery -A config inspect scheduled\n\n# Purge all pending tasks (DANGER: loses tasks!)\ndocker compose -f compose/docker-compose.yml exec celery celery -A config purge -f\n```\n\n### RabbitMQ Troubleshooting\n\n```bash\n# Check RabbitMQ is running\ndocker compose -f compose/docker-compose.yml exec rabbitmq rabbitmqctl status\n\n# List queues with message counts\ndocker compose -f compose/docker-compose.yml exec rabbitmq rabbitmqctl list_queues name messages consumers\n\n# Check connections\ndocker compose -f compose/docker-compose.yml exec rabbitmq rabbitmqctl list_connections\n\n# Access management UI\n# http://localhost:15672 (guest/guest)\n```\n\n### Reload Cerbos Policies\n\n```bash\n# Cerbos watches the policies directory for changes\n# Just edit the YAML files and Cerbos auto-reloads\n\n# Verify policies are valid\ndocker compose -f compose/docker-compose.yml exec cerbos \\\n  /cerbos server --config=/policies/.cerbos.yaml --verify-only\n\n# Force policy reload\ndocker compose -f compose/docker-compose.yml restart cerbos\n```\n\n### Database Backup/Restore\n\n```bash\n# Backup\ndocker compose -f compose/docker-compose.yml exec postgres \\\n  pg_dump -U app app \u003e backup_$(date +%Y%m%d_%H%M%S).sql\n\n# Restore\ndocker compose -f compose/docker-compose.yml exec -T postgres \\\n  psql -U app app \u003c backup_20240101_120000.sql\n```\n\n## Architecture Overview\n\n### Product Overview\n\nThis boilerplate provides a production-ready foundation for multi-tenant B2B/B2C SaaS applications:\n\n- **Multi-tenant by default**: Organizations → Teams → Users hierarchy with data isolation\n- **Hybrid authentication**: Enterprise SSO via Keycloak OIDC or built-in local auth with email verification\n- **Fine-grained authorization**: Cerbos policies enable RBAC and ABAC at resource level\n- **Flexible billing**: Stripe integration supports both org-level (B2B) and user-level (B2C) subscriptions\n- **License tier gating**: Feature flags and rate limits vary by subscription tier (free, starter, pro, enterprise)\n- **Audit compliance**: Tamper-evident audit logs with hash chain verification and HMAC signatures\n- **Async task processing**: Celery with RabbitMQ for background jobs and dead letter queues\n- **Modern admin console**: React frontend with TanStack libraries for data management\n- **API-first design**: All configuration exposed via authenticated endpoints for future portal integration\n- **Enterprise-grade hardening**: Separation of global admin vs org admin vs end-user boundaries, configurable fail modes, PII handling policies\n\nSee [PRD.md](PRD.md) for full product requirements and specifications.\n\n### Request/AuthZ Flow\n\n```mermaid\nsequenceDiagram\n  participant FE as Frontend\n  participant KC as Keycloak\n  participant API as Django/DRF\n  participant CER as Cerbos\n  participant DB as Postgres\n\n  FE-\u003e\u003eKC: OIDC login (or local auth)\n  KC--\u003e\u003eFE: JWT (roles/claims)\n  FE-\u003e\u003eAPI: API call + Bearer JWT\n  API-\u003e\u003eCER: Principal/Resource check\n  CER--\u003e\u003eAPI: Allow/Deny (+policy)\n  API-\u003e\u003eDB: Data access (if allowed)\n  API--\u003e\u003eFE: 200/403 + audit log\n```\n\n### Key Files\n\n| Path | Description |\n|------|-------------|\n| `backend/api/auth.py` | JWT authentication class |\n| `backend/api/permissions.py` | Cerbos permission class |\n| `backend/api/cerbos_client.py` | Cerbos client with caching |\n| `backend/api/views_local_auth.py` | Local authentication endpoints |\n| `backend/api/views_billing.py` | B2B billing endpoints |\n| `backend/api/views_user_billing.py` | B2C billing endpoints |\n| `backend/api/stripe_client.py` | Stripe SDK wrapper |\n| `backend/api/audit.py` | Audit logging system |\n| `policies/*.yaml` | Cerbos policy definitions |\n| `frontend/src/routes/` | React admin pages |\n\n### Configuration Files\n\n#### Keycloak Realm (`keycloak/realm-app.json`)\n\nPre-configured realm with:\n- **Realm**: `app`\n- **Clients**: `api` (public), `global-admin` (confidential), `org-admin` (confidential), `end-user` (public)\n- **Realm Roles**: `platform_admin`, `support_readonly`\n- **Client Roles** (api): `org_admin`, `org_member`, `team_admin`, `team_member`, `billing_admin`\n\nTo customize:\n1. Edit `keycloak/realm-app.json`\n2. Restart Keycloak: `docker compose -f compose/docker-compose.yml restart keycloak`\n\n#### Cerbos Policies (`policies/`)\n\nYAML-based policy files defining access rules:\n\n| Policy File | Resources | Description |\n|-------------|-----------|-------------|\n| `org.yaml` | Organization | Org CRUD, platform_admin bypass |\n| `team.yaml` | Team | Team management, org_admin access |\n| `user.yaml` | User | User profile, self-edit |\n| `membership.yaml` | Membership | Role assignments |\n| `audit_log.yaml` | AuditLog | Log viewing permissions |\n| `sample_resource.yaml` | Sample | Example policy template |\n\nTo add a new policy:\n1. Create `policies/your_resource.yaml`\n2. Define `resourcePolicy` with rules for each action\n3. Cerbos auto-reloads on file change\n\nExample policy structure:\n```yaml\napiVersion: api.cerbos.dev/v1\nresourcePolicy:\n  version: \"default\"\n  resource: \"your_resource\"\n  rules:\n    - actions: [\"read\"]\n      effect: EFFECT_ALLOW\n      roles: [\"org_member\"]\n      condition:\n        match:\n          expr: request.resource.attr.org_id == request.principal.attr.org_id\n```\n\nSee [Cerbos documentation](https://docs.cerbos.dev/) for policy syntax.\n\n## Testing\n\n```bash\n# Run all tests\ndocker compose -f compose/docker-compose.yml exec -w /app/backend web \\\n  pytest --cov=backend --cov-report=term-missing\n\n# Run specific test file\npytest backend/api/tests/test_billing.py -v\n\n# Run with coverage\npytest backend/ --cov=backend --cov-report=html\n```\n\n### Test Coverage\n\nCurrent coverage: **77%+** (threshold: 75%)\n\n### Test Categories\n\n- **Unit Tests**: Models, serializers, utilities\n- **Integration Tests**: API endpoints, auth flows\n- **Policy Tests**: Cerbos RBAC/ABAC policies\n- **Billing Tests**: Stripe integration, webhooks\n\n## License\n\nSee LICENSE file.\n\n## Related Documentation\n\n- [PRD.md](PRD.md) - Full product requirements\n- [Keycloak Admin](http://localhost:8080) - Keycloak console (admin/admin)\n- [RabbitMQ Management](http://localhost:15672) - RabbitMQ console (guest/guest)\n- [Mailpit](http://localhost:8025) - Email testing UI\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmjtechguy%2Fdjango-boilerplate","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmjtechguy%2Fdjango-boilerplate","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmjtechguy%2Fdjango-boilerplate/lists"}