Skip to content

feat(data-warehouse): onboard teams via duckgres org-teams api - #72302

Merged
benben merged 6 commits into
masterfrom
ben/data-ops-team-onboarding
Jul 21, 2026
Merged

feat(data-warehouse): onboard teams via duckgres org-teams api#72302
benben merged 6 commits into
masterfrom
ben/data-ops-team-onboarding

Conversation

@benben

@benben benben commented Jul 20, 2026

Copy link
Copy Markdown
Member

Problem

The managed warehouse (/data-ops) is org-scoped but team membership only lived in Django (DuckgresServerTeam). The duckgres control plane now tracks teams first-class via its org-teams API, including per-team schemas and the billing team. The app needs to speak that API so a second project in an org can onboard itself with its own schema, existing teams get grandfathered into the control plane, and deleting a project keeps duckgres consistent.

Note

Deploy ordering: this PR requires the duckgres org-teams API (GET/POST /orgs/{org}/teams, DELETE /orgs/{org}/teams/{team_id}, provision accepting team_id + schema_name) to be deployed first. This PR sends no default_team_id anywhere anymore.

Changes

Adapter (products/data_warehouse/backend/presentation/views/managed_warehouse.py):

  • list_teams, create_team, delete_team proxy the new org-teams endpoints. create_team omits unset legacy table-name fields so duckgres derives the <schema>.events layout for new teams.
  • provision now sends team_id + schema_name for the calling team instead of default_team_id. The user-facing "table name" pick became the "schema name" pick.
  • onboard_team dual-writes: Django guards run first (the duckgres upsert would overwrite an existing row's schema), then the duckgres team row, then the Django backfill row that Dagster still reads. A duckgres 409 surfaces as a clear schema-conflict error.
  • team_onboarding_state powers the status response: it resolves whether the calling team has a duckgres row and lazily grandfathers Django-only teams into duckgres (schema table_suffix or team_<id>, explicit legacy names events_<suffix>/events, persons_<suffix>/persons, posthog_data_imports_<suffix>/posthog_data_imports_team_<id>, matching the duckling DAG and the data-imports schema helper). Best-effort and idempotent, never fails the status read.
  • check_schema_name checks a candidate schema against both duckgres rows and Django-only suffixes (not yet grandfathered).
  • block_team_deletion removes the team from duckgres ahead of a Django team deletion. It only calls the control plane when the org has a DuckgresServer row, blocks with guidance when duckgres 409s on its last team, and blocks with a retry error when the control plane is unreachable for a Django-onboarded team. Teams without warehouse involvement are unaffected.

API (data_warehouse.py viewset):

  • provision takes schema_name (was table_name); enable_backfill is replaced by onboard-team; new check-schema-name; warehouse_status gains team_onboarded + schema_name.

Team deletion:

  • TeamViewSet.perform_destroy and ProjectViewSet.perform_destroy consult block_team_deletion before dispatching the deletion workflow. For a project, environments already removed from duckgres before a block self-heal via the lazy grandfather push on the next status read.

Frontend (warehouseProvisioningLogic.ts + SettingsTab.tsx):

  • Provision form asks for a schema name (format validation as before) and states it can't be changed later.
  • New onboarding screen when the warehouse is ready but the current project isn't onboarded: pick a schema name with a live availability check (same UX as the database-name check), submit calls onboard-team, 409 surfaces as a clear conflict.
  • Onboarded projects see a read-only schema card. Connection details and password reset only show once onboarded.

Django admin: provision and enable-backfill forms now take a schema name; the enable-backfill view runs the same dual-write onboarding path.

flowchart TD
    S[warehouse_status] --> A{warehouse in duckgres?}
    A -- no --> P[Provision form: database name + schema name]
    A -- yes --> B{team onboarded?}
    B -- duckgres row --> N[Normal screen]
    B -- Django row only --> G[Lazy grandfather push to duckgres] --> N
    B -- no rows --> O[Onboarding screen: pick schema, live availability, onboard-team]
    classDef phBlue fill:#1d4aff,stroke:#1d4aff,color:#fff;
    classDef phYellow fill:#f9bd2b,stroke:#f9bd2b,color:#000;
    classDef phGray fill:#e5e7eb,stroke:#c7ccd1,color:#000;
    class S,P,O phBlue;
    class N phYellow;
    class A,B,G phGray;
Loading

How did you test this code?

Automated tests only (no manual run against a live control plane):

  • products/data_warehouse/backend/tests/api/test_managed_warehouse.py (46 passed): provision body carries team_id + schema_name and never default_team_id; org-teams proxy routes and upsert body shape (unset legacy fields omitted, which would otherwise flip a derived-layout team to explicit names); onboard-team dual-write, 409 surfacing, and Django-guard-before-control-plane ordering; onboarding-state branches including both grandfather mappings (suffix set and NULL) and control-plane failure fallbacks; schema-name availability against duckgres and Django-only rows; every block_team_deletion branch (no warehouse, deleted/404, last-team 409, unreachable for onboarded vs not-onboarded team).
  • posthog/ducklake/test_common.py (45 passed) covers the enable_team_backfill guard split (check_team_backfill_enable).
  • posthog/api/test/test_team.py / test_project.py deletion tests: new wiring guard that a warehouse block stops the deletion before the workflow dispatch, on both the environments and projects routes.
  • Frontend: warehouseProvisioningLogic.test.ts (11 passed) — onboarding branch detection, onboard success refresh, and 409 marking the schema name unavailable. No component-render tests added; the behavior lives in the kea logic.
  • pnpm --filter=@posthog/frontend typescript:check clean, hogli build:openapi regenerated types, hogli ci:preflight --fix clean.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Docs update

No docs under docs/ cover the managed-warehouse onboarding flow yet.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Authored by Claude Code (Fable 5) directed by the assignee. Skills invoked: /improving-drf-endpoints and /writing-tests.

Decisions worth knowing about while reviewing:

  • Onboarding runs Django guards before the duckgres upsert because the upsert overwrites an existing row's schema; a rejected rename must never reach the control plane. The reverse order would let a legacy team's Django-only suffix be claimed in duckgres and then rejected locally, wedging the later grandfather push on a permanent 409.
  • On control-plane failure during team deletion, an onboarded team's deletion is blocked with a "try again" error rather than proceeding, to avoid orphaning duckgres rows. A team without a Django backfill row is let through on control-plane failure so a duckgres outage can't brick unrelated deletions in warehouse orgs.
  • The old enable_backfill endpoint is removed rather than kept as an alias; the scene is its only consumer and the admin now uses the onboarding path. A stale frontend would briefly 404 on it during rollout.
  • The grandfather mapping's legacy shared-table names (events/persons for NULL suffix, posthog_data_imports_team_<id>) were verified against the duckling backfill DAG and duckgres_data_imports_schema before hardcoding.

benben added 2 commits July 20, 2026 15:52
Switch managed-warehouse provisioning to the duckgres org-teams API:
provision sends team_id + schema_name (no default_team_id), new
list/create/delete team adapter functions, a team onboarding flow for
additional projects, lazy grandfathering of existing Django-only teams,
and team deletion blocking when duckgres refuses to drop its last team.
@benben benben self-assigned this Jul 20, 2026
@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

Bundle size — 🟢 -72.4 KiB (-0.1%)

Uncompressed size of every built .js bundle, compared against the base branch.

Total: 64.16 MiB · 🟢 -72.4 KiB (-0.1%)

File Size Δ vs base
render-query/src/render-query/render-query.js 21.18 MiB 🟢 -160.0 KiB (-0.7%)
exporter/_parent/products/alerts/frontend/views/EditAlertModal.js 94.6 KiB 🔺 +93.1 KiB (+6237.0%)
posthog-app/_parent/products/alerts/frontend/AlertsScene.js 9.9 KiB 🔺 +9.9 KiB (new)
posthog-app/_parent/products/customer_analytics/frontend/scenes/CustomerJourneyTemplatesScene/CustomerJourneyTemplatesScene.js 10.1 KiB 🟢 -1.1 KiB (-9.5%)
posthog-app/src/scenes/insights/InsightQuickStart/InsightQuickStart.js 8.2 KiB 🟢 -1.1 KiB (-11.4%)
posthog-app/src/scenes/saved-insights/SavedInsights.js 3.7 KiB 🟢 -1.1 KiB (-22.5%)
posthog-app/_parent/products/posthog_ai/frontend/components/tool/widgets/SearchSessionRecordingsWidget.js 4.1 KiB 🟢 -1.0 KiB (-19.9%)

Posted automatically by build-bundle-size-report · uncompressed bytes from dist-report

Eager graph — within budget

How much code each root ships on the eager path — downloaded and parsed before the surface is interactive. Measured from the esbuild output chunks (post-tree-shake, static imports only); lazy import() / React.lazy chunks are not counted.

Root Eager (shipped) Δ vs base Budget
entry (logged-out pages, app bootstrap)
src/index.tsx
1.23 MiB · 22 files 🔺 +234 B (+0.0%) ███░░░░░░░ 28.6% of 4.29 MiB
authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
8.18 MiB · 2,997 files 🔺 +684 B (+0.0%) █████████░ 88.4% of 9.25 MiB

🟢 node_modules/monaco-editor/ stays out of src/index.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 node_modules/monaco-editor/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx

Largest files eagerly shipped from src/index.tsx
Size File
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
24.6 KiB ../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.js
6.3 KiB ../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js
4.5 KiB ../node_modules/.pnpm/@jspm+core@2.1.0/node_modules/@jspm/core/nodelibs/browser/process.js
3.9 KiB ../node_modules/.pnpm/scheduler@0.23.2/node_modules/scheduler/cjs/scheduler.production.min.js
1.4 KiB ../node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js
1.3 KiB src/RootErrorBoundary.tsx
912 B ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js
789 B src/scenes/ChunkLoadErrorBoundary.tsx
762 B src/index.tsx
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
Size File
281.3 KiB ../node_modules/.pnpm/posthog-js@1.404.1/node_modules/posthog-js/dist/rrweb.js
267.7 KiB ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
235.5 KiB src/taxonomy/core-filter-definitions-by-group.json
223.4 KiB ../node_modules/.pnpm/posthog-js@1.404.1/node_modules/posthog-js/dist/module.js
164.0 KiB src/queries/validators.js
154.3 KiB ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
105.9 KiB src/lib/api.ts
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
93.2 KiB ../packages/quill/packages/quill/dist/index.js

Posted automatically by check-eager-graph · sizes are eager output bytes (shipped, post-tree-shake) from the esbuild metafile · part of #32479

Dist folder size — 🔺 +1.23 MiB (+0.1%)

Total size of the built frontend/dist folder (all assets), compared against the base branch.

Total: 1344.09 MiB · 🔺 +1.23 MiB (+0.1%)

ℹ️ MCP UI apps size — 31 app(s), 16603.4 KB JS

Built size of each MCP UI app (main.js + styles.css).

App JS CSS
debug 599.5 KB 187.4 KB
action 457.8 KB 187.4 KB
action-list 564.3 KB 187.4 KB
cohort 456.8 KB 187.4 KB
cohort-list 563.3 KB 187.4 KB
email-template 456.6 KB 187.4 KB
error-details 472.4 KB 187.4 KB
error-issue 457.5 KB 187.4 KB
error-issue-list 564.2 KB 187.4 KB
experiment 561.5 KB 187.4 KB
experiment-list 565.1 KB 187.4 KB
experiment-results 563.2 KB 187.4 KB
feature-flag 567.1 KB 187.4 KB
feature-flag-list 570.9 KB 187.4 KB
feature-flag-testing 461.0 KB 187.4 KB
insight-actors 562.1 KB 187.4 KB
invite-email-preview 456.0 KB 187.4 KB
llm-costs 559.5 KB 187.4 KB
session-recording 458.6 KB 187.4 KB
session-summary 463.9 KB 187.4 KB
survey 458.4 KB 187.4 KB
survey-global-stats 562.2 KB 187.4 KB
survey-list 565.0 KB 187.4 KB
survey-stats 562.2 KB 187.4 KB
trace-span 457.2 KB 187.4 KB
trace-span-list 564.2 KB 187.4 KB
workflow 457.1 KB 187.4 KB
workflow-list 563.7 KB 187.4 KB
query-results 745.1 KB 187.4 KB
render-ui 825.7 KB 187.4 KB
visual-review-snapshots 461.6 KB 187.4 KB
Playwright — all passed

All tests passed.

View test results →

⚠️ Backend coverage — 96.0% of changed backend lines covered — 16 uncovered

🧪 Backend test coverage

Patch coverage — changed backend lines (products + core): ███████████████████░ 96.0% (407 / 423)

File Patch Uncovered changed lines
posthog/ducklake/common.py 65.0% 588–594
products/data_warehouse/backend/presentation/views/data_warehouse.py 75.0% 894, 897, 1015
products/data_warehouse/backend/presentation/views/managed_warehouse.py 95.0% 410, 450, 452, 490–491, 589

🤖 Agents: add a test covering the lines above, or note why under "How did you test this code?". Machine-readable gap list: the patch-coverage artifact on this run (gh run download 29756980746 -n patch-coverage), or the coverage-data block at the end of this comment.

Per-product line coverage (touched products)
Product Coverage Lines
platform_features ██░░░░░░░░░░░░░░░░░░ 12.1% 7 / 58
batch_exports ████████░░░░░░░░░░░░ 39.7% 8,416 / 21,225
demo ███████████░░░░░░░░░ 56.2% 1,497 / 2,663
warehouse_sources_queue ████████████░░░░░░░░ 59.2% 148 / 250
tasks █████████████░░░░░░░ 67.1% 26,193 / 39,053
data_tools ██████████████░░░░░░ 70.0% 63 / 90
ai_gateway ███████████████░░░░░ 75.0% 9 / 12
signals ████████████████░░░░ 79.4% 19,499 / 24,561
data_modeling ████████████████░░░░ 80.0% 4,834 / 6,045
cdp ████████████████░░░░ 80.7% 3,118 / 3,864
wizard ████████████████░░░░ 82.5% 772 / 936
notebooks █████████████████░░░ 85.1% 7,096 / 8,338
cohorts █████████████████░░░ 86.2% 4,065 / 4,717
agent_platform █████████████████░░░ 86.4% 3,807 / 4,405
actions █████████████████░░░ 86.6% 717 / 828
product_tours █████████████████░░░ 87.5% 1,266 / 1,447
exports ██████████████████░░ 88.4% 6,943 / 7,853
business_knowledge ██████████████████░░ 88.5% 4,400 / 4,969
conversations ██████████████████░░ 89.1% 16,372 / 18,371
mcp_analytics ██████████████████░░ 89.2% 2,514 / 2,819
dashboards ██████████████████░░ 89.4% 5,917 / 6,616
visual_review ██████████████████░░ 89.4% 5,818 / 6,505
engineering_analytics ██████████████████░░ 89.5% 5,507 / 6,154
alerts ██████████████████░░ 89.9% 4,054 / 4,508
error_tracking ██████████████████░░ 90.1% 9,757 / 10,828
early_access_features ██████████████████░░ 90.1% 1,031 / 1,144
streamlit_apps ██████████████████░░ 90.4% 2,501 / 2,767
slack_app ██████████████████░░ 90.6% 8,989 / 9,926
links ██████████████████░░ 90.6% 183 / 202
marketing_analytics ██████████████████░░ 90.8% 11,514 / 12,684
stamphog ██████████████████░░ 91.0% 3,993 / 4,387
product_analytics ██████████████████░░ 91.4% 5,781 / 6,328
mcp_store ██████████████████░░ 91.8% 3,685 / 4,012
data_warehouse ███████████████████░ 92.6% 19,145 / 20,683
notifications ███████████████████░ 92.7% 1,031 / 1,112
ai_observability ███████████████████░ 92.7% 14,921 / 16,093
workflows ███████████████████░ 92.8% 5,520 / 5,949
web_analytics ███████████████████░ 92.9% 13,853 / 14,913
surveys ███████████████████░ 93.0% 5,724 / 6,157
posthog_ai ███████████████████░ 93.2% 1,325 / 1,421
approvals ███████████████████░ 93.3% 3,395 / 3,640
reminders ███████████████████░ 93.4% 468 / 501
tracing ███████████████████░ 93.4% 2,546 / 2,725
managed_migrations ███████████████████░ 93.8% 1,220 / 1,300
legal_documents ███████████████████░ 94.1% 1,568 / 1,667
endpoints ███████████████████░ 94.1% 8,606 / 9,143
messaging ███████████████████░ 94.2% 2,647 / 2,810
revenue_analytics ███████████████████░ 94.5% 3,598 / 3,809
skills ███████████████████░ 94.5% 2,881 / 3,049
review_hog ███████████████████░ 94.6% 6,806 / 7,191
logs ███████████████████░ 95.4% 9,915 / 10,396
experiments ███████████████████░ 95.7% 24,417 / 25,527
replay_vision ███████████████████░ 95.8% 13,850 / 14,459
growth ███████████████████░ 95.8% 2,837 / 2,960
annotations ███████████████████░ 96.2% 732 / 761
feature_flags ███████████████████░ 96.3% 16,233 / 16,859
user_interviews ███████████████████░ 96.4% 2,242 / 2,325
warehouse_sources ███████████████████░ 96.5% 289,438 / 300,067
access_control ███████████████████░ 96.8% 849 / 877
customer_analytics ███████████████████░ 97.2% 7,482 / 7,700
data_catalog ███████████████████░ 97.4% 2,303 / 2,365
analytics_platform ████████████████████ 98.0% 2,102 / 2,145
metrics ████████████████████ 98.2% 2,491 / 2,536
pulse ████████████████████ 98.4% 2,017 / 2,049
live_debugger ████████████████████ 99.2% 613 / 618
field_notes ████████████████████ 99.4% 158 / 159

Report-only. Patch coverage = changed backend lines covered vs origin/master. Sorted lowest first.
Known gaps: lines covered only by Temporal tests show as uncovered; core line numbers may drift if master changed the same file.

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

🦔 Hogbox preview · ✅ ready

▶ Open the preview

🔑 Login test@posthog.com / 12345678 (demo data)
🧩 Running this PR's backend and frontend, on the PostHog :master base
🔗 Link stable across rebuilds — a re-push swaps the box underneath, the URL stays
🔒 Access tailnet only (PostHog VPN)
🛠️ Admin inspect & debug state in hogland
💤 Idle sleeps after ~30 min idle (snapshot to S3, zero node cost) and wakes on your next visit in ~30s, behind a brief "waking up" screen

commit c0c4870 · box box-3d95ca682fd9 · ready in 801s (push → usable) · build log · rebuilds on every push, torn down on close

@pr-assigner-resolver-posthog
pr-assigner-resolver-posthog Bot requested review from a team July 20, 2026 14:53
@greptile-apps

greptile-apps Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 4
products/data_warehouse/backend/presentation/views/managed_warehouse.py:274-277
**Provisioning Can Skip Django Registration**

If `enable_team_backfill` fails after Duckgres accepts provisioning, this exception is swallowed and provisioning still succeeds. Status then finds the Duckgres team and reports it as onboarded, but the missing `DuckgresServerTeam` row means the Dagster backfill never runs and this path does not recreate it.

### Issue 2 of 4
products/data_warehouse/backend/presentation/views/managed_warehouse.py:410-417
**Dual Write Leaves Missing Backfill State**

The Duckgres upsert has already succeeded when this Django write runs. If the write loses a race or raises a database error, the request fails with a Duckgres-only team; later status reads report that team as onboarded but never create the `DuckgresServerTeam` row required by Dagster.

### Issue 3 of 4
frontend/src/scenes/data-warehouse/scene/warehouseProvisioningLogic.ts:523-528
**Prefilled Schema Never Gets Checked**

`afterMount` sets the default schema before the status response can make `needsTeamOnboarding` true, so this branch skips its availability request. When an existing warehouse then reports that the team needs onboarding, the valid prefilled name remains at `schemaNameAvailable === null` and the enable button stays disabled until the user edits the name.

### Issue 4 of 4
frontend/src/scenes/data-warehouse/scene/warehouseProvisioningLogic.ts:532-542
**Stale Checks Clear Current State**

Only the success assignment checks that the response still belongs to the current name. If a request for an older name fails after a newer request succeeds, its catch resets the newer name's availability to `null`, and either stale completion can clear `schemaNameChecking` while another request remains active.

Reviews (1): Last reviewed commit: "fix(data-warehouse): canonical kea-typeg..." | Re-trigger Greptile

…lity checks

Address review findings on the team-onboarding flow:

- A duckgres team row without a Django backfill row (provision registered the
  team with the control plane but enable_team_backfill failed, or onboard's
  Django write raced) is now healed on the next status read: the Django row is
  recreated from the duckgres schema so Dagster picks the team up instead of it
  reporting onboarded forever with no backfill. Guarded to rows without legacy
  table-name overrides, where schema name and table suffix are the same
  identifier by construction.
- The prefilled default schema name now gets its availability checked when the
  status response flips the scene into team onboarding (the prefill runs on
  mount, before needsTeamOnboarding can be true, so its check never fired).
- Availability checks guard every state write on the request's name still being
  current, so a stale completion (success or failure) can no longer clobber a
  newer check's result or clear its spinner.
@veria-ai

veria-ai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 1 · PR risk: 0/10

…x stale admin tests

The schema availability check scans every project's schema in the org, so a
non-admin could probe candidate names and learn what otherwise inaccessible
projects use. Gate it behind the same organization-admin check as onboarding,
and show non-admins an ask-an-admin state instead of the onboarding form.

Also update the Django admin and access-control tests that still exercised the
old surface (table_name field, removed enable_backfill adapter).
@benben
benben merged commit 6720c74 into master Jul 21, 2026
334 of 340 checks passed
@benben
benben deleted the ben/data-ops-team-onboarding branch July 21, 2026 13:07
@deployment-status-posthog

deployment-status-posthog Bot commented Jul 21, 2026

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-07-21 13:38 UTC Run
prod-us ✅ Deployed 2026-07-21 14:05 UTC Run
prod-eu ✅ Deployed 2026-07-21 14:07 UTC Run

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants