Ctrl K
ComunidadGuías…

AGENTS.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Janua Agent Operating Guide

> [!IMPORTANT]
> MADFAM-ENCLII-FIRST-LEGACY-RAW v1: This document contains legacy raw infrastructure command examples.
> Routine production operations must use Enclii web, API, or CLI. Treat raw
> `kubectl`, `helm`, SSH, provider CLI/API, `docker exec`, and direct container
> access as platform bootstrap or documented break-glass only, and record any
> missing Enclii adapter gap.


<!-- MADFAM-AGENTS-CANONICAL v1 -->

This is the canonical instruction file for Claude, Codex, and any other LLM
agent working in this repository. `CLAUDE.md` is kept only as a compatibility
redirect and should not become the source of truth again.

## Required operating doctrine

- CTM communications work: never execute or attempt real email sends, resends,
  provider tests, or hosted-auth synthetics. Validate templates only with local
  synthetic fixtures and outbound networking blocked. Presentation changes do
  not release billing holds or enable/enforce MFA; Dirección controls activation.
- Recovery boundary: legacy `POST /api/v1/mfa/initiate-recovery` only looks in
  the platform identity pool. It creates an unused one-hour token, then passes
  backup-code data to a mailer that has no temporary recovery link/code. The
  presentation now offers an honest help notice and never emits that list.
  CTM's hosted security flow does not use this endpoint: it accepts TOTP or an
  existing one-use backup code and displays newly generated codes once in the
  authenticated session. Email alone is not a replacement for the second factor.
- Read this file before making repo changes.
- Prefer existing repo conventions, scripts, and docs over introducing new
  patterns.
- Preserve user work and never revert unrelated changes.
- Treat production operations as Enclii-first: use Enclii web, API, or CLI for
  provisioning, deployment, observability, domains, secrets, provider
  operations, scaling, rollback, and remediation.
- Use direct `kubectl`, `helm`, SSH, provider CLIs/APIs, `docker exec`, or
  direct container access only for platform bootstrap or documented break-glass
  emergencies when Enclii is unavailable or lacks an implemented adapter.
- Record any missing Enclii adapter gap instead of normalizing raw production
  access in docs or runbooks.
- Treat user identities, email/phone/profile data, organizations/tenants,
  OAuth clients, redirect URIs, sessions, refresh/access tokens, JWKS/private
  keys, MFA/passkeys/backup codes, SAML/SCIM metadata, webhooks, audit logs,
  migration exports, and service-to-service API keys as sensitive identity data.
  Do not expose sample records, tokens, key material, client secrets, payloads,
  screenshots, logs, or credentials in commits, PRs, issues, chat, CI logs, or
  MCP memory.
- Treat OAuth client mutations, key rotation, user/session migration, SAML/SCIM
  provisioning, webhook delivery, hosted-auth synthetics against production,
  database migrations/seeds/resets, local docker stacks, SDK/package publishing,
  and GitOps deploy/promote/rollback workflows as side-effectful. Require an
  explicit operator request plus the appropriate local guard env before running
  mutating commands.
- Keep examples placeholder-only: Janua signing keys, OAuth/SAML/SCIM secrets,
  database/Redis URLs, GitHub/OAuth provider secrets, Resend/email keys, R2/S3
  credentials, npm/GitHub tokens, Enclii tokens, and production credentials must
  stay in approved secret stores or local operator environments.

## Repo entrypoints

- `README.md`
- `ECOSYSTEM.md`
- `docs/`
- `infra/`
- `.github/workflows/`

## LLM context files

- `llms.txt` is the compact context index.
- `llms-full.txt` is the durable full-context map and operating contract.
- `AGENTS.md` is canonical for agent instructions.
- `CLAUDE.md` redirects here for Claude compatibility.

## Maintenance

Regenerate or repair these files with
`internal-devops/scripts/sync-agent-docs.py` from the labspace ecosystem.

---

## Legacy CLAUDE.md guidance imported on 2026-05-13

<!-- BEGIN LEGACY_CLAUDE_IMPORT -->

# Janua - CLAUDE.md

> **Self-Hosted Authentication for Developers Who Own Their Infrastructure**

## Overview

**Status**: 🟢 Production Ready  
**Purpose**: Open-source OAuth2/OIDC identity platform  
**License**: AGPL v3  
**Domain**: [janua.dev](https://janua.dev)

Janua is a self-hosted authentication platform built with FastAPI and modern web technologies. Think Auth0 or Clerk, but you run it on your own infrastructure. **All features are free and open source.**

## MADFAM Ecosystem Boundary — Identity, Not Money

In the MADFAM ecosystem, Janua is **identity + credential federation, not money**. Per the operator's 2026-04-25 directive, Dhanam is the only service that holds Stripe keys; Janua may relay OAuth credentials for Conekta/Polar integration into Dhanam, but it never holds Stripe keys and never moves money on its own.

The Conekta integration runs through `apps/api/app/services/billing_service.py` per ADR-001 (`internal-devops/decisions/adr-001-conekta-via-janua.md`): consumer apps call `JanuaBillingService` → Janua determines provider (Conekta MX / Polar intl / Stripe fallback) → relays normalized webhook events back to Dhanam. Conekta API keys live only in Janua's namespace; Dhanam never touches raw provider credentials for relayed payments.

**Full monetization architecture**: `internal-devops/ecosystem/monetization-architecture-2026-04-26.md` is the canonical end-to-end reference for money flows, fan-out signing, CFDI pipeline, PMF gating, and operator-only blockers across the ecosystem.

---

## Quick Start

```bash
# Clone and setup
cd janua

# Start backend infrastructure
cd apps/api
docker-compose up -d postgres redis
pip install -r requirements.txt
uvicorn app.main:app --reload --port 4100

# Start dashboard (separate terminal)
cd apps/dashboard
pnpm install
pnpm dev

# Open http://localhost:4101
```

---

## Project Structure

```
janua/
├── apps/
│   ├── api/              # FastAPI backend (Python 3.11+)
│   ├── admin/            # Admin dashboard
│   ├── dashboard/        # User management UI
│   ├── docs/             # Documentation site
│   ├── edge-verify/      # Edge token verification
│   ├── landing/          # Marketing landing page
│   └── marketing/        # Marketing site
├── packages/
│   ├── core/             # Shared TypeScript utilities
│   ├── ui/               # Shared React components
│   ├── jwt-utils/        # JWT utilities
│   ├── monitoring/       # Observability utilities
│   ├── feature-flags/    # Feature flag system
│   └── mock-api/         # Testing mock API
├── packages/ (SDKs)
│   ├── nextjs-sdk/       # Next.js App Router SDK
│   ├── react-sdk/        # React hooks and components
│   ├── vue-sdk/          # Vue 3 composables
│   ├── typescript-sdk/   # Type-safe API client
│   ├── python-sdk/       # Python integration
│   ├── go-sdk/           # Go service integration
│   ├── flutter-sdk/      # Mobile Flutter SDK
│   └── react-native-sdk/ # React Native SDK
├── scripts/
│   └── migration/        # Auth0 migration tools
├── infra/                # Infrastructure (monitoring, secrets, postgres)
├── config/               # Shared config (docker-compose, agent manifest)
├── tests/
│   └── e2e/              # End-to-end tests (Playwright)
└── docs/                 # Documentation files
```

---

## Development Commands

### Backend (FastAPI)
```bash
cd apps/api

# Setup
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt

# Database
docker-compose up -d postgres redis
alembic upgrade head

# Run server
uvicorn app.main:app --reload --port 4100

# Run tests
pytest
pytest --cov=app tests/
```

### Frontend Apps
```bash
# Any frontend app
cd apps/<app-name>
pnpm install
pnpm dev

# Build for production
pnpm build
```

### SDK Development
```bash
cd packages/<sdk-name>
pnpm install
pnpm build
pnpm test
```

### Monorepo Commands (from root)
```bash
pnpm install          # Install all dependencies
pnpm build            # Build all packages
pnpm dev              # Run all in dev mode
pnpm test             # Run all tests
pnpm lint             # Lint all packages
```

---

## Port Allocation

Per [MADFAM Ecosystem Standard](https://github.com/madfam-org/solarpunk-foundry/blob/main/docs/PORT_ALLOCATION.md), Janua uses the 4100-4199 block.

| Service | Port | Container | Public Domain |
|---------|------|-----------|---------------|
| API | 4100 | janua-api | api.janua.dev |
| Dashboard | 4101 | janua-dashboard | app.janua.dev |
| Admin | 4102 | janua-admin | admin.janua.dev |
| Docs | 4103 | janua-docs | docs.janua.dev |
| Website | 4104 | janua-website | janua.dev |
| Demo | 4105 | janua-demo | demo.janua.dev |
| Email Worker | 4110 | janua-worker-email | - |
| WebSocket | 4120 | janua-ws | - |
| Metrics | 4190 | janua-metrics | - |

**Note**: Production now uses standard MADFAM ports. API runs on port 4100 via kubectl port-forward service (`janua-port-forward.service`).

---

## Features

### ✅ Working
- Email/password authentication
- OAuth (Google, GitHub, Microsoft, etc.)
- SAML 2.0 SSO
- TOTP/SMS multi-factor authentication
- WebAuthn/Passkeys (FIDO2)
- Multi-tenancy with organizations and RBAC
- REST API (202 endpoints)
- SDKs: React, Vue, Next.js, Python, Go, Flutter

### ⚠️ In Progress
- Documentation completeness
- Scale testing
- Edge case handling
- UI component polish

### ❌ Not Available
- Managed hosting (self-host only)
- Enterprise support contracts
- SOC 2 compliance reports

---

## Admin Panel (admin.janua.dev)

The Admin Panel is the internal management interface for Janua platform operators.

**Access:** https://admin.janua.dev

**Authorization Model:**
Access to Janua Admin requires BOTH:
1. **Email Domain**: Must be from an allowed domain (default: `@janua.dev`, `@madfam.io`)
2. **User Role**: Must have an admin role (`superadmin` or `admin`)

**Configuration (Environment Variables):**
| Variable | Description | Default |
|----------|-------------|---------|
| `ALLOWED_ADMIN_DOMAINS` | Comma-separated allowed email domains | `@janua.dev,@madfam.io` |
| `ALLOWED_ADMIN_ROLES` | Comma-separated allowed roles | `superadmin,admin` |

**Adding New Platform Operators:**
1. Create user in Janua with appropriate role (`superadmin` or `admin`)
2. Ensure their email domain is in `ALLOWED_ADMIN_DOMAINS` (or add it)
3. No code changes required - configuration-driven authorization

**Key Files:**
| Purpose | Location |
|---------|----------|
| Middleware (server-side auth) | `apps/admin/middleware.ts` |
| Auth Context (client-side) | `apps/admin/lib/auth.tsx` |
| Login Page | `apps/admin/app/login/page.tsx` |
| Access Denied Page | `apps/admin/app/access-denied/page.tsx` |

**Consistent with Enclii Dispatch:**
Both admin interfaces (admin.janua.dev and admin.enclii.dev) use the same domain+role based authorization pattern for scalability and consistency.

---

## API Reference

**Local Base URL**: `http://localhost:4100`
**Production Base URL**: `https://api.janua.dev`
**OpenAPI Docs**: `/docs`

### Production Endpoints
- `https://api.janua.dev` - Main API
- `https://auth.madfam.io` - Auth alias (same API)

### Key Endpoints
```
POST   /api/v1/auth/login          # Email/password login
POST   /api/v1/auth/register       # User registration
POST   /api/v1/auth/refresh        # Refresh JWT (refuses a revoked session; 503 while Redis is down)
GET    /api/v1/auth/me             # Current user
POST   /api/v1/mfa/enable          # Start TOTP MFA enrolment
POST   /api/v1/passkeys/register/options # Passkey registration options (then /register/verify)
POST   /api/v1/auth/signout        # Sign out: revokes this session's refresh-token family
DELETE /api/v1/sessions/{id}       # Revoke one session (owner or platform admin)
POST   /api/v1/oauth/revoke        # RFC 7009 revocation (client authentication required)
GET    /api/v1/health/ready        # Readiness probe: reports Redis/DB, gates on neither

GET    /api/v1/users               # List users (admin)
POST   /api/v1/admin/users         # Create user directly (admin; Supabase createUser parity)
GET    /api/v1/organizations       # List organizations
POST   /api/v1/organizations       # Create organization
```

**Admin create-user** (`POST /api/v1/admin/users`): platform-admin-gated
(`check_admin_permission`). Body: `email`, optional `name`, optional `password`,
`is_admin`, `email_verified`, optional `organization_id` + `organization_role`
(member|admin|owner). When `password` is omitted the user is created without a
usable password and the response carries a one-time `set_password_token` (a
`PasswordReset` token, 24h) that the new user redeems at
`POST /api/v1/auth/password/reset` — no guessable default is ever set. The
platform `is_admin` flag and org-scoped `organization_role` are disjoint (an org
role never grants platform admin). The response never returns the password/hash.

**Runtime signup switch**: `auth.allow_signups` (system setting) now overrides the
`ENABLE_SIGNUPS` env default at signup time, so an operator can enable/disable
self-signup without a redeploy. Precedence: DB `auth.allow_signups` if set, else
`ENABLE_SIGNUPS`. `EMAIL_PROVIDER` accepts only `resend|smtp` (SendGrid was never
wired on the auth path and its dep is not installed).

### Dhanam Billing Integration

Janua integrates with Dhanam (billing platform) for subscription management via checkout and webhook endpoints.

**Endpoints:**
- `POST /api/v1/checkout/dhanam` -- Create a checkout session for a product plan
- `POST /api/v1/webhooks/dhanam` -- Receive subscription lifecycle events from Dhanam

**Products**: Open registration — any lowercase alphanumeric product name is accepted
(zero-touch: new ecosystem services don't need Janua code changes).

**Plan ID Format:** `{product}_{tier}` (e.g., `karafiel_pro`, `newservice_essentials`)

**Client Registration:** `POST /api/v1/oauth/clients/register` — headless, idempotent
client registration via `X-Internal-API-Key` header (for consumer bootstrap scripts).
Identity is the client **`name`** (one client per consumer edge, ADR-006): same name
updates in place and returns no secret; a new name creates a new client **even on a
shared `audience`**. Writes `audit_logs` rows for every internal-key registration.

**Consumer provisioning CLI:** `@janua/cli` (`packages/cli`) — `janua provision apply|plan|verify`
with `janua.client.yaml`. Publish tag `cli-v*`. Template: `examples/consumer-bootstrap/`.
Docs: `apps/docs/app/cli/page.mdx`, `packages/cli/README.md`.

**Key Files:**
| Purpose | Location |
|---------|----------|
| Checkout handler | `apps/api/app/routers/v1/checkout_dhanam.py` |
| Webhook handler | `apps/api/app/routers/v1/webhooks_dhanam.py` |
| Webhook tests | `apps/api/tests/unit/routers/test_webhooks_dhanam.py` |

### Email events and first-party measurement

Operator detail and owner steps: [`docs/runbooks/resend-email-events.md`](docs/runbooks/resend-email-events.md).
Webhook secret rotation: [`docs/runbooks/secrets/resend-webhook-secret-rotation.md`](docs/runbooks/secrets/resend-webhook-secret-rotation.md).

- **Resend webhook receiver** `POST /api/v1/email/webhooks/resend/{cuenta}`
  (`cuenta` = `platform` or a tenant account's slug, one per Resend ACCOUNT;
  the map is `WEBHOOK_SECRET_SETTINGS` in `app/services/email_events.py`).
  Public; the Svix-style signature is the authentication, verified with
  `RESEND_WEBHOOK_SECRET_<CUENTA>` (e.g.
  `RESEND_WEBHOOK_SECRET_PLATFORM`). Secret unset or blank: **404**, same body
  as an unknown account. Bad or missing signature: **401**
  `{"detail":"Invalid webhook signature"}`. Verified and stored: **200**
  `{"status":"stored"}` (`duplicate` / `ignored` otherwise). Nothing is written
  before the signature verifies; no recipient, subject, body, IP or user agent
  is stored. The per-app feed is `GET /api/v1/internal/email/events`.
- **First-party open/click measurement** (#648). Only for mail sent through
  `POST /api/v1/internal/email/send` with `"track_engagement": true` that is
  NOT token mail (`contains_token_link` false and no credential-looking link),
  still has an HTML part, and whose sender binding has a tracking host on the
  From domain actually used. Token-bearing mail (sign-in, reset, verification,
  invitations) is never measured, and templates/preview are never
  instrumented. Links become `{host}/e/c/{token}/{i}` and a pixel
  `{host}/e/o/{token}.gif` is added; only the token's SHA-256 is stored.
  Resend's own tracking stays OFF and `EMAIL_TRACKED_SENDER_DOMAINS` stays empty.
- **Per-tenant tracking host**: the setting a binding names in
  `tracking_host_setting` (https origin only, no path or port) is that tenant's
  on-switch; unset or invalid means the tenant's mail is never instrumented.
  Production sets it to a host on the tenant's own domain in
  `k8s/base/deployments/janua-api.yaml` (#652). The platform binding has none.
- **Tracking host scope** (#655): tracking hosts are added to
  `TrustedHostMiddleware`'s list at startup, and `TrackingHostScopeMiddleware`
  (registered after it, so it runs first) answers 404 to every path on a
  tracking host except `/e/o/*` and `/e/c/*`, and closes websockets with 1008.
  Sign-in, reset and OIDC discovery are never served under a tenant's domain.
  Hosts are fixed at startup: changing a tenant's tracking host needs a restart.

| Purpose | Location |
|---------|----------|
| Webhook receiver | `apps/api/app/routers/v1/email_webhooks.py` |
| Signature, parsing, minimization | `apps/api/app/services/email_events.py` |
| Pixel / click endpoints | `apps/api/app/routers/v1/email_engagement.py` |
| Instrumentation rules | `apps/api/app/services/email_engagement.py` |
| Tracking host per binding | `apps/api/app/services/sender_binding.py` (`tracking_host_for`, `tracking_bindings`) |
| Host scope middleware | `apps/api/app/middleware/tracking_host_scope.py` |
| Tests | `tests/unit/routers/test_email_webhooks_resend.py`, `test_email_engagement_endpoints.py`, `tests/unit/services/test_email_engagement.py`, `tests/unit/middleware/test_tracking_host_scope.py` (under `apps/api/`) |

---

## Environment Variables

```bash
# Database
DATABASE_URL=postgresql://user:pass@localhost:5432/janua
REDIS_URL=redis://localhost:6379

# JWT (RS256 with RSA-2048 keys)
JWT_SECRET_KEY=your-secret-key       # For HS256 fallback
JWT_ALGORITHM=RS256
JWT_PRIVATE_KEY_PATH=./keys/private.pem
JWT_PUBLIC_KEY_PATH=./keys/public.pem
ACCESS_TOKEN_EXPIRE_MINUTES=15       # Default: 15min
REFRESH_TOKEN_EXPIRE_DAYS=7          # Default: 7 days
# MFA challenge tokens (HS256, 5 min) are signed with JWT_SECRET_KEY when set,
# otherwise with a key derived from SECRET_KEY; outside dev/test there is no
# built-in default (#729). Note: k8s/base sets `JWT_SECRET`, which the API does
# not read; production therefore uses the SECRET_KEY-derived key. Renaming that
# variable changes the challenge key (in-flight challenges expire): do it with a
# promote, not silently.
# Wrong second-factor codes count toward ACCOUNT_LOCKOUT_* (#731). The hosted
# password form always challenges MFA-enrolled users, independent of
# MFA_ENFORCE_ON_LOGIN (#732).

# Field Encryption (required in production — SOC 2 CF-01)
# Generate with: python -c 'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())'
FIELD_ENCRYPTION_KEY=your-fernet-base64-key

# Admin Bootstrap (optional - creates admin@madfam.io on first run)
ADMIN_BOOTSTRAP_PASSWORD=your-secure-password

# OAuth Providers
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
GITHUB_CLIENT_ID=...
GITHUB_CLIENT_SECRET=...

# Email (SMTP - legacy)
SMTP_HOST=localhost
SMTP_PORT=1025
SMTP_FROM=noreply@janua.dev

# Email (Resend)
EMAIL_ENABLED=true               # Enable email sending (default: false)
EMAIL_PROVIDER=resend             # Email provider (resend)
EMAIL_FROM_ADDRESS=hola@madfam.io  # PLATFORM sender; see docs/EMAIL_SENDER_POLICY.md
EMAIL_FROM_NAME=MADFAM            # platform default, NOT "one sender for every
                                  # platform": vCTO tenants resolve their own
                                  # From line from sender_binding.py
EMAIL_SYSTEM_FROM_ADDRESS=noreply@madfam.io  # R101: platform sender for system
                                  # and security mail; empty => everything back
                                  # on EMAIL_FROM_ADDRESS (the rollback)
EMAIL_SUPPORT_REPLY_TO=support@madfam.io    # Reply-To on system mail
EMAIL_SECURITY_REPLY_TO=security@madfam.io  # Reply-To on security mail
RESEND_API_KEY=re_XXXXX           # MADFAM's Resend account (platform sending)
RESEND_VERIFIED_DOMAINS=madfam.io # domains verified on MADFAM's account
RESEND_WEBHOOK_SECRET_PLATFORM=   # whsec_... of MADFAM's Resend webhook; unset =>
                                  # /email/webhooks/resend/platform answers 404
EMAIL_TRACKED_SENDER_DOMAINS=     # domains with RESEND tracking on; keep empty
# Per-tenant (vCTO) variables are NAMED by the tenant's binding in
# app/services/sender_binding.py and are all optional:
#   - its own Resend key (credential_ref); absent => the tenant degrades to the
#     platform sender, the link still sends
#   - its first-party tracking origin (tracking_host_setting); unset => the
#     tenant's mail is never measured
#   - its webhook secret RESEND_WEBHOOK_SECRET_<CUENTA> (WEBHOOK_SECRET_SETTINGS
#     in app/services/email_events.py); unset => its receiver answers 404
```

### Admin Bootstrap

Set `ADMIN_BOOTSTRAP_PASSWORD` to automatically create an admin user on first startup:

```bash
# Creates admin@madfam.io with is_admin=true, email_verified=true
ADMIN_BOOTSTRAP_PASSWORD='SecurePassword123!' uvicorn app.main:app --port 4100
```

---

## SDK Installation

### NPM Registry Configuration
```bash
# Add to .npmrc
@janua:registry=https://npm.madfam.io
//npm.madfam.io/:_authToken=${NPM_MADFAM_TOKEN}
```

### React/Next.js
```bash
npm install @janua/react-sdk
# or
npm install @janua/nextjs-sdk
```

### Python
```bash
pip install janua-sdk
```

### Go
```bash
go get github.com/madfam-org/janua/packages/go-sdk
```

---

## Auth0 Migration

Migrating from Auth0? Use our migration tool:

```bash
cd scripts/migration
pip install -r requirements.txt
python auth0_migrate.py --config config.json
```

**Savings**: $24,000-58,000/year vs Auth0 enterprise pricing.

See `scripts/migration/README.md` for full guide.

---

## Testing

```bash
# Backend tests
cd apps/api
pytest                           # All tests
pytest tests/unit/               # Unit tests only
pytest tests/integration/        # Integration tests
pytest --cov=app --cov-report=html  # Coverage report

# Frontend tests
cd apps/dashboard
pnpm test
pnpm test:e2e                    # Playwright E2E
```

---

## Key Documentation

| Document | Path |
|----------|------|
| API Reference | `apps/api/docs/` |
| SDK Guides | `packages/*/README.md` |
| Migration Guide | `scripts/migration/README.md` |
| Architecture | `docs/architecture/` |
| Silent SSO, estate cookie, account switching | `docs/architecture/SILENT_SSO_SESSION.md` |
| OAuth consent, revocation, passkeys, health and readiness (runbook hub) | `docs/runbooks/oauth-shared-state-redis.md` |
| Protected resources (RFC 8707), CIMD, MCP clients such as Claude | `docs/reference/PROTECTED_RESOURCES_AND_MCP_CLIENTS.md`; after promote: `docs/runbooks/mcp-connector-post-promote.md` |
| Deployment | `docs/deployment/` |
| Domain manifest | `enclii.yaml` |

---

## Architecture

```
┌─────────────────────────────────────────────────────┐
│                    Client Apps                       │
│  (React, Vue, Next.js, Mobile, Backend Services)    │
└─────────────────────┬───────────────────────────────┘
                      │ SDKs
┌─────────────────────▼───────────────────────────────┐
│                  Janua API                           │
│         FastAPI + SQLAlchemy + Redis                 │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐   │
│  │  Auth   │ │  Users  │ │  Orgs   │ │   MFA   │   │
│  └─────────┘ └─────────┘ └─────────┘ └─────────┘   │
└─────────────────────┬───────────────────────────────┘
                      │
┌─────────────────────▼───────────────────────────────┐
│              PostgreSQL + Redis                      │
│         (Sessions, Users, Organizations)             │
└─────────────────────────────────────────────────────┘
```

---

## Deployment

### Docker Compose (Recommended)
```bash
docker-compose -f docker-compose.prod.yml up -d
```

### Kubernetes
```bash
kubectl apply -f k8s/
```

### Enclii (MADFAM Infrastructure)
```bash
enclii deploy --service janua
```

### Domain Auto-Provisioning

`enclii.yaml` at the repo root declares domains for Enclii's self-service auto-provisioning.
On push to main, Enclii's webhook fetches this file and provisions:
- `CustomDomain` record in the platform DB
- Cloudflare tunnel route (via API)
- DNS CNAME record (→ `tunnel.enclii.dev`)

Only janua-website domains are listed due to the single-service-per-file format.
Other service domains (api, dashboard, admin, docs) are managed via static tunnel config in
`enclii/infra/k8s/production/cloudflared-unified.yaml`.

See [Enclii Service Spec Reference](https://docs.enclii.dev/reference/service-spec) for the full schema.

### Production Infrastructure
**Control-plane node**: dedicated bare-metal (control-plane + primary workload); reached only via Cloudflare Tunnel
**Worker node**: dedicated bare-metal (worker + Longhorn 2nd replica)
**CI builder node**: cloud compute instance (ARC runners only)
**K3s API**: not published here. Node hostnames, addresses and hardware live only in the private operations record (`madfam-org/internal-devops`).
**K8s Cluster**: K3s 3-node cluster on Hetzner dedicated servers (CP + worker + builder)
**Namespace**: janua

**Active Services**:
| Service | Type | Notes |
|---------|------|-------|
| janua-api | Deployment | Port 4100, ClusterIP |
| janua-dashboard | Deployment | Port 4101 |
| janua-admin | Deployment | Port 4102 |
| janua-docs | Deployment | Port 4103 |
| janua-website | Deployment | Port 4104 |

**Cloudflare Tunnel Routing** (via unified `enclii-production` tunnel):

**Port Mapping Hierarchy** (Critical for Cloudflare Tunnel Configuration):
1. **Container Port**: What the application listens on internally (e.g., 4100, 4101, 4104)
2. **K8s Service Port**: What the service exposes to the cluster (typically port 80)
3. **Cloudflare Tunnel Route**: Should point to K8s Service port (80), NOT container port

| Public Domain | Internal Service (K8s Service:Port) | Container Port |
|---------------|-------------------------------------|----------------|
| api.janua.dev / auth.madfam.io | janua-api.janua.svc.cluster.local:80 | 4100 |
| app.janua.dev / dashboard.janua.dev | janua-dashboard.janua.svc.cluster.local:80 | 4101 |
| admin.janua.dev | janua-admin.janua.svc.cluster.local:80 | 4102 |
| docs.janua.dev | janua-docs.janua.svc.cluster.local:80 | 4103 |
| janua.dev / www.janua.dev | janua-website.janua.svc.cluster.local:80 | 4104 |

> **Important**: The Cloudflare tunnel routes traffic to K8s Services, not directly to containers. Always use the K8s Service port (80) in tunnel configuration, not the container port.

See `enclii/infra/k8s/production/cloudflared-unified.yaml` for routing configuration.

**NetworkPolicy**: Requires `allow-cloudflared-ingress` policy to permit traffic from cloudflare-tunnel namespace.

---

## Deployment Pipeline (dev → staging → prod)

Janua is a **Phase 2** target (critical-service priority — auth underpins the
entire MADFAM ecosystem) for the 3-tier pipeline defined in
[internal-devops/rfcs/0001-dev-staging-prod-pipeline.md](https://github.com/madfam-org/internal-devops/blob/main/rfcs/0001-dev-staging-prod-pipeline.md).

**Current state (post PP.3b, PR #404):** Structural 3-tier pipeline is in the
repo. `docker-publish.yml` commits digests to `k8s/overlays/staging/` only;
production digests land via `promote-to-prod.yml`. Full staging tenant requires
ops bootstrap (ArgoCD, secrets, tunnel routes) — see
[docs/PP_3B_STAGING_PIPELINE.md](docs/PP_3B_STAGING_PIPELINE.md).

**Legacy note:** Before PP.3b, every push to `main` committed digests directly
into `k8s/base/deployments/kustomization.yaml` and ArgoCD reconciled straight
into prod (~15% RFC 0001 compliance). See
[docs/PP_3_STAGING_AUDIT.md](docs/PP_3_STAGING_AUDIT.md) for the row-by-row audit.

### Pipeline status (2026-05-23)

| Item | Status |
|------|--------|
| Staging overlay in repo | Done (PR #404) |
| Production overlay in repo | Done (PR #404) |
| Promote / rollback workflows | On `main` |
| ArgoCD → production overlay | **Ops pending** |
| janua-staging tenant live | **Ops pending** |
| Staging smoke in CI | Open |

### Remaining divergences (ops + follow-up PRs)

| Divergence | Impact | Resolution |
|---|---|---|
| ArgoCD still on `k8s/base/deployments` until ops migrates | Prod may use `:main` tags if overlay not wired | Ops: point App at `k8s/overlays/production` |
| No staging subdomain / tunnel routes yet | Can't cross-service smoke | Ops: Cloudflare + `janua-staging` App |
| No distinct staging JWT keypair in cluster yet | Staging not safe to use | Ops: `janua-staging-secrets.template.yaml` |
| No staging OAuth clients with Google/GitHub/Microsoft | Full OIDC untestable in staging | Ops: register staging provider apps |
| No staging smoke step in docker-publish | Soak not auto-verified | Follow-up PR |
| Nightly prod→staging masked DB refresh | Staging DB seeded manually | Deferred (RFC 0001) |

### Janua-specific staging constraints

Janua cannot use itself as a "staging auth provider" the way every other
MADFAM service does. Staging Janua must be a **fully separate tenant**:

- **Separate PostgreSQL DB** (`janua_staging`), never a replica of prod
- **Separate OAuth client registry** — staging downstream services register against staging Janua, never against prod
- **Distinct JWT RSA-2048 keypair** — if staging ever shares prod keys, a compromised staging env can issue prod-valid tokens. **Hard requirement**, not a best practice
- **Distinct `FIELD_ENCRYPTION_KEY`** (Fernet) — needed for SOC 2 CF-01 separation
- **Distinct issuer URL** — `https://staging-auth.madfam.io` or `https://auth-staging.janua.dev`, Cloudflare tunnel + DNS managed by ops

A "staging reads prod in read-only mode" workaround is **rejected** because Janua issues JWTs (staging tokens would be valid against prod services), the OAuth client registry is write-heavy, and audit log bleed-through is unacceptable.

See `docs/PP_3_STAGING_AUDIT.md` § Janua-specific staging constraints for the
full `janua-staging-secrets` template (keys to generate, providers to register,
redirect URIs).

### Promotion pattern

Janua is **Pattern B — manual gate** per RFC 0001 § Promotion mechanics.
Reasoning: Janua is the auth floor for every MADFAM service. A wrong promote
breaks every downstream login. `enclii.yaml` declares:

```yaml
promotion:
  pattern: manual
  min_soak_minutes: 30
  require_smoke_pass: true
```

Auto-promote exists in `promote-to-prod.yml` but is gated off unless repo
variable `AUTO_PROMOTE_ENABLED=true`.

### What runs on push to `main`

| Workflow | Trigger | Effect |
|---|---|---|
| `docker-publish.yml` | push to main (apps/packages/Dockerfile paths) | Builds images, commits digests to `k8s/overlays/staging/`, ArgoCD syncs staging (after ops bootstrap) |
| `promote-to-prod.yml` | workflow_dispatch (manual) | Promotes soaked staging digest(s) → `k8s/overlays/production/` |
| `sync-prod-gitops.yml` | workflow_dispatch (break-glass, `main` only) | ARC apply of `main`'s production overlay + GHCR refresh when cluster lags git |
| `rollback-prod.yml` | workflow_dispatch (incident) | Rolls back prod digest from git history or explicit input; an explicit digest must be one production already ran (main's prod-overlay history) |

**Docs image path-filter trap.** `docker-publish.yml` rebuilds a service only
when its `dorny/paths-filter` entry matches. The `docs` entry is `apps/docs/**`
and `Dockerfile.docs` only, without `pnpm-lock.yaml` or `packages/**`, so a
lockfile-only dependency bump rebuilds admin, dashboard and website but **not**
docs. Ship docs with `gh workflow run docker-publish.yml`; a dispatch rebuilds
all five services and commits all five staging digests (`force_all` is
informational). Details: `docs/PP_3B_STAGING_PIPELINE.md`.

**Image installs use pnpm 9, the repo pins pnpm 10 (known gap).** The root
`packageManager` is `pnpm@10.25.0` and CI installs with `--frozen-lockfile`, but
`Dockerfile.admin`, `.dashboard`, `.docs` and `.website` run
`corepack prepare pnpm@9.15.0` and `pnpm install --no-frozen-lockfile
--shamefully-hoist`. Images can therefore resolve versions the lockfile does
not pin, including after an advisory bump. Moving them to pnpm 10 with
`--frozen-lockfile` is not a one-line change: pnpm 10 skips dependency build
scripts unless `pnpm.onlyBuiltDependencies` lists them, and the four images
must be rebuilt and checked. Track it as its own change (listed in
«Known gaps / Backlog» at the end of this file).

Post-promote reconcile and common blockers (Kyverno, GHCR pull, Argo app name):
[docs/runbooks/production-gitops-reconcile.md](docs/runbooks/production-gitops-reconcile.md).
Incident record (2026-06-15 website): [docs/runbooks/incidents/2026-06-15-janua-website-prod-rollout.md](docs/runbooks/incidents/2026-06-15-janua-website-prod-rollout.md).

Canonical operator guide: [docs/PP_3B_STAGING_PIPELINE.md](docs/PP_3B_STAGING_PIPELINE.md).

---

## Common Workflows

### Adding a New API Endpoint

1. **Create route handler** in `apps/api/app/routers/v1/`
   ```python
   # apps/api/app/routers/v1/my_feature.py
   from fastapi import APIRouter, Depends
   from app.dependencies import get_current_user

   router = APIRouter(prefix="/my-feature", tags=["my-feature"])

   @router.get("/")
   async def list_items(user = Depends(get_current_user)):
       return {"items": []}
   ```

2. **Register router** in `apps/api/app/main.py`
   ```python
   from app.routers.v1 import my_feature
   app.include_router(my_feature.router, prefix="/api/v1")
   ```

3. **Add tests** in `apps/api/tests/unit/routers/`
4. **Update OpenAPI** by checking `/docs` endpoint
5. **Validate**: `pytest tests/unit/routers/test_my_feature.py`

### Adding OAuth Provider

1. **Configure provider** in `apps/api/app/config.py`
2. **Add provider handler** in `apps/api/app/services/oauth_service.py`
3. **Register routes** in `apps/api/app/routers/v1/oauth.py`
4. **Update SDK types** in `packages/typescript-sdk/src/types.ts`
5. **Add to UI** in `packages/ui/src/components/auth/`

### Database Migration

```bash
cd apps/api

# Create migration
alembic revision --autogenerate -m "add_user_preferences"

# Review generated migration
cat alembic/versions/*_add_user_preferences.py

# Apply locally
alembic upgrade head

# Rollback if needed
alembic downgrade -1
```

**Production is different: migrations are applied by hand, and a ledger records
them.** `promote-to-prod.yml` only moves image digests; nothing in the pipeline
runs `alembic upgrade`. A new revision ships with owner-run SQL
(`docs/ops/sql/<revision>.sql`, e.g. `018_email_events`,
`019_email_first_party_engagement`), applied in the database pod BEFORE any
image that needs it is promoted. After reading the database, a reviewed PR
records the revision in `apps/api/alembic/PROD_ALEMBIC_STATE.json`
(`recorded_revision`, `verified_at`, `verified_by`, a `notes` line). The promote
guard (`apps/api/scripts/alembic_promote_guard.py`) compares the repo head with
that ledger and blocks unless they match or `migrations_acknowledged=true`.
Never edit the ledger forward without reading the database. Full procedure:
[`docs/runbooks/ALEMBIC_CONVERGENCE.md`](docs/runbooks/ALEMBIC_CONVERGENCE.md).

### SDK Updates

```bash
# After API changes, update TypeScript types
cd packages/typescript-sdk
pnpm build

# Rebuild dependent packages
cd packages/react-sdk && pnpm build
cd packages/nextjs-sdk && pnpm build

# Update frontend apps
cd apps/dashboard && pnpm build
```

**SDK timers and releases.** `JanuaClient` starts its background timers
(auto-refresh interval, proactive refresh, WebSocket auto-connect) only where a
browser `window` exists, so a client built per server render holds nothing
(0.1.x before 0.1.5 leaked one interval per server render). Keep it that way:
no timer in a constructor without a `window` guard and an `unref()`, and every
timer reachable from `destroy()`. Versions are bumped by hand in
`package.json` (and `typescript-sdk/src/version.ts`, which a test keeps in
sync); `publish-sdks.yml` publishes on a `<package-dir>-v<version>` tag with
`pnpm publish`. Publish `typescript-sdk` and `ui` before the framework SDKs
that depend on them via `workspace:^`. See `docs/PUBLISHING.md`.

### Running Specific Test Suites

```bash
cd apps/api

# Run by category
pytest tests/unit/                    # Unit tests
pytest tests/integration/             # Integration tests
pytest tests/unit/services/           # Service tests only
pytest tests/unit/routers/            # Router tests only

# Run by marker
pytest -m "not slow"                  # Skip slow tests
pytest -m "security"                  # Security tests only

# Run with coverage
pytest --cov=app --cov-report=html tests/
open htmlcov/index.html
```

---

## Debugging Guide

### Enable Debug Logging

```bash
# Full debug mode
DEBUG=true LOG_LEVEL=DEBUG uvicorn app.main:app --reload --port 4100

# SQL query logging
SQLALCHEMY_ECHO=true uvicorn app.main:app --reload --port 4100

# Both
DEBUG=true LOG_LEVEL=DEBUG SQLALCHEMY_ECHO=true uvicorn app.main:app --reload
```

### API Debugging

```bash
# Health check
curl http://localhost:4100/health | jq

# Detailed health with dependencies
curl http://localhost:4100/health/detailed | jq

# Test authentication flow
curl -X POST http://localhost:4100/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "test@example.com", "password": "password123"}'

# Test with JWT
TOKEN="your-jwt-token"
curl http://localhost:4100/api/v1/auth/me \
  -H "Authorization: Bearer $TOKEN" | jq
```

### Database Debugging

```bash
# Connect to local database
psql postgresql://user:pass@localhost:5432/janua

# Check active connections
psql -c "SELECT pid, usename, state, query FROM pg_stat_activity WHERE datname='janua';"

# Check table sizes
psql -c "SELECT relname, pg_size_pretty(pg_total_relation_size(relid)) FROM pg_catalog.pg_statio_user_tables ORDER BY pg_total_relation_size(relid) DESC LIMIT 10;"

# Check migration status
alembic current
alembic history --verbose
```

### Redis Debugging

```bash
# Connect to Redis
redis-cli -h localhost -p 6379

# Check session keys
redis-cli KEYS "session:*"

# Check cache keys
redis-cli KEYS "cache:*"

# Monitor in real-time
redis-cli MONITOR

# Memory stats
redis-cli INFO memory
```

### JWT Token Debugging

```bash
# Decode JWT (without verification)
echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null | jq

# Verify with public key
python3 << 'EOF'
import jwt
token = "your-jwt-token"
public_key = open("keys/public.pem").read()
try:
    payload = jwt.decode(token, public_key, algorithms=["RS256"])
    print("Valid:", payload)
except jwt.ExpiredSignatureError:
    print("Token expired")
except jwt.InvalidTokenError as e:
    print("Invalid:", str(e))
EOF
```

### Common Issues

| Symptom | Check | Fix |
|---------|-------|-----|
| 500 errors | API logs, `DEBUG=true` | Check exception traceback |
| Auth fails | Token expiry, algorithm | Refresh token, check JWT_ALGORITHM |
| DB connection | PostgreSQL status | `docker-compose up -d postgres` |
| Cache miss | Redis connection | `docker-compose up -d redis` |
| CORS errors | Preflight from the origin: `204` without `access-control-allow-origin` means not allowed | Register the origin in an OAuth client's `redirect_uris`, or add it to the static `CORS_ORIGINS` in `k8s/base/deployments/janua-api.yaml` ([procedure](docs/guides/PROVIDER_OPERATIONS.md#adding-a-static-browser-origin)) |
| SSO fails | Certificate, clock sync | Check IdP cert, NTP sync |

---

## Key File Locations

### API (FastAPI Backend)

| Purpose | Location |
|---------|----------|
| Entry point | `apps/api/app/main.py` |
| Configuration | `apps/api/app/config.py` |
| Routes | `apps/api/app/routers/v1/` |
| Services | `apps/api/app/services/` |
| Models | `apps/api/app/models/` |
| Schemas | `apps/api/app/schemas/` |
| Middleware | `apps/api/app/middleware/` |
| Dependencies | `apps/api/app/dependencies.py` |
| Exceptions | `apps/api/app/core/exceptions.py` |
| Error handling | `apps/api/app/core/errors.py` |
| Caching | `apps/api/app/core/caching.py` |
| Performance | `apps/api/app/core/performance.py` |
| Database migrations | `apps/api/alembic/versions/` |

### Authentication

| Purpose | Location |
|---------|----------|
| Auth router | `apps/api/app/routers/v1/auth.py` |
| Auth service | `apps/api/app/services/auth_service.py` |
| JWT handling | `apps/api/app/core/jwt_manager.py` (`app/services/jwt_service.py` has no mounted caller) |
| MFA router | `apps/api/app/routers/v1/mfa.py` |
| Social login (OAuth client side) | `apps/api/app/services/oauth.py`, `apps/api/app/routers/v1/oauth.py` |
| OAuth/OIDC provider (authorize, consent, token, revoke, introspect, account chooser) | `apps/api/app/routers/v1/oauth_provider.py` |
| Discovery metadata (OIDC + RFC 8414, one document) | `apps/api/app/core/oauth_metadata.py` |
| Protected-resource registry (closed list, RFC 8707) and its client policy | `apps/api/app/core/protected_resources.py`; CIMD fetcher `apps/api/app/services/client_id_metadata.py`; tokens `apps/api/app/services/resource_tokens.py` |
| Sessions API | `apps/api/app/routers/v1/sessions.py` |
| Revocation list reader | `apps/api/app/services/token_revocation.py` |
| Strict Redis operations and the circuit breaker | `apps/api/app/core/redis_circuit_breaker.py` |
| Health and readiness | `apps/api/app/routers/v1/health.py`, checks registered in `apps/api/app/main.py` |
| Passkey/WebAuthn | `apps/api/app/routers/v1/passkeys.py`; SDK `packages/typescript-sdk/src/webauthn-helper.ts`, `src/utils/webauthn-encoding.ts` |
| SSO/SAML | `apps/api/app/sso/` |

### SDKs

| SDK | Main File | Types |
|-----|-----------|-------|
| TypeScript | `packages/typescript-sdk/src/client.ts` | `packages/typescript-sdk/src/types.ts` |
| React | `packages/react-sdk/src/index.tsx` | `packages/react-sdk/src/types.ts` |
| Next.js | `packages/nextjs-sdk/src/index.ts` | `packages/nextjs-sdk/src/types.ts` |
| Python | `packages/python-sdk/src/janua/client.py` | `packages/python-sdk/src/janua/types.py` |
| Go | `packages/go-sdk/janua/client.go` | `packages/go-sdk/janua/types.go` |

### Frontend Apps

| App | Main Entry | Config |
|-----|------------|--------|
| Dashboard | `apps/dashboard/app/page.tsx` | `apps/dashboard/lib/janua-client.ts` |
| Admin | `apps/admin/app/page.tsx` | `apps/admin/lib/janua-client.ts` |

### Documentation

| Doc Type | Location |
|----------|----------|
| API FAQ | `apps/api/docs/FAQ.md` |
| Guides | `docs/guides/` |
| Architecture | `docs/architecture/` |
| Security | `docs/security/` |
| Deployment | `docs/deployment/` |

---

## Error Codes Reference

### API Error Codes

| HTTP | Code | Meaning |
|------|------|---------|
| 400 | `BAD_REQUEST` | Invalid request |
| 401 | `AUTHENTICATION_ERROR` | Auth failed |
| 401 | `TOKEN_ERROR` | Invalid/expired token |
| 403 | `AUTHORIZATION_ERROR` | Permission denied |
| 404 | `NOT_FOUND_ERROR` | Resource not found |
| 409 | `CONFLICT_ERROR` | Duplicate resource |
| 422 | `VALIDATION_ERROR` | Invalid input |
| 429 | `RATE_LIMIT_ERROR` | Too many requests |
| 500 | `INTERNAL_ERROR` | Server error |

### SSO Error Codes

| Code | Meaning |
|------|---------|
| `SSO_AUTHENTICATION_ERROR` | SAML authentication failed |
| `SSO_VALIDATION_ERROR` | Configuration invalid |
| `SSO_CONFIGURATION_ERROR` | Provider setup incomplete |
| `SSO_METADATA_ERROR` | SAML metadata parsing failed |
| `SSO_CERTIFICATE_ERROR` | Certificate validation failed |
| `SSO_PROVISIONING_ERROR` | User provisioning failed |

---

## Production Operations

### Secrets Rotation Registry

`infra/secrets/SECRETS_REGISTRY.yaml` is the declarative source of truth
for rotation cadences (quarterly / semi_annual / annual / on_demand)
across Janua, Enclii, and the broader MADFAM ecosystem. Added
2026-04-17: an `ecosystem` section covering ENCLII_API_TOKEN, probe
tokens, webhook secrets, AI-provider keys, Karafiel compliance creds,
Dhanam provider creds (Belvo/Plaid/Bitso), and Colyseus.

Enforcement ships as a K8s CronJob at
`infra/k8s/production/rotation-monitor-cronjob.yaml`. Daily at 13:05 UTC
it runs `scripts/secrets/rotation_monitor.py` which classifies every
registry entry as `overdue | reminder | bootstrap | ok | skipped` and
exits non-zero on any actionable finding so Alertmanager pages
on-call. Slack webhook URL is mounted from the optional
`rotation-monitor-secrets` Secret.

13 pytest cases at `scripts/secrets/test_rotation_monitor.py` cover all
severity branches and malformed-entry resilience. Run:

```bash
python3 scripts/secrets/rotation_monitor.py                 # markdown, exit 0/1
python3 scripts/secrets/rotation_monitor.py --json          # JSON output
python3 -m pytest scripts/secrets/test_rotation_monitor.py  # test suite
```

### Kubernetes Commands

```bash
# Set namespace
export NS=janua

# Check pod status
kubectl get pods -n $NS

# View logs
kubectl logs -n $NS -l app=janua-api -f

# Check events
kubectl get events -n $NS --sort-by='.lastTimestamp'

# Restart deployment
kubectl rollout restart deployment/janua-api -n $NS

# Scale deployment
kubectl scale deployment/janua-api -n $NS --replicas=3

# Port forward for debugging
kubectl port-forward svc/janua-api -n $NS 4100:4100
```

### Health Checks

```bash
# Local
curl http://localhost:4100/health

# Production (via tunnel)
curl https://api.janua.dev/health

# Check all dependencies
curl https://api.janua.dev/health/detailed | jq
```

### Backup Procedures

```bash
# Database backup
pg_dump $DATABASE_URL > janua_backup_$(date +%Y%m%d).sql

# Redis backup
redis-cli BGSAVE

# Verify backup
pg_restore --list janua_backup_*.sql | head
```

---

## Related Resources

- **Token verification contract** (for every service that verifies a Janua
  token): [`docs/reference/ISSUER_AND_JWKS.md`](docs/reference/ISSUER_AND_JWKS.md):
  issuer selection, JWKS URL, `kid` rotation (hard cut, one key published),
  `aud` per token type. Service clients: [`docs/service-tokens.md`](docs/service-tokens.md).
- **Data-subject request exports**: only the subject or a platform admin
  (`users.is_admin`) can read `GET /api/v1/compliance/data-subject-request/{id}/data`;
  everyone else gets the unknown-id 404. Read-only audit:
  `apps/api/scripts/audit_data_subject_request_access.py`. Runbook:
  [`docs/runbooks/data-subject-request-access-audit.md`](docs/runbooks/data-subject-request-access-audit.md).
- **Dependency pins**: `sqlalchemy>=2.0.36,<2.1` in `apps/api/requirements.in`
  (2.1 makes psycopg v3 the default driver; janua ships psycopg2).
  `apps/api/requirements.txt` is the hashed lock compiled from it with
  `uv pip compile`; images and CI install it with `--require-hashes`. Bump
  steps are in the `requirements.in` header. GitHub-hosted
  jobs run on `ubuntu-24.04` (#675), not `ubuntu-latest`.
- **Website**: [janua.dev](https://janua.dev)
- **GitHub**: [madfam-org/janua](https://github.com/madfam-org/janua)
- **Documentation**: `docs/` (204 markdown files)
- **Troubleshooting**: `docs/guides/TROUBLESHOOTING_GUIDE.md`
- **Security**: `docs/guides/SECURITY_CHECKLIST.md`
- **Performance**: `docs/guides/PERFORMANCE_TUNING_GUIDE.md`
- **License**: AGPL v3

## LLM-Friendly Documentation (llmstxt.org)

This project provides standardized AI-readable documentation:

| File | URL | Purpose |
|------|-----|---------|
| [llms.txt](llms.txt) | janua.dev/llms.txt | Concise navigation index |
| [llms-full.txt](llms-full.txt) | janua.dev/llms-full.txt | Comprehensive technical reference |

These follow the [llmstxt.org](https://llmstxt.org/) specification for LLM-friendly content.

---

## Agent Session Protocol (Level 5 Autonomy)

This section defines the operating protocol for AI agents (Claude Code, GitHub Copilot, etc.) when working in this repository.

### Session Start
1. **READ AI_CONTEXT.md** in the repository root for critical paths and directives
2. Run `git status && git branch` to verify clean state and current branch
3. Check for existing TodoWrite items from previous sessions
4. Load any Serena memories: `list_memories()` → `read_memory()`

### During Session
1. **ALWAYS use feature branches** - never commit directly to main/master
2. **ALWAYS run validation** before commits:
   - Python: `pytest` in apps/api
   - TypeScript: `pnpm typecheck && pnpm lint`
3. **UPDATE TodoWrite** after completing each task
4. **CHECKPOINT every 30 minutes** via `write_memory()` for session persistence

### Secret Management Protocols (Safe-Patch Mode)
**High-Value Targets**: You are PERMITTED to edit `.env` and `.env.local` files, but MUST adhere to:

1. **Backup First**: Before ANY modification to a secret file:
   ```bash
   cp .env .env.bak  # Create immediate restore point
   ```

2. **Patch, Don't Purge**: NEVER overwrite with `> .env` (deletes existing keys). ALWAYS use:
   ```bash
   sed -i '' 's/OLD_VALUE/NEW_VALUE/' .env  # Modify specific key
   echo "NEW_KEY=value" >> .env             # Append new key
   ```

3. **Placeholder Ban**: FORBIDDEN from writing values containing:
   - `your_key_here`, `placeholder`, `example`, `xxx`, `TODO`
   into active config files (`.env`, `.env.local`)

### Session End
1. Verify all TodoWrite items completed or documented
2. Run final validation: `pnpm typecheck && pnpm lint`
3. Save session state: `write_memory("session_summary", outcomes)`
4. **DO NOT leave uncommitted changes** without explicit user approval

### Validation Requirements
| Change Type | Required Validation |
|-------------|---------------------|
| Python API | `cd apps/api && pytest` |
| TypeScript | `pnpm typecheck && pnpm lint` |
| Database migration | `alembic upgrade head` (local) |
| Configuration | Manual review required |

---

*Janua - The Gatekeeper | One key for the whole city*

## Known gaps / Backlog

The one pending-work list for this repo (reviewed 2026-10-02 against `main`
after #681). Remove an item in the PR that finishes it. **Kind** is
*engineering* or *owner decision*. Security hardening that is not yet shipped
is tracked privately and appears here only by name.

| Item | Why it matters | Priority | Kind | Link |
|---|---|---|---|---|
| Native isolated staging activation and runtime promotion proof | Source API/Redis are held at zero replicas; source authority, inert materialization, dedicated DB/custody/migration, synthetic auth and continuous healthy runtime soak still require typed Enclii adapters and native receipts. Promotion is held until those exist | P1 | engineering | [activation contract](docs/runbooks/isolated-staging-activation.md) |
| Reserved OAuth boundaries hotfix image (owner exception, optional) | If the owner chooses it, ships the merged reservation registry before isolated staging is proven: production's image plus two reviewed files, verified strictly tighter. Delete the folder, workflow and test in the first normal promotion | P1 | owner decision | [`deployment/hotfix/janua-api-reserved-boundaries`](deployment/hotfix/janua-api-reserved-boundaries/README.md) |
| Merge and deploy window for the audit-log fixes | Both PRs change the audit logger and add migration `020`; they need a scheduled window and a migration run | P1 | owner decision | #673, #676 |
| Next.js hardening checklist for `apps/admin`, `apps/dashboard`, `apps/docs` and `apps/website` (tracked privately) | Brings the four Next apps to the posture the rest of the estate already ships | P1 | engineering | — |
| Un-skip the 11 core auth e2e tests in `tests/e2e/auth-flows.spec.ts` and run them in CI | Every `test.skip` there (invalid credentials, password reset, MFA enrollment, session persistence, logout, protected-route redirect, Google OAuth, lockout, email verification, concurrent sessions, inactivity timeout) leaves a core auth journey without an E2E proof | P1 | engineering | — |
| Alert on the readiness body for Redis and the database | Since #696/#697 `GET /api/v1/health/ready` answers 200 with `"status": "degraded"` during a Redis or database outage, so a probe or alert that reads only the HTTP status no longer sees either outage. Monitoring must alert on a non-empty `degraded` and on `redis_circuit.strict_failures` rising | P1 | engineering | [`docs/runbooks/oauth-shared-state-redis.md`](docs/runbooks/oauth-shared-state-redis.md#follow-up-alerting-on-the-readiness-body) |
| Access-token revocation on `get_current_user` routes | Most routes do not read the revocation list, so a signed-out session's access token works until it expires (default `JWT_ACCESS_TOKEN_EXPIRE_MINUTES` 480). Checking it there makes every route depend on Redis | P2 | owner decision | [runbook → Open items](docs/runbooks/oauth-shared-state-redis.md#open-items) |
| Auth and OAuth follow-ups after #694–#698 | OIDC `refresh_token` grant has no rotation or reuse detection; grant access tokens carry no family; bulk admin revocations bypass `AuthService.revoke_sessions`; password-reset race; the code grant's `expires_in` (3600) differs from the token's `exp`; SDK surfaces that call routes that do not exist (`client.sessions.revokeAllSessions` / `getCurrentSession` / `refresh`, two-argument `client.users.revokeSession`, react-native passkeys, `@janua/ui` `PasskeyButton`); the dashboard Sessions page reads `.items` from a `{sessions, total}` response and its revoke-all button uses the missing route; react-sdk JWT decoding without base64url; passkey verify errors echo library text | P2 | engineering | [runbook → Open items](docs/runbooks/oauth-shared-state-redis.md#open-items) |
| Skip-inventory sweep | About 274 `pytest.skip` / `skip` / `xfail` markers under `apps/api/tests` (CI's full API selection reports 99 skipped) and 43 `.skip` calls in JS/TS tests. Largest groups: import-guarded skips (`Model/Config imports failed`), «httpx async await issue in billing service» (16), PostgreSQL-only tests that skip without a database (they run in the PostgreSQL CI job), and rate-limit tests mocked in the test environment. Fix or justify each | P2 | engineering | — |
| Move the four app Dockerfiles to pnpm 10 with `--frozen-lockfile` | Images install with pnpm 9.15 and `--no-frozen-lockfile`, so they can resolve versions the lockfile does not pin. See «Image installs use pnpm 9» under Deployment Pipeline | P2 | engineering | — |
| Remove the legacy email-verification token fallback in `apps/api/app/services/email_service.py` | The JSON format shipped in April 2026; the fallback for the older format was meant to last one 24 h token TTL | P3 | engineering | — |
| Publish `@janua/ui` (and the `@janua/nextjs` that pulls it) with a React 19 peer range | `@janua/nextjs` 0.2.0 pulls `@janua/ui` 0.1.4, which declares `react ^18` as a peer (and `lucide-react` 0.303). Consumer apps that moved to Next 15 / React 19 report an unmet peer; builds and runtime are clean, but the range is wrong until a React 19 line ships | P3 | engineering | — |

Done and removed from this list: the edge-verify CORS allowlist (2026-04-23),
`ENABLE_DOCS=false` in the base API deployment (dev and staging overlays turn
it on), JSON parsing for email-verification tokens.

<!-- END LEGACY_CLAUDE_IMPORT -->