Skip to content

feat(api): Share membership tables with OSS and fix account-table drift#4671

Merged
junaway merged 7 commits into
release/v0.104.0from
feat/oss-membership-tables
Jun 17, 2026
Merged

feat(api): Share membership tables with OSS and fix account-table drift#4671
junaway merged 7 commits into
release/v0.104.0from
feat/oss-membership-tables

Conversation

@jp-agenta

@jp-agenta jp-agenta commented Jun 12, 2026

Copy link
Copy Markdown
Member

Context

OSS is hard single-org today. The first blocker is that OSS has no membership tables at all: organization_members, workspace_members, and project_members exist only in EE, so OSS derives "which orgs does user X belong to" implicitly from organizations.owner_id plus used invitations. That only works with exactly one org. The account tables have also drifted between the two editions (details in docs/designs/oss-ee-convergence/assessment-a-oss-multi-org.md, sequencing step 1).

Changes

Membership models move from api/ee/src/models/db_models.py to api/oss/src/models/db_models.py; the EE file becomes a re-export so existing EE imports keep working. project_members.is_demo moves with the class (kept in OSS for schema parity).

New OSS core migrations:

  • 0a1b2c3d4e5f creates the three membership tables with the exact shapes the EE database has (same columns, order, FKs, including updated_by_id).
  • The same migration fixes OSS-side drift: api_keys.project_id becomes NOT NULL (rows with NULL project_id are pre-project-scoping keys that cannot authenticate any scope, so they are deleted), the duplicate fk_projects_organization_id (SET NULL) is dropped in favor of the CASCADE FK that matches EE, fk_projects_workspace_id is renamed to projects_workspace_id_fkey, and the redundant uq_projects_id is dropped.
  • 1b2c3d4e5f6a backfills membership rows: org owners become owner members of their org and its workspaces/projects; users holding a used invitation become members of the invitation's project, workspace, and org with the invitation's role.

New EE core migration 2c3d4e5f6a7b fixes EE-side drift: api_keys.created_by_id becomes nullable. No FK is added: lifecycle actor columns carry no FKs by convention (phase-2 review decision); OSS drops its legacy FK on this column in the follow-up cleanup.

Both chains also align projects with the shared model: organization_id and workspace_id become NOT NULL (after backfill) and the workspace FK becomes ON DELETE CASCADE. The backfill works with 0, 1, or N organizations, slugged or not: the OSS org adoption prefers the legacy singleton (slug oss-default, backfilled earlier in the chain) and falls back to the oldest org. On fresh replays the chain seeds a default project (911e6034d05e) before any organization can exist, so NULL-scope projects on a user-less database are deleted (they are recreated per organization at signup); on databases with users both migrations still raise instead of guessing.

Behavior change: get_user_organizations() in db_manager.py now uses the membership join in both editions. Before, the OSS branch returned [].

Tests / notes

  • ruff format and ruff check pass; all touched files compile.
  • The OSS and EE alembic chains are independent; each migration lands in its own chain only.
  • After this PR the OSS core schema differs from EE only by the four EE-only tables (meters, subscriptions, organization_domains, organization_providers), the legacy tables (dropped in a follow-up PR), and the cosmetic api_keys column order.
  • Reviewers: the membership table shapes were taken from a live EE schema dump (docs/designs/oss-ee-convergence/ee_core.txt), not from the model classes; the dump carries updated_by_id columns the models do not declare.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Jun 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview, Comment Jun 16, 2026 12:34pm

Request Review

@dosubot dosubot Bot added size:XL This PR changes 500-999 lines, ignoring generated files. Backend labels Jun 12, 2026
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds OrganizationMemberDB, WorkspaceMemberDB, and ProjectMemberDB ORM models to OSS; re-exports them in EE. Introduces OSS and EE Alembic migrations that create membership tables, enforce account schema parity, and backfill historical membership rows. Updates get_user_organizations to query via the membership join unconditionally. Adds a separate data migration that repairs workflow revision version numbering inconsistencies.

Changes

Membership models, account schema parity, and service integration

Layer / File(s) Summary
Membership DB model definitions
api/oss/src/models/db_models.py, api/ee/src/models/db_models.py
Adds OrganizationMemberDB, WorkspaceMemberDB, and ProjectMemberDB ORM models with UUID PKs, foreign keys, role fields, optional timestamps and is_demo flag. EE db_models becomes a re-export of the three OSS classes, removing local definitions.
Service query updated to use membership model
api/oss/src/services/db_manager.py
Imports OrganizationMemberDB unconditionally and replaces get_user_organizations with a join query on OrganizationMemberDB.user_id, removing the prior is_ee() branch and OSS empty-list fallback.
OSS migration: membership tables and account parity
api/oss/databases/postgres/migrations/core/versions/0a1b2c3d4e5f_add_membership_tables_and_account_parity.py
Creates organization_members, workspace_members, project_members tables; enforces api_keys.project_id NOT NULL; backfills and validates projects.organization_id/workspace_id; adds cascading FK for projects.workspace_id; downgrade reverses all steps.
Membership data backfill logic and migration entrypoint
api/oss/databases/postgres/migrations/core/data_migrations/memberships.py, api/oss/databases/postgres/migrations/core/versions/1b2c3d4e5f6a_backfill_membership_rows.py
Backfill loads organizations, workspaces, projects, and existing membership records; inserts missing rows for organization owners and invitation users with appropriate roles; downgrade is a no-op; Alembic entrypoint delegates to the data migration helpers.
EE account schema parity migration
api/ee/databases/postgres/migrations/core/versions/2c3d4e5f6a7b_account_schema_parity.py
Makes api_keys.created_by_id nullable; replaces projects.workspace_id FK with ON DELETE CASCADE; backfills and validates projects.organization_id/workspace_id; enforces NOT NULL on both; downgrade reverts constraints.

Workflow revision version repair

Layer / File(s) Summary
Workflow revision version repair data migration
api/oss/databases/postgres/migrations/core/data_migrations/workflow_revision_versions.py
Seeds missing version 0 rows for variants using UUIDv7 ids; repairs duplicate version numbers by sequential reindex; propagates corrections into environment_revisions.data->references JSON via a temp mapping table; downgrade raises NotImplementedError.
OSS and EE Alembic entrypoints
api/oss/databases/postgres/migrations/core/versions/b3c4d5e6f7a9_repair_workflow_revision_versions.py, api/ee/databases/postgres/migrations/core/versions/b3c4d5e6f7a9_repair_workflow_revision_versions.py
Both entrypoints obtain the Alembic connection and delegate to upgrade_workflow_revision_versions/downgrade_workflow_revision_versions, registering the repair step in each migration chain.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(70, 130, 180, 0.5)
    note over Alembic,PostgreSQL: OSS upgrade: 0a1b2c3d4e5f
    Alembic->>PostgreSQL: CREATE TABLE organization_members / workspace_members / project_members
    Alembic->>PostgreSQL: DELETE api_keys WHERE project_id IS NULL
    Alembic->>PostgreSQL: ALTER api_keys.project_id NOT NULL
    Alembic->>PostgreSQL: Backfill projects.organization_id / workspace_id
    Alembic->>PostgreSQL: DELETE orphan projects (no users guard)
    Alembic->>PostgreSQL: COUNT(*) NULL check → RuntimeError if > 0
    Alembic->>PostgreSQL: ALTER projects NOT NULL + ADD FK CASCADE
  end
  rect rgba(60, 179, 113, 0.5)
    note over Alembic,PostgreSQL: OSS upgrade: 1b2c3d4e5f6a (backfill)
    Alembic->>upgrade_membership_backfill: session
    upgrade_membership_backfill->>PostgreSQL: Query orgs / workspaces / projects / memberships
    upgrade_membership_backfill->>PostgreSQL: INSERT missing owner memberships
    upgrade_membership_backfill->>PostgreSQL: INSERT missing invitation-derived memberships
  end
  rect rgba(210, 105, 30, 0.5)
    note over Alembic,PostgreSQL: EE upgrade: 2c3d4e5f6a7b
    Alembic->>PostgreSQL: ALTER api_keys.created_by_id nullable
    Alembic->>PostgreSQL: DROP / RECREATE projects_workspace_id_fkey CASCADE
    Alembic->>PostgreSQL: Backfill projects.organization_id / workspace_id via members
    Alembic->>PostgreSQL: DELETE orphan projects (no users guard)
    Alembic->>PostgreSQL: COUNT(*) NULL check → RuntimeError if > 0
    Alembic->>PostgreSQL: ALTER projects NOT NULL
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Agenta-AI/agenta#4600: Introduces count_organization_members() for EE self-serve account deletion, directly consuming the OrganizationMemberDB model that this PR moves from EE-local definitions into the shared OSS layer.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.90% which is insufficient. The required threshold is 60.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title concisely and accurately summarizes the primary change: moving membership tables to OSS and addressing schema drift between OSS and EE.
Description check ✅ Passed The description provides thorough context about the OSS membership table limitations, detailed enumeration of all changes made, new migrations added, behavioral changes, and verification steps.
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 feat/oss-membership-tables

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b5fb7659-a52c-49ed-8bad-b932df0176f0

📥 Commits

Reviewing files that changed from the base of the PR and between a2e9150 and 661a7d3.

📒 Files selected for processing (7)
  • api/ee/databases/postgres/migrations/core/versions/2c3d4e5f6a7b_account_schema_parity.py
  • api/ee/src/models/db_models.py
  • api/oss/databases/postgres/migrations/core/data_migrations/memberships.py
  • api/oss/databases/postgres/migrations/core/versions/0a1b2c3d4e5f_add_membership_tables_and_account_parity.py
  • api/oss/databases/postgres/migrations/core/versions/1b2c3d4e5f6a_backfill_membership_rows.py
  • api/oss/src/models/db_models.py
  • api/oss/src/services/db_manager.py

Comment thread api/oss/databases/postgres/migrations/core/data_migrations/memberships.py Outdated
Comment thread api/oss/src/models/db_models.py
@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Preview URL https://gateway-production-8da9.up.railway.app/w
Project agenta-oss-pr-4671
Image tag pr-4671-43a5df7
Status Deployed
Railway logs Open logs
Workflow logs View workflow run
Updated at 2026-06-16T12:44:17.465Z

@mmabrouk mmabrouk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
api/ee/databases/postgres/migrations/core/versions/2c3d4e5f6a7b_account_schema_parity.py (1)

48-59: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Non-deterministic organization selection when user belongs to multiple organizations.

The subquery uses LIMIT 1 without ORDER BY, so the selected organization_id depends on physical row ordering. For users with multiple organization memberships, different migration runs could assign the same project to different organizations.

The workspace backfill at lines 60-69 correctly uses ORDER BY w.created_at for determinism—apply the same pattern here.

Proposed fix to add deterministic ordering
     op.execute(
         """
         UPDATE projects p SET organization_id = (
             SELECT om.organization_id
             FROM project_members pm
             JOIN organization_members om ON om.user_id = pm.user_id
             WHERE pm.project_id = p.id AND om.organization_id IS NOT NULL
+            ORDER BY om.created_at
             LIMIT 1
         )
         WHERE p.organization_id IS NULL
         """
     )

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 65c0f48c-2ff7-4ec5-b4b3-0d03cef027ba

📥 Commits

Reviewing files that changed from the base of the PR and between 911d815 and 339c787.

📒 Files selected for processing (1)
  • api/ee/databases/postgres/migrations/core/versions/2c3d4e5f6a7b_account_schema_parity.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
api/oss/databases/postgres/migrations/core/versions/0a1b2c3d4e5f_add_membership_tables_and_account_parity.py (1)

39-42: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add ON DELETE CASCADE to the membership user_id FKs.

api/oss/src/models/db_models.py declares OrganizationMemberDB.user_id, WorkspaceMemberDB.user_id, and ProjectMemberDB.user_id with ondelete="CASCADE", but this migration creates all three constraints without it. That recreates schema drift immediately, and deleting a user will start failing once membership rows exist.

Suggested fix
-        sa.ForeignKeyConstraint(["user_id"], ["users.id"]),
+        sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
@@
-        sa.ForeignKeyConstraint(["user_id"], ["users.id"]),
+        sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
@@
-        sa.ForeignKeyConstraint(["user_id"], ["users.id"]),
+        sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),

Also applies to: 55-58, 72-73

🧹 Nitpick comments (2)
api/entrypoints/worker_events.py (1)

26-27: ⚡ Quick win

Extract duplicate MAXLEN_QUEUES_WEBHOOKS constant to a shared location.

Both files define MAXLEN_QUEUES_WEBHOOKS = 100_000 for the same stream ("queues:webhooks"). To prevent drift, extract this constant to a shared module (e.g., api/oss/src/utils/constants.py or api/oss/src/core/webhooks/constants.py) and import it in both workers.

  • api/entrypoints/worker_events.py#L26-L27: replace local definition with import from shared constants module.
  • api/entrypoints/worker_webhooks.py#L29-L30: replace local definition with import from shared constants module.
api/oss/tests/legacy/conftest.py (1)

842-842: 💤 Low value

Add trailing slashes to OSS API test secret paths for consistency.

The OSS API test files use "secrets" without trailing slashes, while the SDK test files correctly use "secrets/" with trailing slashes to match the router definition at api/oss/src/apis/fastapi/vault/router.py. FastAPI will redirect, but the inconsistency adds unnecessary redirects and could confuse maintainers.

  • api/oss/tests/legacy/conftest.py#L842-L842: Change "secrets" to "secrets/" in reset_llm_keys GET request.
  • api/oss/tests/legacy/conftest.py#L859-L859: Change "secrets" to "secrets/" in set_valid_llm_keys POST request.
  • api/oss/tests/legacy/conftest.py#L880-L880: Change "secrets" to "secrets/" in set_invalid_llm_keys GET request.
  • api/oss/tests/legacy/workflows/core/tests.py#L182-L182: Change "secrets" to "secrets/" in test_missing_provider_key POST request.
  • api/oss/tests/legacy/workflows/core/tests.py#L226-L226: Change "secrets" to "secrets/" in test_available_provider_key_with_invalid_provider_key POST request.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c612a217-4b41-441e-b67a-d4916c4c6da3

📥 Commits

Reviewing files that changed from the base of the PR and between 339c787 and 9437bd2.

⛔ Files ignored due to path filters (4)
  • api/uv.lock is excluded by !**/*.lock
  • clients/python/uv.lock is excluded by !**/*.lock
  • sdks/python/uv.lock is excluded by !**/*.lock
  • services/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (22)
  • api/ee/databases/postgres/migrations/core/versions/2c3d4e5f6a7b_account_schema_parity.py
  • api/entrypoints/worker_evaluations.py
  • api/entrypoints/worker_events.py
  • api/entrypoints/worker_webhooks.py
  • api/oss/databases/postgres/migrations/core/versions/0a1b2c3d4e5f_add_membership_tables_and_account_parity.py
  • api/oss/src/core/events/streaming.py
  • api/oss/src/core/tracing/streaming.py
  • api/oss/src/services/db_manager.py
  • api/oss/tests/legacy/conftest.py
  • api/oss/tests/legacy/workflows/core/tests.py
  • api/pyproject.toml
  • clients/python/pyproject.toml
  • docs/openapi-cleanup/research.md
  • hosting/kubernetes/helm/Chart.yaml
  • sdks/python/oss/tests/legacy/new_tests/conftest.py
  • sdks/python/oss/tests/legacy/new_tests/workflows/core/tests.py
  • sdks/python/pyproject.toml
  • services/pyproject.toml
  • web/ee/package.json
  • web/oss/package.json
  • web/package.json
  • web/packages/agenta-api-client/package.json
✅ Files skipped from review due to trivial changes (10)
  • api/pyproject.toml
  • web/packages/agenta-api-client/package.json
  • web/oss/package.json
  • clients/python/pyproject.toml
  • services/pyproject.toml
  • sdks/python/pyproject.toml
  • web/ee/package.json
  • docs/openapi-cleanup/research.md
  • hosting/kubernetes/helm/Chart.yaml
  • web/package.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • api/oss/src/services/db_manager.py
  • api/ee/databases/postgres/migrations/core/versions/2c3d4e5f6a7b_account_schema_parity.py

@junaway junaway force-pushed the feat/oss-membership-tables branch from 9437bd2 to 339c787 Compare June 14, 2026 13:03
@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines, ignoring generated files. and removed size:XL This PR changes 500-999 lines, ignoring generated files. labels Jun 14, 2026
Moves OrganizationMemberDB/WorkspaceMemberDB/ProjectMemberDB from EE to the
shared OSS models (EE re-exports), creates the three tables in the OSS chain
with the exact EE shapes, backfills membership rows from owners and used
invitations, and aligns api_keys/projects with the shared model in both
chains. get_user_organizations now uses the membership join in both editions.
…atabases

Fresh replays seed a default project (911e6034d05e) before any organization
can exist, so the parity migrations' NULL-scope guard killed the chain on
new deployments. Delete NULL-scope projects when the database has no users
(the fresh-replay signature); real deployments keep failing loudly.

The OSS org backfill now prefers the legacy singleton (slug oss-default,
NULLS LAST so unslugged orgs sort after) before falling back to the oldest
org, making adoption deterministic with 0, 1, or N orgs and NULL or set
slugs.
Lifecycle actor columns carry no FKs by convention (settled in the phase-2
review). The parity direction reverses for this column: instead of EE gaining
the OSS FK, OSS drops its legacy FK in the cleanup migration. Only the
nullable-ification stays here. Editing the unshipped migration avoids
shipping an FK that the next PR would immediately drop.
…the chain

The v0.103.5 merge dropped b3c4d5e6f7a9 (repair_workflow_revision_versions),
the head of the v0.103.5 core chain, and re-rooted the membership/parity
migrations onto its parent a2b3c4d5e6f8. Any database that ran v0.103.5 is
stamped at b3c4d5e6f7a9, so upgrading it into the stack failed with
'Can't locate revision b3c4d5e6f7a9'. Restore the migration (OSS + EE) and its
shared data_migration verbatim, and re-root 0a1b2c3d4e5f / 2c3d4e5f6a7b onto
b3c4d5e6f7a9 so the chain is a2b3c4d5e6f8 -> b3c4d5e6f7a9 -> membership.
The 911e6034d05e boot seed creates an empty default project before any org
exists. On a populated DB the app already made a real default project per
workspace at signup, so adopting the seed left two is_default projects in one
workspace and the members view (which reads the default project) showed only
the seed's lone member. Drop the unscoped empty seed when a scoped default
already exists; the org keeps exactly one default.
@junaway junaway force-pushed the feat/oss-membership-tables branch from 774c502 to 548913d Compare June 14, 2026 17:13
@dosubot dosubot Bot added size:XL This PR changes 500-999 lines, ignoring generated files. and removed size:XXL This PR changes 1000+ lines, ignoring generated files. labels Jun 14, 2026
The data migration imported live ORM models, so a later model change could
break replaying revision 1b2c3d4e5f6a on a fresh or delayed upgrade. Use
local sa.table()/sa.column() definitions pinned to the schema at this
revision instead. Backfill logic is unchanged.
@junaway junaway changed the base branch from main to release/v0.104.0 June 17, 2026 08:40
@junaway junaway merged commit 9f8aeee into release/v0.104.0 Jun 17, 2026
52 of 53 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Backend lgtm This PR has been approved by a maintainer size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants