Org-default handover: invotek → dashecorp
Context
Section titled “Context”The multi-tenancy proposal documents how tenant resolution and isolation work, but has carried two gaps that block activation of the dashecorp tenant:
- 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. - No documented step for the
invotek → dashecorphandover.invotektoday owns thedashecorporg as itsGithubOrg(a historical accident — invotek was the tenant-0 stand-in while only one tenant existed).dashecorpis the intended org-default. There is no recorded procedure for transferring ownership, andinvotek.GithubOrgafter 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:
invotekis not a GitHub org or user — bothGET /orgs/invotekandGET /users/invotekreturn 404. It owns no GitHub namespace; nothing on GitHub will ever resolve to it. Its role on the rig is lineage / tenant-0 legacy.dashecorpis a real GitHub org. ThedashecorpGitHub App installation on it hasinstallationId = 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, plusdashe-website) and rig-internal infrastructure repos (rig-*+infra). The two groups have different tenant ownership going forward.
Decision
Section titled “Decision”1. Org-default uniqueness rule
Section titled “1. Org-default uniqueness rule”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
RepoTenantrow. - Repos that should route elsewhere get an explicit
RepoTenantrow pointing at the other tenant. - A second active tenant claiming the same
GithubOrgis 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.
2. invotek → dashecorp handover
Section titled “2. invotek → dashecorp handover”| Tenant | GithubOrg (after) | installationId | Status | Model | Role |
|---|---|---|---|---|---|
dashecorp | dashecorp | 123727838 | active | Model B (first-party) | Owns the dashecorp org-default; routes the 8 dashe-* app/site repos |
invotek | null | (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):
| Group | Tenant | Mechanism |
|---|---|---|
8 dashe-* app/site repos (Cuti-E, Count-E, Drink-E, Fast-E, Heart-E, Nutri-E, Reward-E, dashe-website) | dashecorp | RepoTenant 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) | invotek | RepoTenant pin (permanent; no org-default would catch them once invotek.GithubOrg = null) |
Anything else in the dashecorp org without a pin | dashecorp | Org-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 pinView 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 pinStep-by-step:
| Step | Action | Interim behaviour for dashe-* event |
|---|---|---|
| 1 | Provision + 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). |
| 2 | Insert 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. |
| 3 | UPDATE 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. |
| 4 | UPDATE 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”.
4. Drain-don’t-migrate history
Section titled “4. Drain-don’t-migrate history”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.
Consequences
Section titled “Consequences”- Routing determinism. After the flip, every
dashe-*repo event has exactly one resolver answer (dashecorp); everyrig-*repo event has exactly one (invotek); ambiguity in the org-default is structurally impossible. - No data migration. Historical event/memory in
invotekstays there. The newrig_t_dashecorp_evtDB starts empty. Operators querying historicaldashe-*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 intoactive. - 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 todashecorprather than silently inheriting invotek.
Alternatives considered
Section titled “Alternatives considered”- 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 (RepoTenantpins) covers every real-world case without this ambiguity. - Activate
dashecorpfirst, then flip invotek. Rejected: violates the uniqueness rule at the intermediate state (both rows active with the sameGithubOrg). - 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
invotekafter 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.
References
Section titled “References”- Issue: rig-docs#324
- Epic: rig-conductor#1926, rig-conductor#1476
- Canonical operating guide: Multi-Tenancy proposal