Skip to content

feat(configstore): replace org default_team_id with duckgres_org_teams#969

Merged
benben merged 1 commit into
mainfrom
ben/configstore-org-teams
Jul 20, 2026
Merged

feat(configstore): replace org default_team_id with duckgres_org_teams#969
benben merged 1 commit into
mainfrom
ben/configstore-org-teams

Conversation

@benben

@benben benben commented Jul 20, 2026

Copy link
Copy Markdown
Member

Problem

An org maps to exactly one PostHog team via the default_team_id column on duckgres_orgs. Managed warehouses need to serve many teams per org (multi-environment orgs), and per-team state — which warehouse schema a team's data lives in, whether the team is enabled, whether its backfill runs — has no home in the config store.

Change

Migration 000024 introduces duckgres_org_teams: one row per (org, team) with

  • schema_name (required) — the warehouse schema the team's data lives in (team_<id> convention)
  • enabled (NOT NULL, default TRUE) — per-team serving switch
  • is_billing_team (nullable) — the team pull-based billing keys the org's usage buckets by; a partial unique index allows at most one billing row per org
  • backfill_enabled (nullable, tri-state)

The migration backfills one billing row per org from default_team_id (team_<id> schema) and drops the column. Down restores the column (nullable) from the billing row.

Billing keeps its contract, keyed off the billing team

  • ConfigStore.OrgDefaultTeamIDOrgBillingTeamID, same 0-when-missing contract; the snapshot loads Teams alongside Users/Warehouse so reads stay snapshot-backed
  • Compute meter and storage sampler resolve the billing team
  • Provisioning inserts/repoints the billing row (SetOrgBillingTeamTx: demote-then-upsert, ordered for the immediate partial-index check) and re-attributes unacked usage buckets in the same transaction, exactly as the column update used to
  • Repointing keeps the old team's row, demoted to non-billing (teams are members; billing is a mark)

API compat

The admin/provisioning wire field stays default_team_id: Org.DefaultTeamID is now a gorm:"-" view populated from the preloaded Teams association on reads, translated into the billing-team upsert on writes. Admin UI and e2e payloads unchanged (TS types extended with OrgTeam). Clearing is still rejected.

Tests

  • Migration tests against real Postgres: table shape, legacy-schema backfill, v8-upgrade replay, Up/Down, GORM-model/goose-schema metadata match (partial unique index + CASCADE FK included)
  • TestOrgBillingTeamID snapshot lookup (billing row → id, absent → 0, nil snapshot → 0)
  • Billing-repoint + cascade assertions in the admin/provisioning postgres tests; seeds insert team rows
  • Ran green: go build ./..., go vet ./... (+ -tags kubernetes), go test ./controlplane/..., admin + configstore suites against a real DB, admin UI tsc + vitest. The live-k8s e2e harness was not run (needs a cluster); its payloads use the unchanged wire field.

An org previously mapped to exactly one PostHog team via the
default_team_id column on duckgres_orgs. Managed warehouses need to
serve many teams per org, so migration 000024 introduces
duckgres_org_teams: one row per (org, team) carrying the warehouse
schema the team's data lives in ("team_<id>"), a per-team enabled
switch, a tri-state backfill_enabled flag, and an is_billing_team mark
constrained to at most one row per org by a partial unique index. The
migration backfills one billing row per org from default_team_id and
drops the column; Down restores the (nullable) column from the billing
row.

Billing keeps its contract, keyed off the billing team instead of the
column:

- ConfigStore.OrgDefaultTeamID becomes OrgBillingTeamID (same
  0-when-missing contract); the snapshot loads Teams alongside
  Users/Warehouse, so reads stay snapshot-backed.
- The compute meter and storage sampler resolve the billing team.
- Provisioning inserts/repoints the org's billing team row
  (SetOrgBillingTeamTx: demote the old row, upsert the new one) and
  re-attributes unacked usage buckets in the same transaction, exactly
  as the column update used to.
- The admin API keeps the default_team_id JSON field for wire/UI
  compat: Org.DefaultTeamID is now a gorm:"-" view populated from the
  preloaded Teams association on reads and translated into the
  billing-team upsert on org create/update. Clearing stays rejected;
  a changed value still re-attributes usage in the same transaction.

Seeds (local/kind/tenant-isolation) insert the team row instead of the
column. Migration tests cover the new table, the legacy-schema
backfill, the v8-upgrade replay, and the GORM-model/goose-schema
metadata match for duckgres_org_teams (including the partial unique
index and the CASCADE FK).
@github-actions

Copy link
Copy Markdown

Test Impact Plan

Deterministic summary of how this PR changes tests, CI runners, and coverage-risk signals.

Summary

Area Added Changed Deleted
Test files 0 12 0
E2E/journey files 0 0 0
Workflow files 0 0 0

Signals

  • Test cases: +1 / -0
  • Assertions: +22 / -2
  • Skips or known failures added: 0
  • Workflow continue-on-error added: 0
  • Workflow path filters added: 0
  • Test commands removed from justfile: 0
  • E2E/journey retry lines added: 0

Coverage risk: neutral or increased

No coverage-reduction warnings detected.

@benben
benben merged commit 8b5160f into main Jul 20, 2026
28 checks passed
@benben
benben deleted the ben/configstore-org-teams branch July 20, 2026 12:06
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.

1 participant