# Org to Instance Rename Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Rename the tenant entity from `Org` to `Instance` everywhere — Go types, collections, the `org_id` field on every document, REST routes and UI copy — with a migration that only ever renames and never deletes.

**Architecture:** A code rename driven from the `shared` module outward, plus migration `0004_org_to_instance` that renames two collections and `$rename`s one field across every scoped collection. The migration verifies document counts before recording its marker, so a partial run retries rather than half-completing. sitesvc refuses to boot against an unmigrated database.

**Tech Stack:** Go 1.26, MongoDB driver v2, Next.js 16, Docker.

**No automated tests.** This moves the tenant isolation key across 17 collections. With no test suite, the production-snapshot rehearsal in Task 8 is not optional and not a formality — it is the only evidence that no tenant's data was orphaned. Do not deploy without completing every step of it, including the rollback rehearsal.

## Global Constraints

- **Depends on plan 0a being merged and deployed.** Do not start otherwise; this plan assumes one definition of `Org`, `User` and `Settings` in `shared`.
- Take a database backup before deploying. The inverse rename is the first recovery path; the backup is the second.
- **The migration renames. It never deletes, drops or unsets** — the one exception is dropping indexes keyed on the old field name, which touches no documents.
- No behaviour changes. Same permissions, same responses, same data.
- The `agent` module and `agent-release.yml` are not touched by any task.
- REST route renames are breaking and ship in the same release as the frontend. No compatibility aliases.
- After Task 4, `grep -rn '"org_id"' shared/ server/ sitesvc/` must return hits **only** in `migrate_instance.go`.

## The naming map

Apply exactly. Every task references this table.

| Today                                                                       | After                                                                                                     |
| --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| collection `orgs`                                                           | `instances`                                                                                               |
| collection `org_oidc`                                                       | `instance_oidc`                                                                                           |
| field `org_id`                                                              | `instance_id`                                                                                             |
| `models.Org`                                                                | `models.Instance`                                                                                         |
| `Org.OrgID`                                                                 | `Instance.InstanceID`                                                                                     |
| `User.OrgID`, `Settings.OrgID`, every `OrgID` field                         | `InstanceID`                                                                                              |
| `provision.CreateOrg`, `RollbackOrg`                                        | `CreateInstance`, `RollbackInstance`                                                                      |
| `services/orgs.go`                                                          | `services/instances.go`                                                                                   |
| `GetOrg`, `GetOrgBySlug`, `ListOrgIDs`, `CountOrgs`, `FirstOrg`, `AdoptOrg` | `GetInstance`, `GetInstanceBySlug`, `ListInstanceIDs`, `CountInstances`, `FirstInstance`, `AdoptInstance` |
| `CountOrgUsers`, `GetUserInOrg`                                             | `CountInstanceUsers`, `GetUserInInstance`                                                                 |
| `services/org_oidc.go`                                                      | `services/instance_oidc.go`                                                                               |
| `auth/orghost.go`                                                           | `auth/instancehost.go`                                                                                    |
| `/api/org/users`, `/api/org/oidc`                                           | `/api/instance/users`, `/api/instance/oidc`                                                               |
| session field `org_id`                                                      | `instance_id`                                                                                             |
| `GET /auth/me` fields `org_id`, `org`                                       | `instance_id`, `instance`                                                                                 |
| `PendingSignup.OrgName` / `bson:"org_name"`                                 | `InstanceName` / `bson:"instance_name"`                                                                   |
| UI copy "Organisation"/"Organization" (tenant)                              | "Instance"                                                                                                |
| UI copy "Organisation" (customer, marketing site)                           | "Account"                                                                                                 |

---

## File Structure

**Created:**

| Path                                           | Responsibility                                           |
| ---------------------------------------------- | -------------------------------------------------------- |
| `shared/models/instance.go`                    | `Instance` (replaces `org.go`)                           |
| `shared/provision/instance.go`                 | `CreateInstance`, `RollbackInstance` (replaces `org.go`) |
| `server/internal/services/instances.go`        | replaces `orgs.go`                                       |
| `server/internal/services/instance_oidc.go`    | replaces `org_oidc.go`                                   |
| `server/internal/auth/instancehost.go`         | replaces `orghost.go`                                    |
| `server/internal/services/migrate_instance.go` | migration `0004`, `ScopedCollections`, boot assertion    |
| `server/cmd/rename-rollback/main.go`           | one-shot inverse rename                                  |

**Modified:** `shared/models/user.go`, `shared/models/settings.go`, `shared/provision/user.go`, `shared/indexes/indexes.go`, every file in `server/internal/` referencing a renamed symbol, `server/cmd/main.go`, `sitesvc/internal/{models,store,api}`, `sitesvc/cmd/main.go`, `web/` and `site/` sources.

---

### Task 1: Rename in the shared module

Everything downstream depends on these names, so they change first.

**Files:**

- Rename: `shared/models/org.go` → `shared/models/instance.go`
- Rename: `shared/provision/org.go` → `shared/provision/instance.go`
- Modify: `shared/models/user.go`, `shared/models/settings.go`, `shared/provision/user.go`, `shared/indexes/indexes.go`

**Interfaces:**

- Consumes: the plan 0a API
- Produces:
    - `models.Instance` with field `InstanceID string` and tag `bson:"instance_id"`
    - `models.User.InstanceID`, `models.Settings.InstanceID`
    - `provision.CreateInstance(ctx, db, name) (*models.Instance, error)`
    - `provision.RollbackInstance(ctx, db, instanceID) error`
    - `provision.CreateUser(ctx, db, instanceID, email, password, role, authSource)` — first argument renamed, signature otherwise unchanged
    - `provision.CreateUserWithHash(ctx, db, instanceID, email, passwordHash, role, authSource)`
    - `indexes.EnsureCoreIndexes` — now indexes `instances.slug`

- [ ] **Step 1: Rename the Instance model**

```bash
cd c:/Work/Repos/vantage
git mv shared/models/org.go shared/models/instance.go
```

Replace its contents with:

```go
// Package models holds the MongoDB documents written by more than one Vantage
// service.
package models

import (
	"time"

	"go.mongodb.org/mongo-driver/v2/bson"
)

// Instance is one deployment of Vantage: its own subdomain, users, servers,
// keys, workflows, monitors and secrets. It is the unit a licence attaches to.
//
// A paying customer may hold several. That grouping is called an Account and
// lives only in the admin control plane — this service never sees it.
type Instance struct {
	ID         bson.ObjectID `bson:"_id,omitempty" json:"_id,omitempty"`
	InstanceID string        `bson:"instance_id" json:"instance_id"`
	Name       string        `bson:"name" json:"name"`
	Slug       string        `bson:"slug" json:"slug"`
	CreatedAt  time.Time     `bson:"created_at" json:"created_at"`
}
```

- [ ] **Step 2: Rename the fields on User and Settings**

In `shared/models/user.go`, change the one field:

```go
	InstanceID   string        `bson:"instance_id" json:"instance_id"`
```

In `shared/models/settings.go`, change the one field:

```go
	InstanceID string          `bson:"instance_id" json:"instance_id"`
```

- [ ] **Step 3: Rename the provisioning functions**

```bash
cd c:/Work/Repos/vantage
git mv shared/provision/org.go shared/provision/instance.go
```

Replace its contents with:

```go
package provision

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/google/uuid"
	"gitea.hostxtra.co.uk/mrhid6/vantage/shared/models"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/mongo"
)

// ErrNameRejected wraps every reason a name cannot become an instance.
var ErrNameRejected = errors.New("organisation name rejected")

const maxSlugAttempts = 50

// CreateInstance inserts an instance under the first free slug derived from name.
//
// The count-then-insert loop is racy on its own. It is safe only because
// instances.slug carries a unique index: a lost race surfaces as a duplicate-key
// error, which we treat as "that slug is taken" and retry. Do not remove the
// duplicate-key branch, and do not remove the index.
func CreateInstance(ctx context.Context, db *mongo.Database, name string) (*models.Instance, error) {
	base, err := BaseSlug(name)
	if err != nil {
		return nil, fmt.Errorf("%w: %s", ErrNameRejected, err.Error())
	}

	for attempt := 1; attempt <= maxSlugAttempts; attempt++ {
		slug := NextSlug(base, attempt)

		n, err := db.Collection("instances").CountDocuments(ctx, bson.M{"slug": slug})
		if err != nil {
			return nil, err
		}
		if n > 0 {
			continue
		}

		inst := models.Instance{
			InstanceID: uuid.NewString(),
			Name:       name,
			Slug:       slug,
			CreatedAt:  time.Now().UTC(),
		}
		if _, err := db.Collection("instances").InsertOne(ctx, inst); err != nil {
			if mongo.IsDuplicateKeyError(err) {
				continue // lost the race; try the next slug
			}
			return nil, err
		}
		return &inst, nil
	}
	return nil, fmt.Errorf("%w: could not find a free slug for %q", ErrNameRejected, name)
}

// RollbackInstance deletes an instance that has no users.
//
// It refuses an instance that has users. Rollback exists to clean up a
// half-finished signup, and an instance with users is not half-finished.
func RollbackInstance(ctx context.Context, db *mongo.Database, instanceID string) error {
	n, err := db.Collection("users").CountDocuments(ctx, bson.M{"instance_id": instanceID})
	if err != nil {
		return err
	}
	if n > 0 {
		return fmt.Errorf("refusing to roll back instance %s: it has %d user(s)", instanceID, n)
	}
	_, err = db.Collection("instances").DeleteOne(ctx, bson.M{"instance_id": instanceID})
	return err
}
```

The `ErrNameRejected` text keeps saying "organisation name" — that message is about the _customer's_ organisation name as typed into a form, which is still the right word. Task 7 revisits UI copy; this is not it.

- [ ] **Step 4: Rename the user provisioning parameter**

In `shared/provision/user.go`, change both signatures and the struct literal:

```go
func CreateUser(ctx context.Context, db *mongo.Database, instanceID, email, password, role, authSource string) (*models.User, error) {
	var hash string
	if password != "" {
		b, err := bcrypt.GenerateFromPassword([]byte(password), BcryptCost)
		if err != nil {
			return nil, err
		}
		hash = string(b)
	}
	return CreateUserWithHash(ctx, db, instanceID, email, hash, role, authSource)
}

func CreateUserWithHash(ctx context.Context, db *mongo.Database, instanceID, email, passwordHash, role, authSource string) (*models.User, error) {
	email = strings.ToLower(strings.TrimSpace(email))
	if email == "" {
		return nil, fmt.Errorf("email required")
	}
	if !models.ValidRole(role) {
		return nil, fmt.Errorf("invalid role %q", role)
	}

	u := &models.User{
		UserID:       uuid.NewString(),
		InstanceID:   instanceID,
		Email:        email,
		PasswordHash: passwordHash,
		Role:         role,
		AuthSource:   authSource,
		CreatedAt:    time.Now().UTC(),
	}
	if _, err := db.Collection("users").InsertOne(ctx, u); err != nil {
		if mongo.IsDuplicateKeyError(err) {
			return nil, ErrEmailTaken
		}
		return nil, err
	}
	return u, nil
}
```

- [ ] **Step 5: Point the index at the renamed collection**

In `shared/indexes/indexes.go`, change the second index block:

```go
	if _, err := db.Collection("instances").Indexes().CreateOne(ctx, mongo.IndexModel{
		Keys:    bson.D{{Key: "slug", Value: 1}},
		Options: options.Index().SetUnique(true),
	}); err != nil {
		return fmt.Errorf("instances.slug index: %w", err)
	}
```

- [ ] **Step 6: Build and confirm no stale references**

```bash
cd c:/Work/Repos/vantage/shared
go build ./... && go vet ./...
cd ..
grep -rn "org_id\|OrgID\|\"orgs\"\|CreateOrg\|RollbackOrg" shared/
```

Expected: no output from any of them.

- [ ] **Step 7: Commit**

```bash
git add shared/
git commit -m "refactor(shared): rename Org to Instance"
```

---

### Task 2: The scoped-collection list and the boot assertion

A code constant, not a runbook list. A collection missing from it is a collection whose tenant key never gets renamed — and with no test suite, the boot assertion is what catches that.

**Files:**

- Create: `server/internal/services/migrate_instance.go`

**Interfaces:**

- Consumes: nothing
- Produces:
    - `var ScopedCollections []string`
    - `func AssertNoScopedCollectionMissed(ctx context.Context, db *mongo.Database) error`

- [ ] **Step 1: Write the list and the assertion**

Create `server/internal/services/migrate_instance.go`:

```go
package services

import (
	"context"
	"fmt"

	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/mongo"
)

// ScopedCollections lists every collection carrying the tenant key.
//
// Migration 0004 renames org_id to instance_id in each. A collection missing
// from this list keeps the old field name and becomes invisible to every scoped
// query — so this list is load-bearing, not documentation.
//
// AssertNoScopedCollectionMissed checks at boot that nothing outside this list
// holds an org_id.
//
// The migrations collection is deliberately absent: it is not tenant-scoped.
// The two renamed collections appear under their post-rename names, because the
// migration renames the collections before it renames the field.
var ScopedCollections = []string{
	"instances",
	"servers",
	"keys",
	"assignments",
	"users",
	"instance_oidc",
	"settings",
	"secrets",
	"workflows",
	"workflow_steps",
	"workflow_runs",
	"monitors",
	"incidents",
	"monitor_rollups",
	"notification_channels",
	"console_sessions",
	"audit_logs",
}

// AssertNoScopedCollectionMissed reports any collection holding an org_id that
// ScopedCollections does not know about. A hit means a collection was added
// without being added to the list, and its tenant key was never renamed.
func AssertNoScopedCollectionMissed(ctx context.Context, db *mongo.Database) error {
	known := map[string]bool{}
	for _, c := range ScopedCollections {
		known[c] = true
	}

	names, err := db.ListCollectionNames(ctx, bson.M{})
	if err != nil {
		return fmt.Errorf("list collections: %w", err)
	}

	for _, n := range names {
		if known[n] {
			continue
		}
		count, err := db.Collection(n).CountDocuments(ctx, bson.M{"org_id": bson.M{"$exists": true}})
		if err != nil {
			return fmt.Errorf("count %s: %w", n, err)
		}
		if count > 0 {
			return fmt.Errorf("collection %q holds %d document(s) with org_id but is not in ScopedCollections", n, count)
		}
	}
	return nil
}
```

- [ ] **Step 2: Cross-check the list against the documented collections**

```bash
cd c:/Work/Repos/vantage
grep -n "console_sessions\|audit_logs\|monitor_rollups\|notification_channels" claude.md
```

Expected: the collection list in `claude.md` matches `ScopedCollections`, allowing for `orgs`→`instances` and `org_oidc`→`instance_oidc`. Any collection in the docs but not in the list must be added, and vice versa. `migrations` is the only intentional omission.

- [ ] **Step 3: Cross-check against a live database**

Against a restored production snapshot, in `mongosh`:

```javascript
use vantage_snapshot
db.getCollectionNames().sort().forEach(function (c) {
  var n = db.getCollection(c).countDocuments({org_id: {$exists: true}});
  if (n > 0) print(c + " " + n);
})
```

Expected: exactly the 17 collections in `ScopedCollections`, with `orgs` and `org_oidc` in place of their renamed forms. **Anything else in that output must be added to the list before continuing.**

- [ ] **Step 4: Build and commit**

```bash
cd c:/Work/Repos/vantage/server && go build ./...
cd ..
git add server/internal/services/migrate_instance.go
git commit -m "chore(server): add ScopedCollections and the boot assertion"
```

---

### Task 3: Migration 0004

The risky part of the plan.

**Files:**

- Modify: `server/internal/services/migrate_instance.go`

**Interfaces:**

- Consumes: `ScopedCollections`
- Produces: `func MigrateOrgToInstance(ctx context.Context, db *mongo.Database) error`

Takes an explicit `*mongo.Database` rather than using the package-level `db.Database`, so it can be pointed at a snapshot database during rehearsal. `server/cmd/main.go` passes `db.Database`.

- [ ] **Step 1: Write the migration**

Append to `server/internal/services/migrate_instance.go`:

```go
// collectionRenames maps the two collections whose names change. Ordered so the
// migration is deterministic.
var collectionRenames = []struct{ from, to string }{
	{"orgs", "instances"},
	{"org_oidc", "instance_oidc"},
}

// MigrateOrgToInstance renames the tenant key from org_id to instance_id.
//
// It only ever renames documents. It never deletes, drops or unsets one, so a
// bad deploy is recovered by running the inverse rename (cmd/rename-rollback)
// rather than by restoring a backup.
//
// The steps are not atomic across collections — multi-document transactions
// would require a replica set, which self-hosted installs do not guarantee.
// Instead every step is safely repeatable: a collection rename is skipped when
// the source is already gone, and $rename matches nothing on a document that
// has already been renamed. A run that fails partway is fixed by running it
// again.
func MigrateOrgToInstance(ctx context.Context, db *mongo.Database) error {
	names, err := db.ListCollectionNames(ctx, bson.M{})
	if err != nil {
		return fmt.Errorf("list collections: %w", err)
	}
	exists := map[string]bool{}
	for _, n := range names {
		exists[n] = true
	}

	// Step 1: rename the collections.
	for _, r := range collectionRenames {
		switch {
		case !exists[r.from]:
			// Nothing to rename: either already done or never existed.
			continue
		case exists[r.to]:
			return fmt.Errorf("cannot rename %s to %s: both exist; resolve by hand", r.from, r.to)
		}
		cmd := bson.D{
			{Key: "renameCollection", Value: db.Name() + "." + r.from},
			{Key: "to", Value: db.Name() + "." + r.to},
		}
		if err := db.Client().Database("admin").RunCommand(ctx, cmd).Err(); err != nil {
			return fmt.Errorf("rename %s to %s: %w", r.from, r.to, err)
		}
		log.Printf("0004: renamed collection %s to %s", r.from, r.to)
	}

	// Step 2: rename the field.
	for _, c := range ScopedCollections {
		res, err := db.Collection(c).UpdateMany(ctx,
			bson.M{"org_id": bson.M{"$exists": true}},
			bson.M{"$rename": bson.M{"org_id": "instance_id"}},
		)
		if err != nil {
			return fmt.Errorf("rename org_id in %s: %w", c, err)
		}
		if res.ModifiedCount > 0 {
			log.Printf("0004: %s renamed %d document(s)", c, res.ModifiedCount)
		}
	}

	// Step 3: verify before anyone records a marker. Any mismatch aborts, and
	// the migration is re-run rather than marked done.
	for _, c := range ScopedCollections {
		total, err := db.Collection(c).CountDocuments(ctx, bson.M{})
		if err != nil {
			return fmt.Errorf("count %s: %w", c, err)
		}
		if total == 0 {
			continue
		}

		stale, err := db.Collection(c).CountDocuments(ctx, bson.M{"org_id": bson.M{"$exists": true}})
		if err != nil {
			return fmt.Errorf("count stale in %s: %w", c, err)
		}
		if stale != 0 {
			return fmt.Errorf("%s still has %d document(s) with org_id; migration incomplete", c, stale)
		}

		scoped, err := db.Collection(c).CountDocuments(ctx, bson.M{"instance_id": bson.M{"$exists": true}})
		if err != nil {
			return fmt.Errorf("count scoped in %s: %w", c, err)
		}
		if scoped != total {
			return fmt.Errorf("%s has %d document(s) but only %d carry instance_id", c, total, scoped)
		}
	}

	// Step 4: indexes keyed on the old field name now point at a field that no
	// longer exists. Drop them; the boot-time index builders recreate the
	// current ones. Dropping an index touches no documents.
	for _, c := range ScopedCollections {
		cur, err := db.Collection(c).Indexes().List(ctx)
		if err != nil {
			return fmt.Errorf("list indexes on %s: %w", c, err)
		}
		var specs []bson.M
		if err := cur.All(ctx, &specs); err != nil {
			return fmt.Errorf("decode indexes on %s: %w", c, err)
		}
		for _, s := range specs {
			name, _ := s["name"].(string)
			if name == "_id_" {
				continue
			}
			keys, ok := s["key"].(bson.M)
			if !ok {
				continue
			}
			if _, keyed := keys["org_id"]; !keyed {
				continue
			}
			if _, err := db.Collection(c).Indexes().DropOne(ctx, name); err != nil {
				return fmt.Errorf("drop index %s on %s: %w", name, c, err)
			}
			log.Printf("0004: dropped stale index %s on %s", name, c)
		}
	}

	log.Printf("0004: verified %d collection(s)", len(ScopedCollections))
	return nil
}
```

Add `"log"` to the import block.

- [ ] **Step 2: Build**

Run: `cd server && go build ./... && go vet ./...`
Expected: no output.

- [ ] **Step 3: Dry-run against a disposable copy**

Do not wait for Task 8 to find out whether this works. Make a scratch copy now:

```bash
mongodump --uri mongodb://localhost:27017 --db vantage --out /tmp/dump
mongorestore --uri mongodb://localhost:27017 --db vantage_dryrun /tmp/dump/vantage
```

Write a throwaway runner at `server/cmd/migratecheck/main.go`:

```go
package main

import (
	"context"
	"log"
	"time"

	"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/services"
	"go.mongodb.org/mongo-driver/v2/mongo"
	"go.mongodb.org/mongo-driver/v2/mongo/options"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
	defer cancel()

	c, err := mongo.Connect(options.Client().ApplyURI("mongodb://localhost:27017"))
	if err != nil {
		log.Fatal(err)
	}
	db := c.Database("vantage_dryrun")

	if err := services.MigrateOrgToInstance(ctx, db); err != nil {
		log.Fatalf("migration: %v", err)
	}
	if err := services.AssertNoScopedCollectionMissed(ctx, db); err != nil {
		log.Fatalf("assertion: %v", err)
	}
	log.Println("dry run complete")
}
```

Run: `cd server && go run ./cmd/migratecheck`

Expected: per-collection rename counts, `0004: verified 17 collection(s)`, `dry run complete`, no error.

- [ ] **Step 4: Confirm idempotency by hand**

Run it a second time: `cd server && go run ./cmd/migratecheck`

Expected: completes with no error and no rename counts (nothing left to rename). If it errors, the migration is not repeatable and a partial production run would be unrecoverable — fix before continuing.

- [ ] **Step 5: Confirm it resumes from a partial run**

Restore a fresh copy, then simulate a crash after the collection rename but before the field rename:

```bash
mongorestore --drop --uri mongodb://localhost:27017 --db vantage_dryrun /tmp/dump/vantage
```

```javascript
db.adminCommand({ renameCollection: "vantage_dryrun.orgs", to: "vantage_dryrun.instances" });
```

Then: `cd server && go run ./cmd/migratecheck`

Expected: completes cleanly and every document in `instances` carries `instance_id`.

Confirm:

```javascript
use vantage_dryrun
db.instances.countDocuments({instance_id: {$exists: true}})
db.instances.countDocuments({org_id: {$exists: true}})
```

Expected: the first equals the total document count; the second is `0`.

- [ ] **Step 6: Delete the throwaway runner and commit**

```bash
rm -rf server/cmd/migratecheck
git add server/internal/services/migrate_instance.go
git commit -m "feat(server): add migration 0004 org_id to instance_id"
```

Keep `/tmp/dump` — Task 8 needs it.

---

### Task 4: Rename the control plane

Mechanical, wide, and guarded by the compiler at every step. Work file by file and let `go build` drive.

**Files:**

- Rename: `server/internal/services/orgs.go` → `instances.go`
- Rename: `server/internal/services/org_oidc.go` → `instance_oidc.go`
- Rename: `server/internal/auth/orghost.go` → `instancehost.go`
- Rename: `server/internal/models/org.go` → `instance.go`
- Modify: every file under `server/internal/` referencing a renamed symbol
- Modify: `server/cmd/main.go`

**Interfaces:**

- Consumes: Task 1's shared API, Tasks 2–3's migration
- Produces: `services.GetInstance`, `GetInstanceBySlug`, `CreateInstance`, `ListInstanceIDs`, `CountInstances`, `FirstInstance`, `AdoptInstance`, `CountInstanceUsers`, `GetUserInInstance` — all with the same signatures as their `Org` predecessors

- [ ] **Step 1: Rename the files**

```bash
cd c:/Work/Repos/vantage
git mv server/internal/services/orgs.go server/internal/services/instances.go
git mv server/internal/services/org_oidc.go server/internal/services/instance_oidc.go
git mv server/internal/auth/orghost.go server/internal/auth/instancehost.go
git mv server/internal/models/org.go server/internal/models/instance.go
```

- [ ] **Step 2: Apply the identifier renames**

Order matters — longest identifiers first, so shorter rules do not corrupt them.

```bash
cd c:/Work/Repos/vantage/server
FILES=$(find . -name '*.go' -not -path './vendor/*')

sed -i 's/CountOrgUsers/CountInstanceUsers/g' $FILES
sed -i 's/GetUserInOrg/GetUserInInstance/g' $FILES
sed -i 's/GetOrgBySlug/GetInstanceBySlug/g' $FILES
sed -i 's/ListOrgIDs/ListInstanceIDs/g' $FILES
sed -i 's/CountOrgs/CountInstances/g' $FILES
sed -i 's/FirstOrg/FirstInstance/g' $FILES
sed -i 's/AdoptOrg/AdoptInstance/g' $FILES
sed -i 's/CreateOrg/CreateInstance/g' $FILES
sed -i 's/GetOrg/GetInstance/g' $FILES
sed -i 's/OrgID/InstanceID/g' $FILES
sed -i 's/orgID/instanceID/g' $FILES
sed -i 's/models\.Org\b/models.Instance/g' $FILES
sed -i 's/"org_id"/"instance_id"/g' $FILES
sed -i 's/"orgs"/"instances"/g' $FILES
sed -i 's/"org_oidc"/"instance_oidc"/g' $FILES
```

- [ ] **Step 3: Restore the migration file**

`sed` will have rewritten the migration's deliberate references to the old names, which would make it a no-op that silently does nothing.

```bash
cd c:/Work/Repos/vantage
git checkout server/internal/services/migrate_instance.go
git diff server/internal/services/migrate_instance.go
```

Expected: no diff. The migration must keep reading `org_id`, `"orgs"` and `"org_oidc"` — that is its entire job.

- [ ] **Step 4: Read the rest of the diff**

`sed` is a blunt instrument. Run `git diff` and check for:

- Log and error strings now reading "instance" where "organisation" was the correct customer-facing word.
- Comments that no longer parse as English.
- Any `instanceID` variable that was previously an unrelated `orgID` in a different sense.

Fix anything that reads wrong before building.

- [ ] **Step 5: Alias the model**

Replace `server/internal/models/instance.go` with:

```go
package models

import shared "gitea.hostxtra.co.uk/mrhid6/vantage/shared/models"

// Instance is defined in the shared module because sitesvc and the admin
// control plane write the same documents.
type Instance = shared.Instance
```

- [ ] **Step 6: Build and fix what falls out**

Run: `cd server && go build ./... 2>&1 | head -40`

Expected initially: a list of errors. Work through them. Common ones:

- A server-only model still declaring `OrgID` — rename the field there too.
- `reservedSlugs` references in `AdoptInstance` — should already point at `provision.ReservedSlugs` from plan 0a.

Repeat until the build is clean.

- [ ] **Step 7: Rename the REST routes**

In the router setup under `server/internal/api/`, change the route group path:

```go
	instanceGroup := api.Group("/instance", auth.RequireRole(models.RoleOwner, models.RoleAdmin))
	instanceGroup.GET("/users", listUsers)
	instanceGroup.POST("/users", createUser)
	instanceGroup.PUT("/users/:id/role", updateUserRole)
	instanceGroup.DELETE("/users/:id", deleteUser)
	instanceGroup.GET("/oidc", getOIDC)
	instanceGroup.PUT("/oidc", putOIDC)
```

- [ ] **Step 8: Rename the session and /auth/me fields**

In the session struct and the `/auth/me` handler, rename the JSON keys `org_id` to `instance_id` and `org` to `instance`.

```bash
cd c:/Work/Repos/vantage
grep -rn '"org' server/internal/auth/ server/internal/api/
```

Expected: no output.

- [ ] **Step 9: Wire the migration and assertion into boot**

In `server/cmd/main.go`, after the existing migrations (`MigrateMissedOrgScopes`, itself renamed by Step 2 to `MigrateMissedInstanceScopes`) and before the index builders:

```go
	migCtx, migCancel := context.WithTimeout(context.Background(), 10*time.Minute)
	err = services.MigrateOrgToInstance(migCtx, db.Database)
	migCancel()
	if err != nil {
		log.Fatalf("instance rename migration failed: %v", err)
	}

	assertCtx, assertCancel := context.WithTimeout(context.Background(), 30*time.Second)
	err = services.AssertNoScopedCollectionMissed(assertCtx, db.Database)
	assertCancel()
	if err != nil {
		log.Fatalf("scoped collection check failed: %v", err)
	}
```

Both are fatal. A half-renamed database must not serve traffic.

- [ ] **Step 10: Build, vet, and confirm the rename is total**

```bash
cd c:/Work/Repos/vantage/server
go build ./... && go vet ./...
cd ..
grep -rn "org_id\|OrgID\|\"orgs\"\|/api/org" server/ | grep -v migrate_instance.go
```

Expected: no output from any of them.

- [ ] **Step 11: Commit**

```bash
git add server/
git commit -m "refactor(server): rename Org to Instance"
```

---

### Task 5: The inverse rename

The recovery path. Deliberately a separate one-shot command rather than a migration — the only reason to run it is a decision to revert the release, which is a human decision.

**Files:**

- Create: `server/cmd/rename-rollback/main.go`

**Interfaces:**

- Consumes: `services.ScopedCollections`
- Produces: a binary, not an API

- [ ] **Step 1: Write it**

Create `server/cmd/rename-rollback/main.go`:

```go
// Command rename-rollback reverses migration 0004.
//
// Run it only as part of a decision to revert the release that introduced the
// instance rename. It renames instance_id back to org_id and restores the two
// collection names. Like the migration, it only renames — it deletes no
// documents.
//
//	rename-rollback -uri mongodb://host:27017 -db vantage -confirm
package main

import (
	"context"
	"flag"
	"log"
	"time"

	"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/services"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/mongo"
	"go.mongodb.org/mongo-driver/v2/mongo/options"
)

func main() {
	uri := flag.String("uri", "mongodb://localhost:27017", "MongoDB URI")
	dbName := flag.String("db", "vantage", "database name")
	confirm := flag.Bool("confirm", false, "required; refuses to run without it")
	flag.Parse()

	if !*confirm {
		log.Fatal("refusing to run without -confirm")
	}

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
	defer cancel()

	client, err := mongo.Connect(options.Client().ApplyURI(*uri))
	if err != nil {
		log.Fatalf("connect: %v", err)
	}
	defer client.Disconnect(ctx)

	db := client.Database(*dbName)

	for _, c := range services.ScopedCollections {
		res, err := db.Collection(c).UpdateMany(ctx,
			bson.M{"instance_id": bson.M{"$exists": true}},
			bson.M{"$rename": bson.M{"instance_id": "org_id"}},
		)
		if err != nil {
			log.Fatalf("rename instance_id in %s: %v", c, err)
		}
		if res.ModifiedCount > 0 {
			log.Printf("%s: reverted %d document(s)", c, res.ModifiedCount)
		}
	}

	for _, r := range []struct{ from, to string }{
		{"instances", "orgs"},
		{"instance_oidc", "org_oidc"},
	} {
		cmd := bson.D{
			{Key: "renameCollection", Value: *dbName + "." + r.from},
			{Key: "to", Value: *dbName + "." + r.to},
		}
		if err := client.Database("admin").RunCommand(ctx, cmd).Err(); err != nil {
			log.Printf("rename %s to %s: %v (continuing)", r.from, r.to, err)
			continue
		}
		log.Printf("renamed collection %s to %s", r.from, r.to)
	}

	// Remove the marker so a redeployed new binary re-runs the migration.
	if _, err := db.Collection("migrations").DeleteOne(ctx, bson.M{"_id": "0004_org_to_instance"}); err != nil {
		log.Printf("clear migration marker: %v", err)
	}

	log.Println("rollback complete")
}
```

- [ ] **Step 2: Build and confirm the guard**

```bash
cd c:/Work/Repos/vantage/server
go build -o /tmp/rename-rollback ./cmd/rename-rollback
/tmp/rename-rollback
```

Expected: exits with `refusing to run without -confirm`.

- [ ] **Step 3: Rehearse the round trip on the dry-run database**

The dry-run database from Task 3 is already migrated. Revert it:

```bash
/tmp/rename-rollback -uri mongodb://localhost:27017 -db vantage_dryrun -confirm
```

Then in `mongosh`:

```javascript
use vantage_dryrun
db.getCollectionNames().sort()
db.orgs.countDocuments({org_id: {$exists: true}})
db.orgs.countDocuments({instance_id: {$exists: true}})
```

Expected: `orgs` and `org_oidc` are back, the first count equals the total document count, and the second is `0`.

- [ ] **Step 4: Commit**

```bash
git add server/cmd/rename-rollback
git commit -m "feat(server): add rename-rollback command for migration 0004"
```

---

### Task 6: Rename sitesvc and add its boot guard

sitesvc writes the same documents. A version skew where it writes `org_id` while the control plane reads `instance_id` creates tenants the application cannot see — so it refuses to start against an unmigrated database.

**Files:**

- Modify: `sitesvc/internal/models/models.go`, `sitesvc/internal/store/store.go`, `sitesvc/internal/api/*.go`, `sitesvc/cmd/main.go`

**Interfaces:**

- Consumes: Task 1's shared API
- Produces: `store.RequireMigratedDatabase(ctx context.Context) error`

- [ ] **Step 1: Rename the pending-signup field**

In `sitesvc/internal/models/models.go`, replace the `OrgName` field with:

```go
	InstanceName string        `bson:"instance_name"`
```

This collection is sitesvc-private and its records live at most 24 hours, so no migration is needed — an in-flight signup written under the old field name simply expires. That has a consequence for rollout; see the Rollout section.

- [ ] **Step 2: Apply the identifier renames**

```bash
cd c:/Work/Repos/vantage/sitesvc
FILES=$(find . -name '*.go')

sed -i 's/CreateOrg/CreateInstance/g' $FILES
sed -i 's/RollbackOrg/RollbackInstance/g' $FILES
sed -i 's/OrgName/InstanceName/g' $FILES
sed -i 's/orgName/instanceName/g' $FILES
sed -i 's/OrgID/InstanceID/g' $FILES
sed -i 's/sharedmodels\.Org\b/sharedmodels.Instance/g' $FILES
sed -i 's/\borg\b/instance/g' $FILES
sed -i 's/"org_id"/"instance_id"/g' $FILES
sed -i 's/"orgs"/"instances"/g' $FILES
sed -i 's/"org_name"/"instance_name"/g' $FILES
```

Read the diff. `\borg\b` is the aggressive rule — check it did not rename an unrelated local variable or mangle a comment. Also check the JSON request field in `internal/api`: the signup form posts `org_name`, so the marketing site's field name and the handler's binding tag must change together (Task 8 handles the site side).

- [ ] **Step 3: Write the boot guard**

Add to `sitesvc/internal/store/store.go`:

```go
// RequireMigratedDatabase refuses to start against a control-plane database
// that has not run migration 0004.
//
// Provisioning into `orgs` while the control plane reads `instances` would
// create tenants nobody can see — the exact skew failure the shared module was
// built to prevent. Failing to start is strictly better.
func RequireMigratedDatabase(ctx context.Context) error {
	names, err := database.ListCollectionNames(ctx, bson.M{})
	if err != nil {
		return fmt.Errorf("list collections: %w", err)
	}

	var hasInstances, hasOrgs bool
	for _, n := range names {
		switch n {
		case "instances":
			hasInstances = true
		case "orgs":
			hasOrgs = true
		}
	}

	// A brand-new database has neither. That is fine — whichever service starts
	// first creates `instances`.
	if !hasInstances && !hasOrgs {
		return nil
	}
	if !hasInstances {
		return errors.New("instances collection not found; deploy the control plane first")
	}
	return nil
}
```

- [ ] **Step 4: Call it at boot**

In `sitesvc/cmd/main.go`, immediately after the successful `store.Connect` and before `store.EnsureIndexes`:

```go
	guardCtx, guardCancel := context.WithTimeout(context.Background(), 10*time.Second)
	err := store.RequireMigratedDatabase(guardCtx)
	guardCancel()
	if err != nil {
		log.Fatalf("database check failed: %v", err)
	}
```

Note `main` currently uses `:=` for its first `err`; adjust to `=` or `:=` as the surrounding code requires — `go build` will tell you.

- [ ] **Step 5: Build and confirm**

```bash
cd c:/Work/Repos/vantage/sitesvc
go build ./... && go vet ./...
cd ..
grep -rn "org_id\|OrgID\|OrgName\|\"orgs\"\|org_name" sitesvc/
```

Expected: no output.

- [ ] **Step 6: Exercise the guard by hand**

Against the un-migrated copy — restore one:

```bash
mongorestore --drop --uri mongodb://localhost:27017 --db vantage_guardtest /tmp/dump/vantage
```

```bash
cd c:/Work/Repos/vantage/sitesvc
MONGO_URI=mongodb://localhost:27017/vantage_guardtest \
  PUBLIC_URL=http://localhost:8082 SITE_ORIGIN=http://localhost:3001 \
  SMTP_HOST=localhost SMTP_PORT=1025 SMTP_FROM=noreply@example.com \
  go run ./cmd
```

Expected: exits immediately with
`database check failed: instances collection not found; deploy the control plane first`

Then migrate that database (boot the server against it) and run sitesvc again.

Expected: starts normally and logs `sitesvc listening on :8082`.

- [ ] **Step 7: Commit**

```bash
git add sitesvc/
git commit -m "refactor(sitesvc): rename Org to Instance, refuse an unmigrated database"
```

---

### Task 7: Rename the control-plane frontend

Breaking route changes ship here, in the same release as Task 4.

**Files:**

- Modify: `web/lib/` API client, every route under `web/app/(app)/`, `web/components/`
- Rename: `web/app/(app)/settings/org/` → `web/app/(app)/settings/instance/`

**Interfaces:**

- Consumes: the renamed REST API from Task 4
- Produces: no API

- [ ] **Step 1: Find every reference and save the list**

```bash
cd c:/Work/Repos/vantage/web
grep -rn "org_id\|orgId\|/api/org\|Organisation\|Organization\|organisation\|organization\|settings/org" \
  --include=*.ts --include=*.tsx . | tee /tmp/web-org-refs.txt
wc -l /tmp/web-org-refs.txt
```

Every line must be resolved by Step 5.

- [ ] **Step 2: Rename the settings route**

```bash
cd c:/Work/Repos/vantage
git mv "web/app/(app)/settings/org" "web/app/(app)/settings/instance"
```

- [ ] **Step 3: Apply the renames**

```bash
cd c:/Work/Repos/vantage/web
FILES=$(grep -rl "org_id\|orgId\|/api/org\|Organisation\|Organization\|organisation\|organization\|settings/org" \
  --include=*.ts --include=*.tsx .)

sed -i 's|/api/org/|/api/instance/|g' $FILES
sed -i 's/org_id/instance_id/g' $FILES
sed -i 's/orgId/instanceId/g' $FILES
sed -i 's|settings/org|settings/instance|g' $FILES
sed -i 's/Organisation/Instance/g' $FILES
sed -i 's/Organization/Instance/g' $FILES
sed -i 's/organisation/instance/g' $FILES
sed -i 's/organization/instance/g' $FILES
```

- [ ] **Step 4: Read every copy change**

The copy substitutions are the ones most likely to produce nonsense. Run `git diff` and read **every changed user-facing string**. "Instance name too short" is fine; "Create your instance" is fine; a sentence that only worked with the old word needs rewriting rather than substituting. Fix them now — nobody else will.

- [ ] **Step 5: Confirm nothing was missed**

```bash
cd c:/Work/Repos/vantage/web
grep -rn "org_id\|orgId\|/api/org\|Organisation\|Organization\|organisation\|organization" \
  --include=*.ts --include=*.tsx .
```

Expected: no output.

- [ ] **Step 6: Build**

Run: `cd web && npm run build`
Expected: build succeeds with no type errors.

- [ ] **Step 7: Commit**

```bash
git add web/
git commit -m "refactor(web): rename Organisation to Instance"
```

---

### Task 8: Marketing site copy

The marketing site uses "organisation" for two different things, and only one becomes "Instance". Where it means the customer, it becomes **Account** — a word that now has a specific meaning in this system, and the marketing site is where a customer meets it first.

**Files:**

- Modify: `site/app/`, `site/components/`

- [ ] **Step 1: Find every reference**

```bash
cd c:/Work/Repos/vantage/site
grep -rn "organisation\|Organisation\|organization\|Organization\|org_name" \
  --include=*.ts --include=*.tsx --include=*.md .
```

- [ ] **Step 2: Decide each one individually**

**Do not bulk-substitute here.** For each hit, decide:

- The thing that gets a subdomain, holds servers and carries a licence → **Instance**
- The customer who pays and may hold several → **Account**

- [ ] **Step 3: Update the signup form**

This is the case that matters most and the only functional change in this task. The form currently asks for an "organisation name" and posts `org_name`; that name becomes the slug, so it is creating an **Instance**.

- Field label becomes "Instance name"
- Helper text explains it becomes the subdomain
- The posted JSON field becomes `instance_name`, matching the sitesvc handler renamed in Task 6 Step 2

- [ ] **Step 4: Build**

Run: `cd site && npm run build`
Expected: succeeds.

- [ ] **Step 5: Commit**

```bash
git add site/
git commit -m "refactor(site): distinguish Instance from Account in copy"
```

---

### Task 9: Full verification against a production snapshot

The gate. With no automated tests, this is the entire safety net. Nothing ships until every step passes against **restored production data**.

**Files:** none

- [ ] **Step 1: Build everything, including the agent**

```bash
cd c:/Work/Repos/vantage
(cd shared  && go build ./... && go vet ./...)
(cd server  && go build ./... && go vet ./...)
(cd sitesvc && go build ./... && go vet ./...)
(cd agent   && go build ./... && go vet ./...)
(cd web     && npm run build)
(cd site    && npm run build)
```

Expected: all succeed.

- [ ] **Step 2: Restore a production snapshot and record the baseline**

```bash
mongorestore --drop --uri mongodb://localhost:27017 --db vantage_snapshot /tmp/dump/vantage
```

In `mongosh`:

```javascript
use vantage_snapshot
db.getCollectionNames().sort().forEach(function (c) {
  print(c + " " + db.getCollection(c).countDocuments({}))
})
```

Save the output to `/tmp/baseline.txt`. Everything below compares against it.

- [ ] **Step 3: Record the per-tenant baseline**

Pick three real tenants from `db.orgs.find({}, {org_id: 1})`. For each:

```javascript
["<tenant-1>", "<tenant-2>", "<tenant-3>"].forEach(function (t) {
    print(
        t +
            " servers=" +
            db.servers.countDocuments({ org_id: t }) +
            " keys=" +
            db.keys.countDocuments({ org_id: t }) +
            " workflows=" +
            db.workflows.countDocuments({ org_id: t }) +
            " monitors=" +
            db.monitors.countDocuments({ org_id: t }) +
            " secrets=" +
            db.secrets.countDocuments({ org_id: t }) +
            " audit=" +
            db.audit_logs.countDocuments({ org_id: t }),
    );
});
```

Save to `/tmp/tenants-before.txt`.

- [ ] **Step 4: Run the migration**

Boot the server against the snapshot:

```bash
cd c:/Work/Repos/vantage/server
MONGO_URI=mongodb://localhost:27017 MONGO_DB=vantage_snapshot \
  GRPC_HOST=localhost:9090 go run ./cmd
```

Expected in the logs: `0004: renamed collection orgs to instances`, `0004: renamed collection org_oidc to instance_oidc`, per-collection rename counts, and `0004: verified 17 collection(s)`. No fatal error, and no `scoped collection check failed`.

- [ ] **Step 5: Compare document counts**

Re-run the Step 2 snippet and diff against the baseline:

```javascript
use vantage_snapshot
db.getCollectionNames().sort().forEach(function (c) {
  print(c + " " + db.getCollection(c).countDocuments({}))
})
```

Expected: identical counts, with exactly two names changed — `orgs` now `instances`, `org_oidc` now `instance_oidc`. **Any count difference is a stop-the-release defect.**

- [ ] **Step 6: The tenant isolation test**

For the same three tenants:

```javascript
["<tenant-1>", "<tenant-2>", "<tenant-3>"].forEach(function (t) {
    print(
        t +
            " servers=" +
            db.servers.countDocuments({ instance_id: t }) +
            " keys=" +
            db.keys.countDocuments({ instance_id: t }) +
            " workflows=" +
            db.workflows.countDocuments({ instance_id: t }) +
            " monitors=" +
            db.monitors.countDocuments({ instance_id: t }) +
            " secrets=" +
            db.secrets.countDocuments({ instance_id: t }) +
            " audit=" +
            db.audit_logs.countDocuments({ instance_id: t }),
    );
});
```

Expected: byte-identical to `/tmp/tenants-before.txt` apart from the field name in the query. **This is the step that proves tenant isolation survived.** Anything else stops the release.

- [ ] **Step 7: Confirm no document kept the old field**

```javascript
db.getCollectionNames().forEach(function (c) {
    var n = db.getCollection(c).countDocuments({ org_id: { $exists: true } });
    if (n > 0) print("STALE " + c + " " + n);
});
```

Expected: no output.

- [ ] **Step 8: Application smoke test**

Against the migrated snapshot, with `web` running:

1. Log in as a real user.
2. Confirm the servers list, keys, workflows, monitors, secrets and audit log all populate with the expected number of rows.
3. Open a server detail page; confirm inventory and assigned keys render.
4. Open `/settings/instance`; confirm members and OIDC settings load.
5. Confirm the sidebar and page copy say "Instance", never "Organisation".
6. Create something — a monitor is cheapest — and confirm it saves and appears.

- [ ] **Step 9: sitesvc against the migrated database**

Boot sitesvc against `vantage_snapshot` and complete a signup end to end: form, verification email via MailHog, link, then log into the control plane with the new credentials.

Expected: succeeds, and the new instance appears in `db.instances`.

- [ ] **Step 10: sitesvc against an unmigrated database**

```bash
mongorestore --drop --uri mongodb://localhost:27017 --db vantage_unmigrated /tmp/dump/vantage
```

Boot sitesvc against it.

Expected: exits immediately with
`database check failed: instances collection not found; deploy the control plane first`

- [ ] **Step 11: Rollback rehearsal**

**Mandatory.** Do not deploy without having done this.

```bash
mongorestore --drop --uri mongodb://localhost:27017 --db vantage_rollback /tmp/dump/vantage
```

Migrate it (boot the server against it), then revert:

```bash
/tmp/rename-rollback -uri mongodb://localhost:27017 -db vantage_rollback -confirm
```

Then:

1. Re-run the Step 2 count snippet against `vantage_rollback` and diff against `/tmp/baseline.txt`. Expected: identical, including the original collection names.
2. Re-run the Step 3 per-tenant snippet with `org_id`. Expected: identical to `/tmp/tenants-before.txt`.
3. Check out the **pre-release** commit, build the old server, and boot it against `vantage_rollback`. Expected: starts and serves normally.

- [ ] **Step 12: Confirm the agent is untouched**

```bash
cd c:/Work/Repos/vantage
git diff --name-only main -- agent/ .gitea/workflows/agent-release.yml
```

Expected: no output.

- [ ] **Step 13: Clean up and commit**

```javascript
["vantage_snapshot", "vantage_dryrun", "vantage_guardtest", "vantage_unmigrated", "vantage_rollback"].forEach(function (d) {
    db.getSiblingDB(d).dropDatabase();
});
```

```bash
git add -A
git commit -m "chore: verify instance rename against a production snapshot"
```

---

## Rollout

**Order matters. Read this before deploying.**

1. **Take a database backup.** Not optional.
2. Confirm the Task 9 rollback rehearsal was actually performed, not just read.
3. Deploy `server`, `web`, `site` and `sitesvc` from **one commit, together**.
4. The server boots first, runs migration `0004`, records the marker.
5. sitesvc may start before the server and exit with the guard message. It restarts and succeeds once the migration has run. **This is expected, not an incident.**

```bash
cd /opt/vantage && \
  docker compose -f docker-compose.yml -f docker-compose.site.yml pull && \
  docker compose -f docker-compose.yml -f docker-compose.site.yml up -d --remove-orphans
```

Expect a short API outage during the server restart while the migration runs. Agents are unaffected — they reconnect, and no gRPC message carries a tenant ID.

**In-flight signups are lost.** Any unverified signup recorded before the deploy carries `org_name` and will fail verification. There are at most 24 hours' worth. Either accept it, or wait for the collection to drain:

```javascript
db.site_pending_signups.countDocuments({});
```

Post-deploy checks:

1. Control-plane login works and the fleet dashboard populates.
2. A fresh signup completes end to end.
3. Server logs show `0004: verified 17 collection(s)` and no scoped-collection failure.
4. `db.servers.countDocuments({status: "active"})` matches the pre-deploy figure — agents are still reporting.

**If something is wrong:** deploy the previous images, run `rename-rollback -confirm`, and confirm the old binaries boot. Do not attempt a partial fix against a live half-renamed database.

## What this unblocks

Plan 1 (`licensing-core`) can define a licence payload bound to `instance_id`
without inventing a word the codebase does not use.
