# 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. 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 # 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/ pnpm install pnpm dev # Build for production pnpm build ``` ### SDK Development ```bash cd packages/ 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_` (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_ (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/.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 `-v` 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.