Reviewing PRs (dograh)
This skill is for reviewing any PR, including PRs written by maintainers. Focus on Dograh-specific regression risks. Skip generic lint, formatting, and type-check comments unless they connect to one of the repo-specific issues below.
The main failure modes in this repo are:
- Missing org scoping on request-reachable reads or writes
- Authless routes or websockets
- Trusting unsigned webhook fields
- SQL written outside
api/db/*_client.py
- Per-worker cache state updated without worker sync
- UI calls that bypass the generated SDK
- Migrations that are not safe on existing production data
How to drive the review
- Get the diff:
- GitHub PR:
gh pr diff <N> or gh pr view <N> --json files,additions,deletions
- Local branch:
git diff origin/main...HEAD
- Bucket changed files into the sections below.
- Run only the sections relevant to the changed files.
- Report findings as
<file>:<line> -> <problem> -> <correct pattern>.
Freshness rule
Treat this file as review policy and navigation, not as a frozen inventory.
- If the current repo conflicts with this skill, trust the repo and mention the drift.
- Do not rely on static allowlists or exact line numbers from this file.
- Review against the code in the PR and the current local repo, not against old prose.
File to section map
| Path pattern in diff |
Sections to run |
api/routes/*.py |
1, 2, 8 |
api/db/*_client.py, api/db/models.py |
2, 3 |
api/services/**/*.py |
2, 3, 4 |
api/tasks/*.py |
2, 3, 5 |
api/alembic/versions/*.py |
6 |
api/mcp_server/**, api/services/workflow/mcp_*.py |
1, 2, 7 |
ui/** |
9 |
api/constants.py, anything os.getenv |
10 |
api/tests/** |
11 |
api/schemas/*.py |
12 |
1. Route authentication (api/routes/*.py)
There is no global auth middleware. Each route declares its own auth behavior. Forgetting one creates a silently public endpoint.
Common auth deps from api.services.auth.depends:
get_user
get_user_ws
get_superuser
Checks:
- A new
@router.<verb>(...) handler with no auth dependency is public. Treat that as a finding unless the current file already establishes a deliberate public auth pattern such as public token auth, signed webhook auth, or an equivalent websocket token flow.
get_user on an impersonation, cross-org, or global reporting endpoint should usually be get_superuser.
- A route that reimplements bearer or API-key parsing instead of using the shared auth dependency is a finding.
- A websocket handler without
Depends(get_user_ws) and without a clear public token path is a finding.
- A PR tightening
CORSMiddleware to a fixed origin list needs strong justification. Dograh relies on cross-origin embedding; endpoint auth is the real control.
Useful commands:
rg -n "Depends\\((get_user|get_user_ws|get_superuser)\\)" api/routes
rg -n "@router\\.(get|post|put|delete|patch|websocket)" api/routes
2. Organization scoping (the cross-tenant rule)
This is the highest priority rule in the repo. Every request-reachable read or write of an org-scoped resource must filter or validate by organization_id.
Use api/AGENTS.md as the canonical summary.
Determine scope from the current code:
- Direct scope: the model has
organization_id
- Indirect scope: the model reaches an org through a parent FK or relationship
- Legacy spelling may exist in old migrations or old code, but new runtime code should use
organization_id
Checks:
- Any
*_by_id(...) call in a route handler is suspicious. If request-reachable and unscoped, it is usually a finding.
- New
list_* or get_* endpoints must filter in SQL, not in Python after .all().
- If a request writes an FK to another org-scoped resource, the route must first fetch that target row with
user.selected_organization_id and reject if it does not belong to the org.
- Services called from routes must preserve scoping. If a service method or DB client call drops
organization_id, trace the caller.
- Background tasks do not get org context for free. They must reload the parent row and derive org from there.
- Webhooks must derive org from a signed or otherwise authenticated identifier, not from caller-supplied body fields like
organization_id.
- New runtime code should use canonical
organization_id, not org_id, tenant_id, or organisation_id.
Useful commands:
rg -n "_by_id\\(" api/routes api/services api/tasks
rg -n "db_client\\.get_\\w+\\(" api/routes api/services api/tasks
rg -n "organization_id|selected_organization_id" api/routes api/services api/tasks api/db
3. DB query layering (api/db/ is the only home for SQL)
Production SQL belongs in api/db/*_client.py. Routes, services, and tasks should call DB client methods, not write SQLAlchemy directly.
Checks:
select, update, delete, insert, AsyncSession, sessionmaker, or async_session in api/routes/, api/services/, or api/tasks/ is a finding.
api/services/admin_utils/ is the exception. It is not a template for production code.
- Session lifecycle belongs inside the DB client.
- New DB client parameters should use canonical
organization_id.
Useful commands:
rg -n "(from sqlalchemy|AsyncSession|sessionmaker|async_session)" api/routes api/services api/tasks
4. Worker sync - multi-process state coherence (api/services/worker_sync/)
Production runs multiple workers. Per-process mutable caches become stale unless updates are broadcast.
Use api/AGENTS.md as the canonical summary.
Checks:
- A new module-level or class-level mutable cache written by an endpoint needs a
WorkerSyncManager broadcast path.
- Local invalidation alone is not enough if other workers can still serve stale state.
- If a PR introduces a new cached object, the diff should usually contain all three:
- the broadcast call
- the event type or equivalent signal definition
- the handler registration that reloads fresh state
5. Background tasks (api/tasks/, ARQ)
Checks:
- User-triggered enqueue paths must validate org ownership before enqueue.
- Tasks that accept IDs and reload rows must derive org from those rows, not assume shared context.
- Tasks must be idempotent or explicitly retry-safe.
- Only real task entrypoints belong in
api/tasks/arq.py::WorkerSettings.functions.
- Secret logging rules from section 10 apply here too.
6. Migrations (api/alembic/versions/)
Checks:
upgrade() and downgrade() should both exist and be meaningfully reversible unless the change truly cannot be reversed.
- Adding a
NOT NULL column to a populated table needs a safe default or a backfill before the constraint.
- Tightening nullable to non-nullable needs the backfill before
alter_column(..., nullable=False).
- New JSON columns should match the table's existing JSON or JSONB conventions.
- Large backfills in a migration should be questioned; they often belong out-of-band.
- Indexes on large tables need concurrent-safe handling.
- Do not turn historical migration naming into a finding by itself. Review the migration being changed, not old untouched migration prose.
7. MCP server (api/mcp_server/)
Checks:
- New tools should use
authenticate_mcp_request(), not reimplement API-key validation.
- New tool DB lookups should preserve org scoping just like REST routes.
- Tools that call external URLs must validate those URLs and consider SSRF.
8. Telephony and webhook handlers
Checks:
- New provider webhook flows should implement
verify_inbound_signature() or the provider equivalent.
- Minimal pre-verification work may be required to identify the candidate config, but the route should not do unrelated workflow, user, or stateful work before verification.
- Org derivation should come from provider identifiers that are validated by the webhook auth flow, then the derived org/config should drive downstream lookups.
- A webhook must not trust raw body
organization_id.
- If a webhook references a phone number, validate that the number exists for the derived org.
9. UI (ui/) - generated SDK only
Use ui/AGENTS.md as the canonical summary.
The frontend should talk to the backend through ui/src/client/. Raw fetch to internal /api/v1/ routes is suspicious by default.
Checks:
fetch('/api/v1/...') or fetch(\${backendUrl}/api/v1/...`)` in app code is usually a finding unless the current code proves a narrow exception.
- Hardcoded backend URLs are a finding.
- Manual
Authorization header construction in regular components is a finding; auth should be injected centrally.
- SDK calls fired before auth state is ready are a finding.
- Local interfaces that duplicate generated types are a finding.
- If backend API shape changed and the UI consumes it,
ui/src/client/ should usually change too.
Useful commands:
rg -n "fetch\\(['\"\\`].*api/v1" ui/src
rg -n "Authorization" ui/src
10. Logging, secrets, constants
Checks:
- New code should use Loguru, not stdlib
logging.
- New
os.getenv(...) outside api/constants.py is a finding.
- Do not log API keys, bearer tokens, credentials, full webhook bodies, or PII.
Common offender shapes:
logger.info(f"config: {config}")
logger.debug(request_body)
- Logging raw config or user configuration rows
11. Tests (api/tests/)
Checks:
- Async waits in tests should use
asyncio.wait_for(...) or another bounded timeout pattern.
- Tests should run against
.env.test, not .env.
- Integration tests should not be neutered by replacing real DB behavior with mocks just to make the test pass.
- Tests that depend on mutable shared DB state across test cases are suspicious.
12. Schemas (api/schemas/)
Checks:
- New response schemas should not expose internal FKs or IDs unless the caller genuinely needs them.
- Request schemas that accept org-scoped FK values are a trigger to inspect the corresponding route for section 2 ownership validation.
Final pass: shape the report
Present findings in three buckets:
Blocker
- Missing org scope on a request-reachable lookup
- Route added without auth and without a proven deliberate public auth mechanism
- Webhook without signature verification, or significant unrelated work done before verification
- Migration without safe backfill or without a meaningful downgrade
- UI bypasses generated SDK for internal API calls
- Secrets logged
Should-fix
- Cached state mutated without worker sync
- JSON vs JSONB inconsistency
- Response schema leaks internal identifiers
- Backend API changed without client regen where UI consumes it
- Test path can hang indefinitely
Nit
- Naming inconsistencies
- Minor convention drift
- Low-risk schema or report-shape cleanup
Cite file:line for each finding. Skip anything a formatter, linter, or IDE would already catch unless it connects to one of the repo-specific risks above.
1---2name: review-pr3description: Review a Dograh pull request, branch diff, or pasted patch for repo-specific security and correctness risks that are not obvious from generic FastAPI, Next.js, or Python conventions. Use when the user asks to review a PR, audit a diff, check whether changes are safe to merge, review their own changes, or asks what to look for in a Dograh PR. Focus on tenant isolation, route auth, webhook signing and org derivation, DB layering, worker-sync, migrations, generated SDK usage, and test hazards.4---56# Reviewing PRs (dograh)78This skill is for reviewing any PR, including PRs written by maintainers. Focus on Dograh-specific regression risks. Skip generic lint, formatting, and type-check comments unless they connect to one of the repo-specific issues below.910The main failure modes in this repo are:1112- Missing org scoping on request-reachable reads or writes13- Authless routes or websockets14- Trusting unsigned webhook fields15- SQL written outside `api/db/*_client.py`16- Per-worker cache state updated without worker sync17- UI calls that bypass the generated SDK18- Migrations that are not safe on existing production data1920## How to drive the review21221. Get the diff:23 - GitHub PR: `gh pr diff <N>` or `gh pr view <N> --json files,additions,deletions`24 - Local branch: `git diff origin/main...HEAD`252. Bucket changed files into the sections below.263. Run only the sections relevant to the changed files.274. Report findings as `<file>:<line> -> <problem> -> <correct pattern>`.2829## Freshness rule3031Treat this file as review policy and navigation, not as a frozen inventory.3233- If the current repo conflicts with this skill, trust the repo and mention the drift.34- Do not rely on static allowlists or exact line numbers from this file.35- Review against the code in the PR and the current local repo, not against old prose.3637### File to section map3839| Path pattern in diff | Sections to run |40| ----------------------------------------------------- | --------------- |41| `api/routes/*.py` | 1, 2, 8 |42| `api/db/*_client.py`, `api/db/models.py` | 2, 3 |43| `api/services/**/*.py` | 2, 3, 4 |44| `api/tasks/*.py` | 2, 3, 5 |45| `api/alembic/versions/*.py` | 6 |46| `api/mcp_server/**`, `api/services/workflow/mcp_*.py` | 1, 2, 7 |47| `ui/**` | 9 |48| `api/constants.py`, anything `os.getenv` | 10 |49| `api/tests/**` | 11 |50| `api/schemas/*.py` | 12 |5152---5354## 1. Route authentication (`api/routes/*.py`)5556There is no global auth middleware. Each route declares its own auth behavior. Forgetting one creates a silently public endpoint.5758Common auth deps from `api.services.auth.depends`:5960- `get_user`61- `get_user_ws`62- `get_superuser`6364Checks:6566- A new `@router.<verb>(...)` handler with no auth dependency is public. Treat that as a finding unless the current file already establishes a deliberate public auth pattern such as public token auth, signed webhook auth, or an equivalent websocket token flow.67- `get_user` on an impersonation, cross-org, or global reporting endpoint should usually be `get_superuser`.68- A route that reimplements bearer or API-key parsing instead of using the shared auth dependency is a finding.69- A websocket handler without `Depends(get_user_ws)` and without a clear public token path is a finding.70- A PR tightening `CORSMiddleware` to a fixed origin list needs strong justification. Dograh relies on cross-origin embedding; endpoint auth is the real control.7172Useful commands:7374```bash75rg -n "Depends\\((get_user|get_user_ws|get_superuser)\\)" api/routes76rg -n "@router\\.(get|post|put|delete|patch|websocket)" api/routes77```7879---8081## 2. Organization scoping (the cross-tenant rule)8283This is the highest priority rule in the repo. Every request-reachable read or write of an org-scoped resource must filter or validate by `organization_id`.8485Use `api/AGENTS.md` as the canonical summary.8687Determine scope from the current code:8889- Direct scope: the model has `organization_id`90- Indirect scope: the model reaches an org through a parent FK or relationship91- Legacy spelling may exist in old migrations or old code, but new runtime code should use `organization_id`9293Checks:9495- Any `*_by_id(...)` call in a route handler is suspicious. If request-reachable and unscoped, it is usually a finding.96- New `list_*` or `get_*` endpoints must filter in SQL, not in Python after `.all()`.97- If a request writes an FK to another org-scoped resource, the route must first fetch that target row with `user.selected_organization_id` and reject if it does not belong to the org.98- Services called from routes must preserve scoping. If a service method or DB client call drops `organization_id`, trace the caller.99- Background tasks do not get org context for free. They must reload the parent row and derive org from there.100- Webhooks must derive org from a signed or otherwise authenticated identifier, not from caller-supplied body fields like `organization_id`.101- New runtime code should use canonical `organization_id`, not `org_id`, `tenant_id`, or `organisation_id`.102103Useful commands:104105```bash106rg -n "_by_id\\(" api/routes api/services api/tasks107rg -n "db_client\\.get_\\w+\\(" api/routes api/services api/tasks108rg -n "organization_id|selected_organization_id" api/routes api/services api/tasks api/db109```110111---112113## 3. DB query layering (`api/db/` is the only home for SQL)114115Production SQL belongs in `api/db/*_client.py`. Routes, services, and tasks should call DB client methods, not write SQLAlchemy directly.116117Checks:118119- `select`, `update`, `delete`, `insert`, `AsyncSession`, `sessionmaker`, or `async_session` in `api/routes/`, `api/services/`, or `api/tasks/` is a finding.120- `api/services/admin_utils/` is the exception. It is not a template for production code.121- Session lifecycle belongs inside the DB client.122- New DB client parameters should use canonical `organization_id`.123124Useful commands:125126```bash127rg -n "(from sqlalchemy|AsyncSession|sessionmaker|async_session)" api/routes api/services api/tasks128```129130---131132## 4. Worker sync - multi-process state coherence (`api/services/worker_sync/`)133134Production runs multiple workers. Per-process mutable caches become stale unless updates are broadcast.135136Use `api/AGENTS.md` as the canonical summary.137138Checks:139140- A new module-level or class-level mutable cache written by an endpoint needs a `WorkerSyncManager` broadcast path.141- Local invalidation alone is not enough if other workers can still serve stale state.142- If a PR introduces a new cached object, the diff should usually contain all three:143 - the broadcast call144 - the event type or equivalent signal definition145 - the handler registration that reloads fresh state146147---148149## 5. Background tasks (`api/tasks/`, ARQ)150151Checks:152153- User-triggered enqueue paths must validate org ownership before enqueue.154- Tasks that accept IDs and reload rows must derive org from those rows, not assume shared context.155- Tasks must be idempotent or explicitly retry-safe.156- Only real task entrypoints belong in `api/tasks/arq.py::WorkerSettings.functions`.157- Secret logging rules from section 10 apply here too.158159---160161## 6. Migrations (`api/alembic/versions/`)162163Checks:164165- `upgrade()` and `downgrade()` should both exist and be meaningfully reversible unless the change truly cannot be reversed.166- Adding a `NOT NULL` column to a populated table needs a safe default or a backfill before the constraint.167- Tightening nullable to non-nullable needs the backfill before `alter_column(..., nullable=False)`.168- New JSON columns should match the table's existing JSON or JSONB conventions.169- Large backfills in a migration should be questioned; they often belong out-of-band.170- Indexes on large tables need concurrent-safe handling.171- Do not turn historical migration naming into a finding by itself. Review the migration being changed, not old untouched migration prose.172173---174175## 7. MCP server (`api/mcp_server/`)176177Checks:178179- New tools should use `authenticate_mcp_request()`, not reimplement API-key validation.180- New tool DB lookups should preserve org scoping just like REST routes.181- Tools that call external URLs must validate those URLs and consider SSRF.182183---184185## 8. Telephony and webhook handlers186187Checks:188189- New provider webhook flows should implement `verify_inbound_signature()` or the provider equivalent.190- Minimal pre-verification work may be required to identify the candidate config, but the route should not do unrelated workflow, user, or stateful work before verification.191- Org derivation should come from provider identifiers that are validated by the webhook auth flow, then the derived org/config should drive downstream lookups.192- A webhook must not trust raw body `organization_id`.193- If a webhook references a phone number, validate that the number exists for the derived org.194195---196197## 9. UI (`ui/`) - generated SDK only198199Use `ui/AGENTS.md` as the canonical summary.200201The frontend should talk to the backend through `ui/src/client/`. Raw `fetch` to internal `/api/v1/` routes is suspicious by default.202203Checks:204205- `fetch('/api/v1/...')` or `fetch(\`${backendUrl}/api/v1/...\`)` in app code is usually a finding unless the current code proves a narrow exception.206- Hardcoded backend URLs are a finding.207- Manual `Authorization` header construction in regular components is a finding; auth should be injected centrally.208- SDK calls fired before auth state is ready are a finding.209- Local interfaces that duplicate generated types are a finding.210- If backend API shape changed and the UI consumes it, `ui/src/client/` should usually change too.211212Useful commands:213214```bash215rg -n "fetch\\(['\"\\`].*api/v1" ui/src216rg -n "Authorization" ui/src217```218219---220221## 10. Logging, secrets, constants222223Checks:224225- New code should use Loguru, not stdlib `logging`.226- New `os.getenv(...)` outside `api/constants.py` is a finding.227- Do not log API keys, bearer tokens, credentials, full webhook bodies, or PII.228229Common offender shapes:230231- `logger.info(f"config: {config}")`232- `logger.debug(request_body)`233- Logging raw config or user configuration rows234235---236237## 11. Tests (`api/tests/`)238239Checks:240241- Async waits in tests should use `asyncio.wait_for(...)` or another bounded timeout pattern.242- Tests should run against `.env.test`, not `.env`.243- Integration tests should not be neutered by replacing real DB behavior with mocks just to make the test pass.244- Tests that depend on mutable shared DB state across test cases are suspicious.245246---247248## 12. Schemas (`api/schemas/`)249250Checks:251252- New response schemas should not expose internal FKs or IDs unless the caller genuinely needs them.253- Request schemas that accept org-scoped FK values are a trigger to inspect the corresponding route for section 2 ownership validation.254255---256257## Final pass: shape the report258259Present findings in three buckets:260261- **Blocker**262 - Missing org scope on a request-reachable lookup263 - Route added without auth and without a proven deliberate public auth mechanism264 - Webhook without signature verification, or significant unrelated work done before verification265 - Migration without safe backfill or without a meaningful downgrade266 - UI bypasses generated SDK for internal API calls267 - Secrets logged268269- **Should-fix**270 - Cached state mutated without worker sync271 - JSON vs JSONB inconsistency272 - Response schema leaks internal identifiers273 - Backend API changed without client regen where UI consumes it274 - Test path can hang indefinitely275276- **Nit**277 - Naming inconsistencies278 - Minor convention drift279 - Low-risk schema or report-shape cleanup280281Cite `file:line` for each finding. Skip anything a formatter, linter, or IDE would already catch unless it connects to one of the repo-specific risks above.