Skip to content

Feature-Flag: org/project feature flags with DB#789

Open
vprashrex wants to merge 75 commits intomainfrom
feature/feature-flag
Open

Feature-Flag: org/project feature flags with DB#789
vprashrex wants to merge 75 commits intomainfrom
feature/feature-flag

Conversation

@vprashrex
Copy link
Copy Markdown
Collaborator

@vprashrex vprashrex commented Apr 27, 2026

Summary

Target issue is #790
Explain the motivation for making this change. What existing problem does the pull request solve?

This PR introduces a persistent feature-flag system scoped by organization_id and project_id and applies it to control access to Feature flagged APIs.

Previously, Assessment functionality was not centrally gated per tenant/project, so rollout control was limited and risky. This change solves that by adding:

  1. A feature_flag model + migration + CRUD operations.
  2. Admin APIs to create/update/delete/list flags (/feature-flags) and a resolved flags endpoint (/features).
  3. A reusable require_feature(...) permission dependency that can be used at the top route level or at independent endpoints.
  4. Assessment route protection behind FeatureFlag.ASSESSMENT.

Checklist

Before submitting a pull request, please ensure that you mark these task.

  • Ran fastapi run --reload app/main.py or docker compose up in the repository root and test.
  • If you've fixed a bug or added code that is tested and has test cases.

Notes

Please add here if any other information is required for the reviewer.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added feature flag management system enabling administrators to control feature availability per organization and project.
    • Assessment feature can now be gated behind a feature flag.
    • User profile now displays enabled features.
  • Documentation

    • Added API documentation for feature flag creation, retrieval, updates, and deletion.
  • Improvements

    • Improved logging consistency across assessment operations.

- Added `service.py` to handle assessment evaluation orchestration, including starting and retrying assessments.
- Introduced utility functions for exporting assessment results in various formats (CSV, XLSX, JSON) in `export.py`.
- Created parsing utilities for handling batch results in `parsing.py`.
- Implemented validation for dataset file uploads in `validators.py`.
- Updated batch job model to include assessment type.
- Enhanced evaluation run model to reference parent assessments.
- Added new fields for reasoning effort and summary preferences in LLM request model.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (1)
  • ready-for-review

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 627cab27-8f6d-4955-bf6d-13dbc66d7b4e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements a comprehensive feature flag system with database schema, data models, CRUD operations, API routes, and permission gating. The system supports organization- and project-scoped feature flags (starting with ASSESSMENT) persisted in the database, exposed via API endpoints, and integrated into route dependencies and user context.

Changes

Feature Flag System Implementation

Layer / File(s) Summary
Database Schema
backend/app/alembic/versions/057_create_feature_flag_table.py
Creates feature_flag table with composite unique index on (key, organization_id, project_id), foreign key constraints, and triggers to auto-seed the ASSESSMENT flag for new projects.
Data Models & Enum
backend/app/models/feature_flag.py, backend/app/core/feature_flags/constants.py, backend/app/models/__init__.py
Defines FeatureFlag StrEnum (ASSESSMENT), FeatureFlagBase with key/org/project/enabled fields, and CRUD/API models (Create, Update, Delete, Public). Exports all models.
Core CRUD Operations
backend/app/crud/feature_flag.py, backend/app/crud/__init__.py
Implements get, create, update, delete, and list feature flag operations with uniqueness enforcement (409 on duplicate) and project isolation. Adds validate_project_belongs_to_organization helper.
Feature Flag Utilities
backend/app/core/feature_flags/__init__.py
Provides is_enabled and resolve_all_flags functions to check flag status and retrieve all enabled flags for a scope.
Permission System & Route Gating
backend/app/api/permissions.py, backend/app/api/routes/assessment/__init__.py
Upgrades Permission to StrEnum; adds require_feature dependency factory for flag-based route gating; applies ASSESSMENT flag gate to assessment routes.
Feature Flag API Routes
backend/app/api/routes/features.py
Implements SUPERUSER-gated CRUD endpoints: create, list (with optional filters), update, and delete feature flags. All operations validate project ownership and include logging.
User Context Integration
backend/app/api/routes/users.py, backend/app/models/user.py
Adds features: list[str] field to UserPublic; updates /users/me to resolve and return enabled flags for the current user's organization/project scope.
Router Wiring
backend/app/api/main.py
Centralizes FastAPI router registration, adds feature flag routes, and conditionally includes private router in dev/test environments.
Validation & Utils
backend/app/utils.py
Adds require_organization_for_project validation utility to enforce organization presence when project scope is provided.
API Documentation
backend/app/api/docs/features/*, backend/app/api/docs/users/get_me.md
Documents create, list, update, delete flag endpoints (validation, payloads, responses) and updated /users/me behavior.
Tests & Assessment Updates
backend/app/tests/api/routes/test_features.py, backend/app/tests/api/test_permissions.py, backend/app/tests/crud/test_feature_flag.py, backend/app/tests/assessment/test_crud.py, backend/app/crud/assessment/dataset.py, backend/app/crud/assessment/processing.py, backend/app/api/routes/cron.py
Comprehensive feature flag CRUD and API route tests; require_feature dependency tests; assessment dataset langfuse_dataset_id parameter support; logging level adjustments in assessment processing; cron error message hardening.

Sequence Diagram

sequenceDiagram
    participant Client
    participant APIRoute as Feature Flag Route
    participant Auth as Auth Context
    participant CRUD as Feature Flag CRUD
    participant Database as DB

    Client->>APIRoute: POST /features (create flag)
    APIRoute->>Auth: Check SUPERUSER permission
    Auth-->>APIRoute: ✓ Authorized
    APIRoute->>CRUD: create_feature_flag(key, org, project, enabled)
    CRUD->>Database: Query existing flag (uniqueness check)
    alt Flag exists
        Database-->>CRUD: Found
        CRUD-->>APIRoute: HTTPException 409
        APIRoute-->>Client: 409 Conflict
    else Flag not found
        Database-->>CRUD: Not found
        CRUD->>Database: INSERT feature_flag
        Database-->>CRUD: Created record
        CRUD-->>APIRoute: FeatureFlagPublic
        APIRoute-->>Client: 200 OK + flag details
    end

    Note over Client,Database: Later: Check flag during request
    Client->>APIRoute: GET /assessments (gated by ASSESSMENT flag)
    APIRoute->>Auth: Extract org/project from context
    Auth-->>APIRoute: org_id, project_id
    APIRoute->>CRUD: is_enabled(ASSESSMENT, org_id, project_id)
    CRUD->>Database: SELECT enabled FROM feature_flag
    Database-->>CRUD: enabled: true/false
    CRUD-->>APIRoute: boolean result
    alt Flag enabled
        APIRoute->>APIRoute: Process request normally
        APIRoute-->>Client: 200 OK + data
    else Flag disabled
        APIRoute-->>Client: 403 Forbidden
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

The PR spans multiple architectural layers (database schema, models, CRUD, permissions, API routes, integrations) with heterogeneous logic: database migration with triggers, StrEnum migration, permission system changes, route gating dependencies, and API integration. While most individual functions follow standard patterns, the interconnected nature of changes and logic density (uniqueness checks, scoping validation, permission integration) demand careful review across all layers.

Possibly related issues

  • Issue #790: Feature toggle system design — this PR directly implements the database-backed, tenant-scoped feature flag system with models, migration, CRUD, API endpoints, FeatureFlag enum, require_feature dependency, Assessment route gating, and /users/me integration as specified.

Possibly related PRs

  • ProjectTech4DevAI/kaapi-backend#788: Both PRs modify backend/app/api/main.py (router wiring) and backend/app/api/routes/cron.py (error handling in assessment polling), indicating overlapping changes to core routing and assessment infrastructure.

Suggested reviewers

  • Ayush8923
  • AkhileshNegi
  • Prajna1999

🐰 A flag springs forth from the database deep,
With scopes and keys that developers keep,
Assessment now gated, controlled with care,
Features resolved, user context laid bare! ✨🚀

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.81% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feature-Flag: org/project feature flags with DB' directly and concisely summarizes the main objective of the PR: introducing organization/project-scoped feature flags with database persistence.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/feature-flag

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@vprashrex vprashrex changed the base branch from main to feature/assessment April 27, 2026 16:04
…n model

- Introduced AssessmentRun model to replace EvaluationRun for assessment-specific runs.
- Updated CRUD operations to manage Assessment and AssessmentRun entities.
- Refactored processing logic to accommodate new AssessmentRun structure.
- Adjusted logging and error handling to reflect changes in run management.
- Modified routes and services to utilize AssessmentRun for assessment evaluations.
- Enhanced export functionality to support new AssessmentRun model.
- Removed EvaluationRun references where applicable and ensured backward compatibility.
@vprashrex vprashrex changed the title Feature/feature flag Flag: Feature Flag Apr 28, 2026
@vprashrex vprashrex self-assigned this Apr 28, 2026
@vprashrex vprashrex added the enhancement New feature or request label Apr 28, 2026
@vprashrex vprashrex changed the title Flag: Feature Flag Feature-Flags: add org/project-level feature flag APIs and gate assessment routes Apr 28, 2026
@vprashrex vprashrex changed the title Feature-Flags: add org/project-level feature flag APIs and gate assessment routes Feature-Flags: add org/project feature flags and gate assessment routes Apr 28, 2026
@vprashrex vprashrex changed the title Feature-Flags: add org/project feature flags and gate assessment routes feature-flag: org/project feature flags with DB Apr 28, 2026
@vprashrex vprashrex changed the title feature-flag: org/project feature flags with DB Feature-Flag: org/project feature flags with DB Apr 28, 2026
@vprashrex vprashrex linked an issue Apr 28, 2026 that may be closed by this pull request
@vprashrex vprashrex moved this to In Review in Kaapi-dev Apr 28, 2026
@vprashrex vprashrex requested a review from AkhileshNegi May 5, 2026 05:34
Comment thread backend/app/api/routes/features.py Outdated
payload: FeatureFlagCreate,
) -> FeatureFlagPublic:
validate_organization(session=session, org_id=payload.organization_id)
validate_project_belongs_to_organization(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need this check? if it is not present already then add in utils since this is quite generic

Comment thread backend/app/api/routes/features.py Outdated
Comment on lines +83 to +86
if organization_id is not None:
validate_organization(session=session, org_id=organization_id)
if project_id is not None:
assert organization_id is not None
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why assert? and then validate organization also

Comment on lines +84 to +88
op.create_index(
op.f("ix_feature_flag_organization_id"),
"feature_flag",
["organization_id"],
unique=False,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need similar index for project_id

from enum import StrEnum


class FeatureFlag(StrEnum):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we use somthing like FeatureFlagKeyEnum to avoid confusion with FeatureFlag class

vprashrex added 6 commits May 5, 2026 12:55
… safety

- Added HTTPException raises for not found cases in `get_assessment_by_id`, `get_assessment_run_by_id`, and `get_assessment_dataset_by_id` functions to improve error handling.
- Updated return types for several functions to ensure they always return the expected model instead of None.
- Refactored logging statements in assessment evaluation polling to use parameterized logging for better readability and performance.
- Introduced utility functions for attachment resolution, including MIME type detection and URL normalization.
- Improved the handling of assessment run statuses and batch processing, ensuring proper error logging and status updates.
- Enhanced test coverage for CRUD operations and mappers, ensuring that exceptions are raised and handled correctly.
- Updated assessment models to use stricter type definitions for status fields and optional attributes.
@vprashrex vprashrex requested a review from AkhileshNegi May 5, 2026 08:06
Comment thread backend/app/api/main.py
api_router.include_router(assessment_routes.router)
api_router.include_router(assistants.router)
api_router.include_router(collections.router)
api_router.include_router(auth.router)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

api_router.include_router(auth.router) already exist in line:49

can you make all these alphabetical to ensure same is not repeated by other developers

Base automatically changed from feature/assessment to main May 6, 2026 10:00
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (1)
backend/app/crud/feature_flag.py (1)

23-35: 💤 Low value

get_feature_flag_enabled conflates "missing" and "disabled" at call sites — document or simplify.

The signature bool | None is technically accurate (None = row not found, False = row exists but disabled, True = enabled), but every caller that just wants a boolean must remember this trinary. If no caller actually distinguishes the two falsy cases, consider returning bool and defaulting to False when no row exists; otherwise add a docstring spelling out the three states so callers don't accidentally treat None as "enabled" via a misuse of or.

-) -> bool | None:
+) -> bool | None:
+    """Returns True/False if a flag row exists, or None when no row matches."""
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/crud/feature_flag.py` around lines 23 - 35,
get_feature_flag_enabled currently returns bool | None which conflates "row
missing" (None) and "flag disabled" (False); either document the three-state
behavior or—preferably if callers never distinguish—simplify to always return
bool by defaulting a missing row to False. Update the get_feature_flag_enabled
function to clarify semantics (add a docstring stating None means row not found
vs False means explicit disabled) and, if you choose the simpler API, change the
return type to bool and have the query result be interpreted as False when
session.exec(...).first() is None so callers get a plain boolean; reference
FeatureFlagModel.enabled in your change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/app/alembic/versions/057_create_feature_flag_table.py`:
- Line 21: Add explicit return type annotations for the migration entrypoints by
changing the function signatures for upgrade and downgrade to include "-> None";
update the def upgrade() and def downgrade() functions in this migration module
to be def upgrade() -> None: and def downgrade() -> None: so they comply with
the repository's requirement for explicit function return typing.
- Line 12: The migration currently imports a runtime enum FeatureFlag and leaves
upgrade() and downgrade() without return type hints; replace the import of
app.core.feature_flags.constants.FeatureFlag with frozen, literal enum values
(e.g., a hard-coded tuple/list of the exact string names/values used in the SQL
DDL and insert statements) so the migration is self-contained and will not break
if the application enum changes, and add explicit return type annotations to
both upgrade() -> None and downgrade() -> None; update any references in the
migration (e.g., inside CREATE TYPE or INSERT INTO feature_flag, and the symbols
FeatureFlag used at lines around 110 and 128) to use the frozen literals instead
of the runtime enum.

In `@backend/app/api/permissions.py`:
- Around line 87-106: The _check_with_session function conflates missing
org/project context with a disabled feature; change it so that if
auth_context.organization or auth_context.project is missing (org_id or
project_id is None) you route to the appropriate permission requirement (e.g.,
invoke require_permission(REQUIRE_ORGANIZATION/REQUIRE_PROJECT) or raise a
distinct HTTPException message like "Missing organization/project scope")
instead of the generic disabled message, and only call is_enabled(...) and raise
the existing HTTPException("Feature 'X' is not enabled...") when both ids exist
and is_enabled(...) returns false; update the HTTPException branches around
is_enabled and the org/project checks accordingly.
- Around line 74-108: Add explicit return type hints for both require_feature
and its inner function _check_with_session and import Callable from
collections.abc; specifically annotate _check_with_session to return None (it
raises or returns None) and annotate require_feature to return a Callable that
accepts the dependency parameters (AuthContextDep, SessionDep) and returns None
(e.g., Callable[[AuthContextDep, SessionDep], None]) so type checkers know the
factory yields a dependency callable.

In `@backend/app/api/routes/features.py`:
- Around line 30-54: The POST endpoint is failing because the migration 057
already inserts an ASSESSMENT flag per project and create_feature_flag()
currently returns 409 on duplicate, making create_feature_flag_route unusable
for the only defined FeatureFlag; modify create_feature_flag (used by
create_feature_flag_route) to be idempotent: detect existing flag by
(project_id, key) before inserting or catch the unique-constraint/IntegrityError
on insert and return the existing FeatureFlag instead of raising 409. Ensure
this logic references the same unique key used by the DB and keep responses
consistent with APIResponse.success_response for the existing record.

In `@backend/app/crud/feature_flag.py`:
- Around line 46-64: The create_feature_flag flow currently performs a
read-then-write leading to a TOCTOU race; keep the existence check but make the
insert atomic by catching a DB unique-constraint failure: wrap the
session.add/commit/refresh sequence in a try/except that catches
sqlalchemy.exc.IntegrityError, calls session.rollback(), and raises
HTTPException(status_code=409, detail="Feature flag already exists"); reference
the existing create_feature_flag function, the FeatureFlagModel construction,
session.commit(), and get_feature_flag, and add the
sqlalchemy.exc.IntegrityError import if missing.

In `@backend/app/models/feature_flag.py`:
- Around line 27-31: The FeatureFlag model's project_id Field is missing
index=True which causes schema drift versus the migration; update the Field
declaration for project_id in the FeatureFlag class to include index=True (i.e.,
add index=True to the Field(...) args for the project_id attribute) so the ORM
metadata matches the existing migration-created ix_feature_flag_project_id
index.

In `@scripts/python/invoke-cron.py`:
- Around line 39-40: The code currently hard-codes self.interval_minutes = 1;
change it to read the configured cron interval (e.g., an environment/config
value like CRON_INTERVAL_MINUTES or the existing config accessor) and fall back
to the expected default (5) if missing/invalid, then set self.interval_seconds =
int(self.interval_minutes) * 60; ensure you parse/validate the env/config value
to an integer and reference the same config key used elsewhere so the cron
cadence matches the rest of the system (modify the initializer where
self.interval_minutes and self.interval_seconds are set).

---

Nitpick comments:
In `@backend/app/crud/feature_flag.py`:
- Around line 23-35: get_feature_flag_enabled currently returns bool | None
which conflates "row missing" (None) and "flag disabled" (False); either
document the three-state behavior or—preferably if callers never
distinguish—simplify to always return bool by defaulting a missing row to False.
Update the get_feature_flag_enabled function to clarify semantics (add a
docstring stating None means row not found vs False means explicit disabled)
and, if you choose the simpler API, change the return type to bool and have the
query result be interpreted as False when session.exec(...).first() is None so
callers get a plain boolean; reference FeatureFlagModel.enabled in your change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 97d20c4f-f871-4c9a-a8bf-8684af137f16

📥 Commits

Reviewing files that changed from the base of the PR and between 48fdba4 and a77ee2b.

📒 Files selected for processing (27)
  • backend/app/alembic/versions/057_create_feature_flag_table.py
  • backend/app/api/docs/features/create_flag.md
  • backend/app/api/docs/features/delete_flag.md
  • backend/app/api/docs/features/list_flags.md
  • backend/app/api/docs/features/update_flag.md
  • backend/app/api/docs/users/get_me.md
  • backend/app/api/main.py
  • backend/app/api/permissions.py
  • backend/app/api/routes/assessment/__init__.py
  • backend/app/api/routes/features.py
  • backend/app/api/routes/users.py
  • backend/app/core/feature_flags/__init__.py
  • backend/app/core/feature_flags/constants.py
  • backend/app/crud/__init__.py
  • backend/app/crud/assessment/dataset.py
  • backend/app/crud/assessment/processing.py
  • backend/app/crud/feature_flag.py
  • backend/app/crud/project.py
  • backend/app/models/__init__.py
  • backend/app/models/feature_flag.py
  • backend/app/models/user.py
  • backend/app/tests/api/routes/test_features.py
  • backend/app/tests/api/test_permissions.py
  • backend/app/tests/assessment/test_crud.py
  • backend/app/tests/crud/test_feature_flag.py
  • backend/app/utils.py
  • scripts/python/invoke-cron.py

import sqlalchemy as sa
from alembic import op

from app.core.feature_flags.constants import FeatureFlag as FeatureFlagKeyEnum
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n '^from app\.|^import app\.' backend/app/alembic/versions
fd '057_create_feature_flag_table\.py$' backend/app/alembic/versions --exec sed -n '1,140p' {}

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 3960


🏁 Script executed:

#!/bin/bash
# Get complete file with line numbers
fd '057_create_feature_flag_table\.py$' backend/app/alembic/versions --exec wc -l {} \;

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 141


🏁 Script executed:

#!/bin/bash
# Read complete file
fd '057_create_feature_flag_table\.py$' backend/app/alembic/versions --exec cat -n {} \;

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 5604


Add return type hints and freeze enum values in the migration.

This migration imports a runtime application symbol (app.core.feature_flags.constants.FeatureFlag) and uses it in SQL (lines 110, 128). Future changes to the enum's location, values, or definition will break database bootstrapping from this historical revision. Alembic migrations must be self-contained with frozen values.

Additionally, both upgrade() (line 21) and downgrade() (line 148) functions lack return type annotations, violating the type hints requirement.

Suggested fix
-from app.core.feature_flags.constants import FeatureFlag as FeatureFlagKeyEnum

+DEFAULT_FEATURE_FLAG_KEY = "ASSESSMENT"

-def upgrade():
+def upgrade() -> None:

     op.execute(
         sa.text(
             f"""
             INSERT INTO feature_flag
                 (key, organization_id, project_id, enabled, inserted_at, updated_at)
-            SELECT '{FeatureFlagKeyEnum.ASSESSMENT.value}'::featureflagkey,
+            SELECT '{DEFAULT_FEATURE_FLAG_KEY}'::featureflagkey,
                    p.organization_id, p.id, FALSE, NOW(), NOW()
             FROM project p
             """
         )
     )

     op.execute(
         sa.text(
             f"""
             CREATE OR REPLACE FUNCTION seed_default_feature_flag()
             RETURNS trigger
             LANGUAGE plpgsql
             AS $$
             BEGIN
                 INSERT INTO feature_flag
                     (key, organization_id, project_id, enabled, inserted_at, updated_at)
                 VALUES
-                    ('{FeatureFlagKeyEnum.ASSESSMENT.value}'::featureflagkey,
+                    ('{DEFAULT_FEATURE_FLAG_KEY}'::featureflagkey,
                      NEW.organization_id, NEW.id, FALSE, NOW(), NOW());
                 RETURN NEW;
             END;
             $$;
             """
         )
     )

-def downgrade():
+def downgrade() -> None:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/alembic/versions/057_create_feature_flag_table.py` at line 12,
The migration currently imports a runtime enum FeatureFlag and leaves upgrade()
and downgrade() without return type hints; replace the import of
app.core.feature_flags.constants.FeatureFlag with frozen, literal enum values
(e.g., a hard-coded tuple/list of the exact string names/values used in the SQL
DDL and insert statements) so the migration is self-contained and will not break
if the application enum changes, and add explicit return type annotations to
both upgrade() -> None and downgrade() -> None; update any references in the
migration (e.g., inside CREATE TYPE or INSERT INTO feature_flag, and the symbols
FeatureFlag used at lines around 110 and 128) to use the frozen literals instead
of the runtime enum.

depends_on = None


def upgrade():
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add explicit -> None to the migration entrypoints.

These are new Python functions, and the repo guideline requires explicit parameter and return typing.

Suggested fix
-def upgrade():
+def upgrade() -> None:
...
-def downgrade():
+def downgrade() -> None:

As per coding guidelines, "Always add type hints to all function parameters and return values in Python code."

Also applies to: 148-148

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/alembic/versions/057_create_feature_flag_table.py` at line 21,
Add explicit return type annotations for the migration entrypoints by changing
the function signatures for upgrade and downgrade to include "-> None"; update
the def upgrade() and def downgrade() functions in this migration module to be
def upgrade() -> None: and def downgrade() -> None: so they comply with the
repository's requirement for explicit function return typing.

Comment on lines +74 to +108
def require_feature(flag: FeatureFlag):
"""Dependency factory that gates a route behind a feature flag.

Returns 403 when the flag is disabled for the caller's org/project,
so callers can explicitly handle feature access denial.

Usage::

router = APIRouter(
dependencies=[Depends(require_feature(FeatureFlag.ASSESSMENT))]
)
"""

def _check_with_session(auth_context: AuthContextDep, session: SessionDep):
from app.core.feature_flags import is_enabled

org_id = auth_context.organization.id if auth_context.organization else None
project_id = auth_context.project.id if auth_context.project else None

if (
org_id is None
or project_id is None
or not is_enabled(
session=session,
flag=flag.value,
organization_id=org_id,
project_id=project_id,
)
):
raise HTTPException(
status_code=403,
detail=f"Feature '{flag.value}' is not enabled for this org or project.",
)

return _check_with_session
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add return type annotations per coding guidelines.

Both require_feature and the inner _check_with_session are missing return type hints. The inner check returns None and the factory returns the dependency callable.

✏️ Proposed annotations
-def require_feature(flag: FeatureFlag):
+def require_feature(flag: FeatureFlag) -> Callable[[AuthContextDep, SessionDep], None]:
     """Dependency factory that gates a route behind a feature flag.
     ...
     """

-    def _check_with_session(auth_context: AuthContextDep, session: SessionDep):
+    def _check_with_session(auth_context: AuthContextDep, session: SessionDep) -> None:
         from app.core.feature_flags import is_enabled

Add from collections.abc import Callable to imports.

As per coding guidelines: "Always add type hints to all function parameters and return values in Python code".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/api/permissions.py` around lines 74 - 108, Add explicit return
type hints for both require_feature and its inner function _check_with_session
and import Callable from collections.abc; specifically annotate
_check_with_session to return None (it raises or returns None) and annotate
require_feature to return a Callable that accepts the dependency parameters
(AuthContextDep, SessionDep) and returns None (e.g., Callable[[AuthContextDep,
SessionDep], None]) so type checkers know the factory yields a dependency
callable.

Comment on lines +87 to +106
def _check_with_session(auth_context: AuthContextDep, session: SessionDep):
from app.core.feature_flags import is_enabled

org_id = auth_context.organization.id if auth_context.organization else None
project_id = auth_context.project.id if auth_context.project else None

if (
org_id is None
or project_id is None
or not is_enabled(
session=session,
flag=flag.value,
organization_id=org_id,
project_id=project_id,
)
):
raise HTTPException(
status_code=403,
detail=f"Feature '{flag.value}' is not enabled for this org or project.",
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Distinguish "missing context" from "feature disabled" in the 403 response.

When org_id or project_id is None, the handler returns the same "Feature 'X' is not enabled..." message used when the flag is genuinely disabled. This is misleading for callers debugging an auth/context issue (e.g., a token without project scope) — they will think the feature is off when in fact the request lacks the required scope. Consider routing missing org/project through require_permission(REQUIRE_ORGANIZATION/REQUIRE_PROJECT) (or returning a distinct message), and only emitting the "not enabled" response when is_enabled(...) returns falsy.

🔧 Suggested split
-        if (
-            org_id is None
-            or project_id is None
-            or not is_enabled(
-                session=session,
-                flag=flag.value,
-                organization_id=org_id,
-                project_id=project_id,
-            )
-        ):
+        if org_id is None or project_id is None:
+            raise HTTPException(
+                status_code=403,
+                detail="Organization and project context are required to evaluate feature flags.",
+            )
+
+        if not is_enabled(
+            session=session,
+            flag=flag.value,
+            organization_id=org_id,
+            project_id=project_id,
+        ):
             raise HTTPException(
                 status_code=403,
                 detail=f"Feature '{flag.value}' is not enabled for this org or project.",
             )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/api/permissions.py` around lines 87 - 106, The
_check_with_session function conflates missing org/project context with a
disabled feature; change it so that if auth_context.organization or
auth_context.project is missing (org_id or project_id is None) you route to the
appropriate permission requirement (e.g., invoke
require_permission(REQUIRE_ORGANIZATION/REQUIRE_PROJECT) or raise a distinct
HTTPException message like "Missing organization/project scope") instead of the
generic disabled message, and only call is_enabled(...) and raise the existing
HTTPException("Feature 'X' is not enabled...") when both ids exist and
is_enabled(...) returns false; update the HTTPException branches around
is_enabled and the org/project checks accordingly.

Comment on lines +30 to +54
@router.post(
"/feature-flags",
description=load_description("features/create_flag.md"),
response_model=APIResponse[FeatureFlagPublic],
dependencies=[Depends(require_permission(Permission.SUPERUSER))],
)
def create_feature_flag_route(
session: SessionDep,
payload: FeatureFlagCreate,
) -> APIResponse[FeatureFlagPublic]:
project = validate_project(session=session, project_id=payload.project_id)
created = create_feature_flag(
session=session,
key=payload.key,
organization_id=project.organization_id,
project_id=payload.project_id,
enabled=payload.enabled,
)
logger.info(
"[create_feature_flag_route] Created flag=%s enabled=%s project=%s",
payload.key,
payload.enabled,
payload.project_id,
)
return APIResponse.success_response(data=created)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

POST cannot create the only supported flag in normal state.

057_create_feature_flag_table.py backfills ASSESSMENT for every existing project and adds a trigger to insert the same row for every new project, while create_feature_flag() returns 409 when that row already exists. Since app.core.feature_flags.constants.FeatureFlag currently only defines ASSESSMENT, this endpoint is effectively unusable unless the row was deleted first.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/api/routes/features.py` around lines 30 - 54, The POST endpoint
is failing because the migration 057 already inserts an ASSESSMENT flag per
project and create_feature_flag() currently returns 409 on duplicate, making
create_feature_flag_route unusable for the only defined FeatureFlag; modify
create_feature_flag (used by create_feature_flag_route) to be idempotent: detect
existing flag by (project_id, key) before inserting or catch the
unique-constraint/IntegrityError on insert and return the existing FeatureFlag
instead of raising 409. Ensure this logic references the same unique key used by
the DB and keep responses consistent with APIResponse.success_response for the
existing record.

Comment on lines +46 to +64
feature_flag = get_feature_flag(
session=session,
key=key,
organization_id=organization_id,
project_id=project_id,
)
if feature_flag is not None:
raise HTTPException(status_code=409, detail="Feature flag already exists")

feature_flag = FeatureFlagModel(
key=key,
organization_id=organization_id,
project_id=project_id,
enabled=enabled,
)
session.add(feature_flag)
session.commit()
session.refresh(feature_flag)
return feature_flag
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

TOCTOU race in create_feature_flag.

The existence check (get_feature_flagraise 409) and the subsequent insert are not atomic. Two concurrent calls with the same (key, organization_id, project_id) can both pass the check and both attempt INSERT, with the second failing on the composite unique index from migration 057_create_feature_flag_table.py and surfacing as an unhandled IntegrityError (HTTP 500) instead of a clean 409. Catch IntegrityError after the insert and translate it, so the unique index — not the read-then-write check — is the source of truth.

🔒 Suggested fix
+from sqlalchemy.exc import IntegrityError
@@
     feature_flag = FeatureFlagModel(
         key=key,
         organization_id=organization_id,
         project_id=project_id,
         enabled=enabled,
     )
     session.add(feature_flag)
-    session.commit()
-    session.refresh(feature_flag)
+    try:
+        session.commit()
+    except IntegrityError:
+        session.rollback()
+        raise HTTPException(status_code=409, detail="Feature flag already exists")
+    session.refresh(feature_flag)
     return feature_flag
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/crud/feature_flag.py` around lines 46 - 64, The
create_feature_flag flow currently performs a read-then-write leading to a
TOCTOU race; keep the existence check but make the insert atomic by catching a
DB unique-constraint failure: wrap the session.add/commit/refresh sequence in a
try/except that catches sqlalchemy.exc.IntegrityError, calls session.rollback(),
and raises HTTPException(status_code=409, detail="Feature flag already exists");
reference the existing create_feature_flag function, the FeatureFlagModel
construction, session.commit(), and get_feature_flag, and add the
sqlalchemy.exc.IntegrityError import if missing.

Comment on lines +27 to +31
project_id: int = Field(
foreign_key="project.id",
nullable=False,
ondelete="CASCADE",
sa_column_kwargs={"comment": "Project scope for this feature flag"},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd 'feature_flag\.py$' backend/app/models --exec sed -n '20,40p' {}
fd '057_create_feature_flag_table\.py$' backend/app/alembic/versions --exec sed -n '86,100p' {}

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 1104


Add index=True to project_id to match the migration.

The migration creates an index ix_feature_flag_project_id for this column, but the model field doesn't declare it. This creates schema drift that will cause the next Alembic autogenerate pass to detect the mismatch. Keep the model metadata aligned with the actual migration.

Suggested fix
     project_id: int = Field(
         foreign_key="project.id",
+        index=True,
         nullable=False,
         ondelete="CASCADE",
         sa_column_kwargs={"comment": "Project scope for this feature flag"},
     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
project_id: int = Field(
foreign_key="project.id",
nullable=False,
ondelete="CASCADE",
sa_column_kwargs={"comment": "Project scope for this feature flag"},
project_id: int = Field(
foreign_key="project.id",
index=True,
nullable=False,
ondelete="CASCADE",
sa_column_kwargs={"comment": "Project scope for this feature flag"},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/models/feature_flag.py` around lines 27 - 31, The FeatureFlag
model's project_id Field is missing index=True which causes schema drift versus
the migration; update the Field declaration for project_id in the FeatureFlag
class to include index=True (i.e., add index=True to the Field(...) args for the
project_id attribute) so the ORM metadata matches the existing migration-created
ix_feature_flag_project_id index.

Comment thread scripts/python/invoke-cron.py Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented May 6, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Status: In Review

Development

Successfully merging this pull request may close these issues.

Feature-Flag: feature toggle system for controlled rollout

3 participants