Skip to content

Org-default handover: invotek → dashecorp

The multi-tenancy proposal documents how tenant resolution and isolation work, but has carried two gaps that block activation of the dashecorp tenant:

  1. No uniqueness rule for the org-default. Two tenants could in principle both claim GithubOrg = "dashecorp" as active. The proposal never said which wins, and the resolver would have non-deterministic precedence.
  2. No documented step for the invotek → dashecorp handover. invotek today owns the dashecorp org as its GithubOrg (a historical accident — invotek was the tenant-0 stand-in while only one tenant existed). dashecorp is the intended org-default. There is no recorded procedure for transferring ownership, and invotek.GithubOrg after the flip is undefined.

This decision fills both gaps before activation. The execution is gated on rig-conductor#1926 (the epic) and rig-conductor#1476.

Verifiable facts feeding the decision:

  • invotek is not a GitHub org or user — both GET /orgs/invotek and GET /users/invotek return 404. It owns no GitHub namespace; nothing on GitHub will ever resolve to it. Its role on the rig is lineage / tenant-0 legacy.
  • dashecorp is a real GitHub org. The dashecorp GitHub App installation on it has installationId = 123727838.
  • The dashecorp portfolio in the org is a mix of first-party app/site repos (the dashe-* family — Cuti-E, Count-E, Drink-E, Fast-E, Heart-E, Nutri-E, Reward-E, plus dashe-website) and rig-internal infrastructure repos (rig-* + infra). The two groups have different tenant ownership going forward.

One GitHub org = exactly one active tenant for the org-default.

The Tenant.GithubOrg column is the org-default claim. Among active tenants, no two rows may share the same non-null GithubOrg value. Suspended tenants may carry any value (including duplicates) — they don’t participate in resolution.

Carve-outs are expressed as per-repo RepoTenant pins, not as a second org claim:

  • An org-default tenant catches every repo in its org that has no RepoTenant row.
  • Repos that should route elsewhere get an explicit RepoTenant row pointing at the other tenant.
  • A second active tenant claiming the same GithubOrg is a configuration error.

Enforcement is in code: rig-conductor’s onboarding guard refuses a tenant transition into active if another active tenant already holds the same GithubOrg, and the deterministic resolver has a single well-defined precedence (RepoTenant row → Tenant.GithubOrg org-default → null). See the epic’s guard slice for the concrete check.

TenantGithubOrg (after)installationIdStatusModelRole
dashecorpdashecorp123727838activeModel B (first-party)Owns the dashecorp org-default; routes the 8 dashe-* app/site repos
invoteknull(unchanged)active(unchanged)Lineage-only / tenant-0 legacy; retained on its existing DB; owns no GitHub namespace

invotek keeps existing pre-cutover event/memory history on its legacy DB (see §4). It owns no GitHub namespace after the flip — GithubOrg = null is intentional and correct, because invotek is verifiably not a GitHub org or user (both endpoints 404). It still resolves for the 6 rig-internal repos via explicit RepoTenant pins.

Repo routing after the handover (concrete RepoTenant rows live in rig-conductor seeds — list here is for orientation, not source of truth):

GroupTenantMechanism
8 dashe-* app/site repos (Cuti-E, Count-E, Drink-E, Fast-E, Heart-E, Nutri-E, Reward-E, dashe-website)dashecorpRepoTenant pin during the suspended window; then the org-default catches them after the flip — pins remain as belt-and-braces
6 rig-internal repos (rig-conductor, rig-agent-runtime, rig-gitops, rig-docs, rig-memory-mcp, rig-tools)invotekRepoTenant pin (permanent; no org-default would catch them once invotek.GithubOrg = null)
Anything else in the dashecorp org without a pindashecorpOrg-default catch-all once active

3. Migration sequence — pin-first / activate-last (no mis-route)

Section titled “3. Migration sequence — pin-first / activate-last (no mis-route)”

The flip is ordered so that at every intermediate state, an event either resolves correctly or fails-to-null/skip — it can never mis-route to the wrong tenant’s DB.

sequenceDiagram
    autonumber
    participant Op as Operator
    participant DB as rig_control
    participant Res as Resolver (uncached)

    Op->>DB: 1. INSERT dashecorp (status='suspended', githubOrg='dashecorp', installationId=123727838)
    Op->>DB: 2. INSERT RepoTenant rows pinning 8 dashe-* repos → dashecorp (still suspended)
    Note over Res: Resolver: pin row matches suspended tenant → fail-to-null/skip<br/>(NEVER mis-routes — interim is safe)
    Op->>DB: 3. UPDATE invotek SET githubOrg = NULL
    Note over Res: dashe-* events: pins now the only signal (org-default gone)<br/>still skip while dashecorp suspended
    Op->>DB: 4. UPDATE dashecorp SET status = 'active' (LAST)
    Note over Res: dashe-* events route to dashecorp<br/>rig-* events route to invotek via pin
View Mermaid source
sequenceDiagram
    autonumber
    participant Op as Operator
    participant DB as rig_control
    participant Res as Resolver (uncached)

    Op->>DB: 1. INSERT dashecorp (status='suspended', githubOrg='dashecorp', installationId=123727838)
    Op->>DB: 2. INSERT RepoTenant rows pinning 8 dashe-* repos → dashecorp (still suspended)
    Note over Res: Resolver: pin row matches suspended tenant → fail-to-null/skip<br/>(NEVER mis-routes — interim is safe)
    Op->>DB: 3. UPDATE invotek SET githubOrg = NULL
    Note over Res: dashe-* events: pins now the only signal (org-default gone)<br/>still skip while dashecorp suspended
    Op->>DB: 4. UPDATE dashecorp SET status = 'active' (LAST)
    Note over Res: dashe-* events route to dashecorp<br/>rig-* events route to invotek via pin

Step-by-step:

StepActionInterim behaviour for dashe-* event
1Provision + seed dashecorp row in rig_control with status='suspended', githubOrg='dashecorp', installationId=123727838. Create the rig_t_dashecorp_evt DB.Still routes to invotek (org-default match wins for invotek).
2Insert RepoTenant rows pinning the 8 dashe-* repos to dashecorp while it is still suspended.Pin matches a suspended tenant → resolver fails to null and the event is skipped. Never mis-routed.
3UPDATE invotek SET githubOrg = NULL — invotek surrenders the org-default.dashe-* pin still points at a suspended tenant → still skipped. rig-* events still pin-route to invotek.
4UPDATE dashecorp SET status = 'active'last step.dashe-* events resolve to dashecorp and start writing to rig_t_dashecorp_evt.

The resolver is uncached (rig-conductor reads Tenant/RepoTenant on each delivery), so every row write takes effect on the very next event. There is no stale-cache window to drain.

Why this order, not the obvious alternative. If you flipped invotek.GithubOrg = null before inserting the RepoTenant pins, the org-default would already be gone but no pin would yet match, so dashe-* events would fall through to null and be skipped during the gap. If you activated dashecorp before flipping invotek, both rows would carry githubOrg = 'dashecorp' simultaneously — which is exactly the state the uniqueness rule in §1 forbids. Pin-first / activate-last is the only ordering that keeps the rule intact at every intermediate state and confines the interim window to “skip” rather than “mis-route”.

Pre-cutover dashe-* event streams and memory entries stay in invotek’s legacy DB. The dashecorp tenant starts fresh for those repos. This is by design:

  • Per-tenant event streams are append-only. Repartitioning historical events into a new DB is a destructive rewrite of immutable history.
  • Read consumers that need pre-cutover history can still query the invotek DB directly (it is not deleted).
  • The cost of repartitioning vastly exceeds the benefit — historical event lookups for dashe-* are rare, and the dashecorp tenant accumulates its own history from the cutover forward.

Operator pre-flip step: quiesce in-flight PRs/issues under the dashe-* repos before flipping. An issue that opens against an active RepoTenant-pinned-to-dashecorp-while-suspended row would skip, then on activation its successor events would land in dashecorp while its predecessor events are stranded in invotek. Drain → flip → resume avoids split histories on any single live work item.

  • Routing determinism. After the flip, every dashe-* repo event has exactly one resolver answer (dashecorp); every rig-* repo event has exactly one (invotek); ambiguity in the org-default is structurally impossible.
  • No data migration. Historical event/memory in invotek stays there. The new rig_t_dashecorp_evt DB starts empty. Operators querying historical dashe-* activity must know to point at the invotek DB for anything pre-cutover.
  • Interim window is safe. Between steps 2 and 4 the resolver may skip events. That is acceptable — webhooks are retry-driven (GitHub redelivers; the operator can replay) and skipping a small handful of events during a planned cutover is preferable to mis-routing them into the wrong tenant’s append-only stream where they would be permanent contamination.
  • Onboarding guard becomes load-bearing. The org-default uniqueness rule is enforced in code, not by operator discipline. A future tenant accidentally claiming GithubOrg = 'dashecorp' while active is rejected at the transition into active.
  • invotek’s role is now lineage-only on GitHub. It retains pinned ownership of the 6 rig-internal repos (which is real, ongoing work), but it no longer holds an org-default. New dashecorp-org repos default to dashecorp rather than silently inheriting invotek.
  • Two active tenants sharing GithubOrg = 'dashecorp'. Rejected: ambiguous resolver precedence, and the runtime would have to pick a “winner” by some non-obvious rule (creation order? alphabetic? configuration toggle?). The carve-out pattern (RepoTenant pins) covers every real-world case without this ambiguity.
  • Activate dashecorp first, then flip invotek. Rejected: violates the uniqueness rule at the intermediate state (both rows active with the same GithubOrg).
  • Migrate pre-cutover dashe-* history into the new DB. Rejected: per-tenant event streams are append-only; repartitioning is a destructive rewrite for marginal benefit. The drain-don’t-migrate pattern matches how the rig has handled prior tenant cutovers.
  • Delete invotek after the flip. Rejected: invotek still owns the 6 rig-internal repos and the historical event/memory history for everything pre-cutover. Deletion would orphan that data and the rig-internal repos would have no tenant.