# The DisExcel Project — Context for Claude Code Backend: FastAPI + SQLAlchemy (async) + Pydantic v2 + PostgreSQL. JWT auth (access + refresh tokens), RBAC permissions, Redis cache, RabbitMQ background workers. Python >=3.13,<4.0, Poetry for dependency management. ## Auth & Permissions - Users have `direct_permissions` (list of `Permissions`) and `group` (list of `PermissionsGroups`, each with its own `permissions`) — many-to-many both ways. - Effective permissions = `direct_permissions ∪ (union of all groups' permissions)`. - `require_permissions(*permissions)` in `src/web/protected_routes` is a FastAPI dependency factory — wraps `CurrentUserService.get_current_user`. Call with no args (`require_permissions()`) for "just authenticated, no specific permission needed". - Access tokens carry a `jti` claim. Logout writes `revoked_access_token:{jti}` to Redis with TTL = remaining token lifetime — `get_current_user` checks this key before anything else. - `secure` flag on refresh_token cookie is driven by `env_settings.PROD_MODE` (bool) — `False` locally/tests so cookies work over plain HTTP, `True` in prod. ## Redis (`src/cache/`) - `RedisClient(redis.Redis)` — module-level shared singleton, subclasses `redis.Redis` directly (inherits all commands, no manual wrapping needed). - Three uses: permissions is-cache was considered and rejected (no real DB savings — `get_user_by_id` already eager-loads everything via `selectin` in one call); rate limiting on login (`RateLimit.rate_limit(ip)` — `INCR` + `EXPIRE` on first attempt, blocks >5/60s); access-token revoke blacklist (see above). - Rate limit is only triggered inside `except HTTPException` on `/protected/token` — i.e. only on failed logins, not successful ones (otherwise legitimate repeated logins would trip it). ## RabbitMQ (`src/messaging/`) - `RabbitMQClient` — shared class, lazy `connect()` (can't be async `__init__`), holds one `connection` + one `channel`, `get_channel()` ensures setup. - Topic exchange named `"email"`. Each message type gets its own routing key (`email.welcome`, `email.reset`) and its own durable queue (`queue_welcome_email`, `queue_reset_email`), each queue explicitly bound to the exchange with its own key. - **Important**: both producer and consumer must declare/bind the queues — if only the consumer does it and the consumer has never run, `publish` on a not-yet-existing queue silently loses the message. `EmailProducer.setup()` also declares+binds both queues defensively. - `message.process()` async context manager = manual ack (auto-ack on success, requeue/nack on exception) — this is the right choice for email, not `no_ack=True`. - Email templates: Jinja2, inline CSS (email clients don't support `