---
title: "Host Portal Governance"
slug: host-governance
section: Portals
group: Host Portal
status: stable
description: "How host authority, job order management, capacity controls, and operational phases are governed across the ecosystem"
last-updated: 2026-10-07
---

# Host Portal Governance

How host authority, job order management, capacity controls, and operational phases are governed across the ecosystem

## Current Host Portal Implementation (Development Baseline)

> **Scope correction**
>
> This page preserves the ecosystem’s host-governance and lifecycle contracts. The implementation notes in this section describe the `HostPortalPrototype` `development` branch reviewed on 2026-07-23. They correct only the portal-specific statements that conflict with current code; they do not transfer sponsor authority from the BackOffice Portal / Globus Hub to the Host Portal. [1] [2]

The Host Portal is a React/Vite client backed by an Express service. The current client mounts a host-company and camp experience for dashboard, applications/reserved participants, talent pool, participants, business/camp views, housing, contacts and addresses, company and arrival details, offers, cultural activities, documents, program resources, notifications, settings, job offers, activities, and administrator user management. The jobs, job approvals, job orders and job-order approvals routes (`/jobs`, `/job-approvals`, `/job-orders`, `/job-order-approvals`) are not registered in the client router at `development` HEAD (they fall through to Not Found) and their navigation sections are hidden; the page components still exist, so re-adding the four routes and the navigation sections restores them. The Transportation & Safety page has been retired and has no route. [3]

| Implementation area | Code-backed current state | Governance implication |
| --- | --- | --- |
| Browser-session access | The client uses Auth0 and reads a namespaced `roles` claim. Normal portal routes require `ROLES.USER`; user management requires `ROLES.ADMIN`. [3] | Browser access is role-gated and must not be described as anonymous API-key use. |
| API access | The Express JWT middleware validates Auth0 tokens and requires `read:hostcompanyportal:basic`. The `/api/v2/user` and `/api/v2/job-orders` modules explicitly apply it. [4] [5] [6] | The prior no-auth premise is not valid for the checked current code. Endpoint middleware must still be confirmed route by route. |
| Service domains | The modular backend mounts v2 dashboard, identity, reservations, participants, activity, talent pool, placements, hosts, profile, user management, notifications, positions, onboarding, job orders, and document domains. The documents domain covers company documents (`/api/v2/company-documents`), job-offer contracts (`/api/v2/job-offer-contracts`), application documents and document expiration; company-document and job-offer-contract uploads have a signed-URL path (`/uploads/initiate` then `/uploads/finalize`) and a server-side relay fallback (`/uploads/relay`, raw request body, 25 MB for company documents and 20 MB for job-offer contracts). [2] | This is the current service map; historical legacy/mocked route claims do not substitute for it. |
| Program-specific behavior | The client aggregates capability across every program assigned to the host (`hasCamp` / `hasNonCamp`) rather than a single selected program. Each Hub program is classified from its code and name; **Camp** and **Camp - Summer Work Travel** (camp_swt) both grant the camp-profile capability (My Camp, User Management), and unrecognized or ambiguous programs raise a program-capability notice. A host whose assignments are all camp-capability programs is redirected to the dashboard from the non-camp business surfaces (Housing, Contacts, Addresses, Arrival Information, Cultural Activities, OPL pages, Program Resources); a host with any non-camp assignment keeps them. Classification reads the Hub program code and name together: the Hub's Summer Work & Travel - Camp codes (`SWT_WP`, `SWT1`) carry no `CAMP_` prefix, and the Alliance Abroad variant is coded plain `SWT` and is recognized by name, so a bare `SWT` code with no name signal is reported as ambiguous and treated as non-camp. SWT-Camp hosts use the same Hub-backed My Camp Housing tab as Camp hosts (records scoped to the host organization, each naming its program) rather than the legacy business housing form. [3] | This is a current UI behavior, not a change to the underlying host-governance contract. |

### User Authentication versus Hub Integration

The browser/session layer above is distinct from the **server-to-server** Host Proxy contract retained later on this page. When a portal service calls the Hub on behalf of a host organization, the integration contract describes scoped server-side API-key usage and host/user-attribution headers. [7] Those headers must not be represented as the end-user browser login mechanism for the checked HPP branch.

### Current Source Pointers

| Subject | Canonical current reference |
| --- | --- |
| Host client journeys and client-role gate | [HostPortalPrototype `client/src/App.tsx`][3] |
| JWT permission verification | [HostPortalPrototype `server/middleware/jwt-auth.ts`][4] |
| Modular v2 backend composition | [HostPortalPrototype `server/routes-modular.ts`][2] |
| Route-level examples | [v2 identity router][5] and [v2 job-orders router][6] |
| Code-current endpoint map | [Host Portal Endpoint Reference](/docs/host-portal-endpoints) |

## Host Organization Lifecycle

Host organizations are the demand side of the placement equation. They create job orders describing positions they need filled, with built-in capacity management (position count, fill rates, pause/resume/cancel controls). The host lifecycle begins with a pending_verification phase where the organization is verified before activation, then moves through active operations to potential termination, with governance controls at every stage. **Program-type exception:** for program type **camp**, verification is no longer a prerequisite for operational activity — a camp host with an accepted operating agreement operates the portal while still pending_verification, and verification gates DS-2019 issuance only. See Camp Program Divergence — Verification Gates DS-2019 Only. All non-camp program types retain the front-loaded gate described in this section unchanged.

> **Architectural Boundary**
>
> The Host Portal does not own business authority. All enforcement, compliance, and placement confirmation controls are centralized in Globus. Globus decides. Host Portal reflects.

### Lifecycle Phases

Setup — pending verification & onboarding 
Active — host can create job orders & manage placements 
Warning — under review, limited operations 
Restricted — access suspended or terminated 
## Hub-Facing Host Proxy Contract

> **Not the browser-login model**
>
> The API-key and header patterns in this section document portal-service-to-Hub integration and user attribution. They do not supersede the current Auth0/JWT browser and `/api/v2` access boundary described above. [4] [7]

The Host Proxy contract supports direct and proxy service credentials for calls into Globus and records the acting host user for audit purposes. These modes describe how a portal service identifies the host context to Globus; they should be used alongside, not instead of, the host user’s browser-session authorization.

### Direct Auth

Direct Auth

An API key is tied directly to a partnerOrgId. This mode is used when a single host organization has its own dedicated API key. The organization is implicitly identified by the key itself — no additional headers are required to identify which org the request is for.

X-API-Key:

# Key is bound to partnerOrgId at creation time

### Host Proxy Auth

Host Proxy Auth

A centralized portal uses a single API key with the portal.host_proxy scope and passes the X-Host-Org-Id header to indicate which host organization the request is acting on behalf of. This is the standard mode for portals that serve multiple host organizations.

X-API-Key:

X-Host-Org-Id:

# Key has scope: portal.host_proxy

### User Attribution

User Attribution

The X-Host-User-Email header provides user-level attribution for audit trail purposes. This header identifies which specific user within the host organization performed an action. It is used alongside either Direct Auth or Host Proxy Auth to create a complete audit record.

X-API-Key:

X-Host-Org-Id:

X-Host-User-Email:

# All mutations are logged with user attribution

> **User Attribution Is Required for Mutations**
>
> All write operations (POST, PATCH, DELETE) from the Host Portal must include the X-Host-User-Email header. Read operations can omit it, but including it improves audit trail completeness. Requests without user attribution on mutation endpoints are rejected with 400.

## Host RBAC Permission Keys

*Status: the table below is partly a design spec. Live capabilities (`role_capabilities` migrations) are `host.org.view`/`manage`, `host.users.view`/`manage`, `host.profile.view`/`manage`, `host.positions.view`/`manage`, `host.reservations.view`/`manage`, `host.placements.view`/`manage` and `host.documents.view`/`manage`. The `host.dashboard.*`, `host.housing.*`, `host.notifications.*`, `host.invoices.*` and `host.billing.*` keys are not defined in code.*

Host users are assigned roles (host_admin, host_hiring_manager, host_finance, host_readonly) that map to fine-grained permission keys. These keys control what each user can access within the Host Portal:

| Permission Key | Description | Admin | Hiring Mgr | Finance | Read-Only |
| --- | --- | --- | --- | --- | --- |
| host.dashboard.view | View host dashboard with metrics and summary data | | | | |
| host.placements.view | View confirmed placements and participant information | | | | |
| host.placements.manage | Confirm arrivals, submit evaluations, manage placement operations | | | | |
| host.positions.view | View job orders and capacity status | | | | |
| host.positions.manage | Create, edit, and submit job orders | | | | |
| host.housing.view | View housing properties and unit assignments | | | | |
| host.housing.manage | Register properties, update units, assign participants to housing | | | | |
| host.users.view | View team members and their roles | | | | |
| host.users.manage | Invite, update, deactivate, and reactivate team members — issuing an invite mints a single-use activation token and fires host.user_invited (see /docs/host-user-invitation) | | | | |
| host.notifications.view | View notifications and alerts for the organization | | | | |
| host.profile.view | Read the Camp Overview tab (GET /api/partner/host/overview, GET /api/partner/host/addresses) — see /docs/host-camp-overview | | | | |
| host.profile.manage | Edit the Camp Overview tab (PATCH /api/partner/host/overview, all address mutations) — see /docs/host-camp-overview | | | | |
| host.documents.view | View host-side compliance and program documents shared into the portal | | | | |
| host.invoices.view | View invoices issued to this host org (TODO: underlying invoice surface is specced separately — cross-link pending) | | | | |
| host.invoices.manage | Acknowledge, dispute, and download invoices for this host org (TODO: underlying invoice surface is specced separately — cross-link pending) | | | | |
| host.billing.view | View billing profile, payment terms, and tax/remit-to information (TODO: underlying billing surface is specced separately — cross-link pending) | | | | |
| host.billing.manage | Edit billing profile, payment terms, and tax/remit-to information (TODO: underlying billing surface is specced separately — cross-link pending) | | | | |
| (no permission key) | Host organization notes (GET / POST /api/partner/host/:hostId/notes) are open to ANY authenticated Globus user — every host role, every proxied staff session, and admin-key staff. There is no per-permission gate; the only requirement is a valid partner session OR a valid x-admin-key header. See /docs/host-camp-overview#host-notes. | | | | |

> **host_finance is not the sponsor-side finance.* namespace**
>
> The host_finance column above is a Host Portal end-user role — billing / AP people inside the host org. It is not a finance/billing alias for the sponsor-side finance.* staff permissions documented on RBAC → Finance Permissions . The two namespaces do not share capabilities, do not share users, and a host_finance invitee never gains any sponsor-side permission.

## Host Organization Notes

Every host record carries a shared, append-only notes log used by account managers, compliance, and ops. Reads and creates are open to any authenticated Globus user (partner session OR x-admin-key staff); there is no per-permission gate. Every create writes an audit_logs row via the shared recordAudit() helper with entityType: host_organization_note and action: host_organization.note.created (.updated and .deleted are reserved for a follow-up edit/delete extension and are filterable in the Audit Console today). For the full contract (data model, endpoints, audit row shape, UI placement) see Camp Overview → Host Notes.

The live surface below is mounted on this governance page so reviewers can exercise the contract from the same surface that documents the RBAC table above. Pick any host from the directory to bind the panel — switching hosts re-binds the list and composer, so host A's notes never appear when host B is selected.

## Reservation Statuses

Reservations link applicants to job order positions. They follow a defined status flow from creation through to final resolution:

&rarr; 
Active / Hired — reservation is live or converted 
Reservation Extension Requested — partner requested more time 
Extended — Reservation Extension approved, new expiry set 
Expired / Cancelled — terminal states 
> **Reservation to Placement**
>
> A reservation in “hired” status means the applicant has been confirmed for the position. This triggers placement creation in Globus. Expired or cancelled reservations release the position back to the job order's available capacity.

> **Staff host-level holds**
>
> Globus staff can place a 7-day host-level hold on a participant (hold_type staff_host) from the applicant view. A bare host-level hold (no job order) debits no partner quota and reserves no job-order seat, and it cannot be confirmed into a placement (STAFF_HOLD_NOT_CONFIRMABLE). The target must be an active host with an active employer type (404 HOST_ORG_NOT_FOUND, 400 HOST_ORG_NOT_ACTIVE, 400 NOT_HOST_ORG) and must not be trial-restricted (422 HOST_ON_TRIAL_AGREEMENT). Any active hold on the same applicant, staff- or host-created, is rejected with 409 PARTICIPANT_ALREADY_RESERVED. A staff hold can later sit on a job order, and its hireable flag is decided server-side.

> **Auto-Expire — where the spec lives**
>
> The expired terminal state above is the only status flip the system performs on its own — partners and staff trigger every other transition explicitly. The full contract for the timer (per-program reservationTimeoutHours, default 72h), the state machine, the capacity-release semantics, the audit row written when a hold expires, and the in-portal countdown display all live on the Quota Model page. The host-portal Reservation Statuses table reads from those contracts — it does not redefine them. Quota Model &rarr; Reservation Hold Policy & Auto-Expire Quota Model &rarr; Expiration Audit Entry Shape — this is what populates the timestamp shown next to an expired row. Quota Model &rarr; Countdown Display Contract — the green / amber / red countdown rendered next to active rows. Batch Jobs &rarr; reservation-expiry-sweeper — the 15-min job that performs the flip.

## Host Adherence Model

> **Two distinct lifecycles, do not conflate**
>
> The host organization lifecycle (org record: created, verified, in-network, off-boarded) is separate from the host agreement lifecycle (agreement record: draft → pending_approval → active → probation → suspended → expired → terminated). The agreement lifecycle is what gates DS-2019 issuance and quota; the org lifecycle simply tracks whether the organization exists in the network. The states below are the agreement-lifecycle states, not the organization-lifecycle states.

The host adherence model is the host-side projection of the canonical agreement state machine defined on Partner Organizations → Partner Agreements . Lifecycle states, approvers, reason rules (≥10 characters), the auto-expiry job (≤15 minutes after endDate), and the 50% probation quota cut all come from the shared spine. Host-only behavior layered on top: a single active "standard" agreement per Brand+Program+Season is treated as the operating agreement and gates DS-2019 issuance.

### Lifecycle States (Canonical)

| State | Meaning | Portal Access | Transitions To |
| --- | --- | --- | --- |
| draft | Agreement record created, terms being prepared by sponsor staff | No portal access | pending_approval, terminated |
| pending_approval | Submitted for sponsor approval; verification + approver review in flight | No portal access | active, draft (returned for edits), terminated |
| active | Operating agreement in force; predicate (active + endDate ≥ today + scope covers Brand+Program+Season) satisfied | Full portal access per role; DS-2019 guard passes | probation, suspended, expired, terminated |
| probation | Under review due to compliance or performance issues; quota cut to 50% of original allocation (rounded down) | Limited — new job orders require enhanced review; DS-2019 guard BLOCKS | active, suspended, expired, terminated |
| suspended | Halted due to serious violations or sponsor decision | Read-only; DS-2019 guard blocks | active (via reactivation), terminated |
| expired | endDate passed without renewal/extension; auto-expiry job runs ≤15 min after endDate | Read-only; DS-2019 guard blocks | (terminal) |
| terminated | Permanently removed from the ecosystem | No portal access; records preserved for audit | None (terminal state) |

> **Camp: org-status pending does not subtract from the table above**
>
> The Portal Access column above is driven by the agreement lifecycle. For program types other than Camp and Camp SWT, the org lifecycle adds a second, front-loaded gate (no portal access until pending_verification &rarr; active). For the camp program types — **camp** and **camp_swt** — that second gate is removed: an accepted, active operating agreement grants the full portal access shown above even while the org is still pending_verification — with DS-2019 issuance alone additionally requiring verification passed (403 `HOST_VERIFICATION_REQUIRED` otherwise). See Camp Program Divergence — Verification Gates DS-2019 Only.

### Approval Guard

State transitions are guarded by the canonical agreement transition policy: every transition requires an approver, a reason of at least 10 characters, and is audit-logged. No host can self-promote. See canonical agreement lifecycle for the shared spine.

### Suspension & Reactivation Flow

The suspension and reactivation flow ensures that host organizations can be taken offline safely and brought back only after verified corrective actions:

> **Deactivation Audit Trail**
>
> Every suspension and reactivation event is permanently recorded in the audit trail with the acting user, timestamp, reason, and any associated compliance documents. This trail is immutable and available for regulatory review.

## Host Onboarding Flow

Before a host organization can create job orders and receive placements, they must be onboarded through a structured process managed by the sponsor. An account manager submits the organization for verification (moving it to pending_verification), and it must pass verification before activation:

### Automated Host Verification

Creating a host organization does not start verification: its verification record is created in the `unsubmitted` state and is not worker-eligible. An account manager submits it (POST /api/admin/host-verifications/:id/submit, site_visits.manage), which moves the record to `pending`, moves the host org from active to pending_verification and emits host.verification.submitted; a repeat submit of a pending record is an idempotent 200, and any other already-run state returns 409 VERIFICATION_ALREADY_SUBMITTED. The automated process then combines web search and AI analysis to assess the organization before sponsor review. The run is advisory: it always leaves the record in needs_review and stores its outcome as a recommendation (recommendedStatus of verified, failed or needs_review, with confidence score and evidence summary), never as a verdict. Only a human compliance decision (PATCH /api/admin/host-verifications/:id/review) writes verified or failed and clears the recommendation; re-running a never-submitted record is refused with 409 VERIFICATION_NOT_SUBMITTED:

> **Verification Is Advisory**
>
> Automated verification provides data and risk signals, but the final decision to activate a host organization always rests with the sponsor admin. The system does not auto-approve organizations regardless of risk score. A human compliance decision of verified returns an organization sitting in pending_verification to active.

A one-time data migration (`20271116000001_task_2457_reset_unrun_host_verifications`) undid the earlier auto-enqueue at creation. Verification records still in `pending` with no run evidence (no confidence score, evidence summary or recommendation, no `host_verification_attempts` row, no reviewer and no `host_verification_submit` audit entry) were reset to `unsubmitted`. The host org was returned from pending_verification to `active` only where its most recent lifecycle-audit transition was exactly active to pending_verification, and each restore wrote its own `partner_org_lifecycle_audit` row with no actor. Records that had run, been reviewed or been explicitly submitted were left alone.

The web-search half of the automated run uses the Brave Search API and needs the `BRAVE_API_KEY` environment variable (no other provider or connector fallback). When it is unset, or every search query fails at the provider, the run records `needs_review` with no confidence score, an evidence summary stating that web search was unavailable, and `diagnostics.kind` `search_provider_failure` in the stored search results, rather than a low score. Staff then review the host manually or re-run once search is available.

The staff Host Verification queue tree (parent queue `host-verification`) has five child queues, one per verification status, and their names do not imply a machine verdict: **Submitted — Awaiting Run** (`pending`: hosts an account manager submitted that await the automated internet presence check), **Verification In Progress** (`in_progress`), **Compliance Review** (`needs_review`: the automated check has run and produced a recommendation that compliance must confirm or override), **Failed by Compliance** (`failed`) and **Verified** (`verified`). The Failed and Verified queues fill only from an explicit human compliance decision. Hosts still in `unsubmitted` match no queue and appear in no queue count. The wording is carried to deployed environments (which boot with seeds disabled) by migration `20271116000002_task_2457_host_verification_queue_labels`.

### Pending Verification → Active Transition

A newly created host organization lands in pending_verification per Org Creation &rarr; Audit & Event Behavior . Moving that record to active is an explicit, sponsor-authored transition — never automatic, never inferred from verification results, and distinct from operating-agreement activation. This section pins who performs that transition, what must be true at the moment of transition, the UI surface that gates it, and the audit + event side-effects it produces.

> **Camp program type: this gate no longer blocks operations**
>
> For program types other than Camp and Camp SWT, the pending_verification &rarr; active transition documented here remains the front-loaded gate it has always been: no portal access, no job orders, no placements until the sponsor activates the org. For the camp program types — **camp** and **camp_swt** — the same transition still exists, is still sponsor-authored, and still runs the same Activation Gate Checklist — but it no longer blocks operational activity. A camp host with an accepted operating agreement operates the portal (job orders, placements, documents, site visits) while pending_verification; the only hard gate verification retains for camp is DS-2019 issuance. See Camp Program Divergence — Verification Gates DS-2019 Only below.

> **Two activations, do not conflate**
>
> The org-status transition pending_verification &rarr; active documented here is the host organization transition. It is a precondition for, but not the same as, moving the host’s operating agreement from draft/ pending_approval to active. For non-camp program types the org must be active before its operating agreement can be activated. For program type **camp** this precondition is relaxed: the operating agreement may be activated and accepted while the org is still pending_verification, which is what makes the camp host operational during verification (see Camp Program Divergence — Verification Gates DS-2019 Only). The operating-agreement activation rules and the EIN-at-activation gate live at Operating Agreement .

#### Activation Gate Checklist

Globus rejects an attempt to set the host org to active with a 409 `ACTIVATION_GATE_UNMET` response whose `unmet` array cites each unmet item, unless every condition below holds at the moment of transition:

Other Camp Overview fields (description, website, capacity, yearEstablished, every metadata JSONB field) remain deferrable past activation — they are filled in by the host via PATCH /api/partner/host/overview and do not block the org-status transition.

The host lifecycle gates are Rules Library rules, each blocking with a seeded global host reason code (severity blocking):

- `HOST_EIN_INVALID` (rule `host.identity.ein_format`): the EIN is missing or not in the XX-XXXXXXX format.
- `HOST_IDENTITY_INCOMPLETE` (rule `host.identity.normalized_facts_complete`): one of legal name, address line 1, city, state, zip or country is missing.
- `HOST_PRIMARY_CONTACT_REQUIRED` (rule `host.contact.primary_contact_qualified`): no accepted host_admin user and no sponsor primary-contact override.
- `HOST_AGREEMENT_REQUIRED` (rule `host.job_order.submit_eligibility`): a job order submit is blocked because the host status is not active (or pending_verification on a camp-model program), no active in-window agreement exists, or the program or season is outside the agreement scope.

#### Responsible Role & UI Surface

Who can perform the transition

Only a sponsor staff user holding the sponsor.host_orgs.activate permission may move a host org from pending_verification to active. By default this permission is held by the Sponsor Admin role; sponsors may delegate it to a dedicated Verification Reviewer sub-role via standard RBAC. The host has no self-activation path — neither Host Admin nor any host-portal role can trigger this transition. As built, the Hub route is `POST /api/sponsor/host-orgs/:orgId/activate` (permission `sponsor.host_orgs.activate`). The body is `reason` (at least 10 characters after trimming; unknown fields are rejected with 400 `INVALID_BODY`). It returns 200 with `orgId`, `status` and `activatedAt`; 404 `NOT_FOUND` for an unknown organization; 409 `INVALID_STATE` when the org is not in `pending_verification` or changed status concurrently; and 409 `ACTIVATION_GATE_UNMET` with an `unmet` array when a gate fails. Each `unmet` entry has `key`, `code` (`ein_missing`, `identity_incomplete`, `no_primary_contact`, `verification_report_missing_or_dirty` or `open_blocking_flag`), `message` and, when set, `reasonCategory` and `nextActor`.

Where the gate is surfaced

Sponsor Review Dashboard lists every host org in pending_verification with its risk score, open flags, and a per-row \u201cActivate Host\u201d action. For camp hosts, each pending row additionally renders an \u201cOperational — verification pending, DS-2019 blocked\u201d state chip when the host has an accepted operating agreement and is already active in the portal, so reviewers can see at a glance which pending hosts are accumulating job orders and placements ahead of their verification decision (see Camp Program Divergence below). / The activation drawer shows the Activation Gate Checklist live: each unmet item is rendered red with the offending value (or \u201cmissing\u201d) and a deep link to the field on the org detail view. / The \u201cActivate Host\u201d CTA is disabled while any checklist item is unmet; hover reveals the unmet count. The transition is therefore impossible to attempt from the UI when it would 409. / An activation-reason free-text field (\u2265 10 characters) is required on submit and persisted to the audit log alongside the transition.
#### Status Transition, Audit & Event

On a successful activation, Globus performs the following in a single transaction:

> **Reversal path**
>
> A host org can be moved back to pending_verification from active only via the suspension / offboarding flow (see Suspension & Offboarding Lifecycle ); there is no direct “de-activate” action. Re-activating a previously suspended org re-runs the same Activation Gate Checklist and emits the same partner_organization.activated event.

### Camp Program Divergence — Verification Gates DS-2019 Only

For the camp program types — **Camp** and **Camp SWT** (program_model_type camp and camp_swt; "camp" below covers both) — the verification model is decoupled from operational access. A camp host organization in pending_verification whose operating agreement has been accepted (per Operating Agreement — Portal Acceptance) operates the Host Portal fully: it can create and submit job orders, receive and confirm placements, upload compliance documents, and participate in site visits — all while the automated verification and sponsor review are still in flight. Verification retains exactly one hard gate: **DS-2019 issuance**.

What changes for camp — and what does not:

- **Operational access while pending.** The operating-agreement acceptance requirement is unchanged and remains the portal entry condition. Once accepted, the camp host's portal capability set is the full role-scoped set from the agreement-state capability table — pending_verification no longer subtracts from it.
- **DS-2019 issuance predicate gains a verification conjunct.** For camp, the issuance guard is: active in-window scope-covering operating agreement AND host verification passed. "Host verification passed" is true when the host organization's canonical verification record (`host_verifications.status`) is `verified`. An unverified camp host that satisfies the operating-agreement predicate receives a distinct refusal — HTTP 403 with error code `HOST_VERIFICATION_REQUIRED` — never the operating-agreement failure string, so portals and staff tooling can distinguish "agreement problem" from "verification not yet passed".
- **Everything else is unchanged.** Suspension/termination cascades, probation quota cuts (50%), compliance-document holds, and blocking flags all apply to camp hosts exactly as documented on this page. The agreement lifecycle states, the automated verification/risk-score mechanics, and the sponsor-authored pending_verification &rarr; active transition itself are not modified — only the operational consequence of still being pending is.
- **A verification *failure* still halts operations.** Pending is permissive; failed is not. A verification outcome of failed (or a needs_review outcome escalated by the sponsor) raises a blocking flag on the org, and blocking flags halt operations exactly as they do today. See the failure path below.
- **Non-camp program types are unaffected.** Every program type other than Camp and Camp SWT retains the front-loaded gate: pending_verification blocks portal access, job orders, and placements until the sponsor activates the org.

The session-visible projection of this state ("operational, verification pending — DS-2019 blocked") is documented under /me Endpoint & Bootstrap, and the Sponsor Review Dashboard rendering under Responsible Role & UI Surface above.

#### Camp Verification Failure Path

Because a camp host may have accumulated live job orders and confirmed placements before its verification concludes, a verification failure after operational activity has begun follows a defined halt-and-remediate path:

1. **Blocking flag raised.** The failed verification outcome raises a blocking flag on the host org in the same transaction the outcome is recorded. The flag is the halt mechanism — no new lifecycle state is introduced.
2. **Operations halt.** While the flag is open: new job order submission is rejected, new reservations are blocked, and confirmed placements at the host are **frozen** (same freeze semantics and `placement_frozen_*` refusal family as the Suspend / Terminate Cascade Contract — reads continue, mutations 409).
3. **DS-2019 refusals continue.** The issuance guard keeps refusing with `HOST_VERIFICATION_REQUIRED` (HTTP 403); a failed verification never converts the refusal into an agreement-failure reason.
4. **Sponsor remediation or reassignment.** The Sponsor Review Dashboard surfaces the failed host with its frozen-placement count. The sponsor either remediates (re-runs verification / overrides with a documented reason ≥10 characters, which clears the flag, unfreezes placements, and re-opens the issuance gate) or terminates the relationship — in which case frozen placements enter the standard sponsor re-matching/reassignment workflow documented under Reservation Release & Capacity Re-Pooling and Active Placement Continuity, and affected applicants are flagged in the sponsor ops queue for re-matching to other hosts.
5. **Audit.** The flag raise, each placement freeze, and the remediation/termination decision are audit-logged with the standard actor + reason + timestamp vocabulary.

### Host Agreement Structure

The full agreement model — shared spine, side-by-side field map, structured knobs vs. narrative terms, lifecycle state machine, enforcement mechanics, and the 10-metric performance model — is documented canonically in Partner Agreements . This section enumerates the host-relevant fields and host-only extensions on top of the shared spine.

Host Agreement Fields

> **Typed Extension Record: host_agreement_extension**
>
> The host-specific fields above (compliance documents, verification state, adherence state, capacity limits, site-visit schedule, housing requirements) live on a thin host_agreement_extension row keyed 1:1 to the shared partner_agreements row. The shared spine carries lifecycle, scope, restrictions, and audit. The DS-2019 guard evaluates the operating-agreement predicate against the shared spine and, where policy requires, reads host-extension fields such as verification state or adherence state. The operating-agreement concept itself is a derived invariant, not a column on the extension. Field-by-field definitions are in Partner Agreements &rarr; Typed Extension Records .

> **Agreement brand and program must match the host's associations**
>
> Every host agreement writer (create, PATCH, renewal successor, trial-to-operating upgrade) validates the agreement's brand and program (including the scope arrays) against the host organization's active program associations, for orgs with an active employer type. A program that is not an active association is rejected with 400 VALIDATION_ERROR details.code AGREEMENT_PROGRAM_NOT_ASSOCIATED; a brand that is not the brand of one of the associated programs (or the org's own brand when it has no associations) is rejected with AGREEMENT_BRAND_ORG_MISMATCH.

> **Host Onboarding Modal Trigger**
>
> The Host Portal renders the host onboarding modal whenever a host user enters a Brand + Program + Season context where the operating-agreement predicate fails. The modal is the user-visible mirror of the same predicate the DS-2019 guard enforces. See the next section.

> **Canonical Portal Copy**
>
> The exact onboarding-modal title, body, and CTA — plus the shared lifecycle chip token set, chip colors, and the DS-2019 guard failure string — are specified in Partner Agreements &rarr; Portal Copy & Label Alignment . Host-side surfaces should render exactly what that table prescribes. The Host Portal does not surface agreement-status banners of its own; host users see the modal trigger and the lifecycle chip on the agreement detail view.

## Operating Agreement (DS-2019 Guard & Modal Trigger)

“Operating agreement” is a documentation-only term. It names the ascribed meaning of the existing active standard agreement row on the host side. No new lookup value, row type, table, or column is introduced. The full canonical definition lives at Partner Agreements &rarr; Operating Agreement (Host-Only) ; the host-side operational behavior is below.

// Operating-agreement predicate (host-only) — see the canonical, code-accurate
// version at Partner Organizations &rarr; Operating Agreement Invariant Enforcement.
// The restatement below matches live behavior; if it ever drifts from the
// canonical predicate, the canonical page wins.

hasOperatingAgreement(host, brand, program, season) :=

∃ row in partner_agreements where

row.host_org_id = host

AND row.agreement_type IN ('operating', 'standard')

AND row.status = 'active'

AND row.end_date ≥ today

AND row.scope covers (brand, program, season)

active

> **Camp: issuance additionally requires verification passed**
>
> For the camp program types — **camp** and **camp_swt** — the DS-2019 issuance guard evaluates hasOperatingAgreement(...) AND hostVerificationPassed(host). hostVerificationPassed is true exactly when the host's canonical verification record (`host_verifications.status`) is `verified`. A camp host that satisfies the operating-agreement predicate but has not passed verification is refused with HTTP 403 error code `HOST_VERIFICATION_REQUIRED` — a distinct code from the operating-agreement failure string, so consumers can tell the two failure modes apart. This conjunct exists because camp hosts operate the portal while pending_verification (see Camp Program Divergence — Verification Gates DS-2019 Only); for program types other than Camp and Camp SWT the org cannot reach issuance without having been activated post-verification, so the conjunct is trivially satisfied there.
> **No Schema Change**
>
> The operating-agreement concept introduces zero schema change. It is purely a documentation term that names an invariant already present in production: an active partner_agreements row — agreement_type 'operating' or 'standard' — whose endDate has not passed and whose scope covers the current Brand + Program + Season. A 'trial' row never satisfies this invariant regardless of status or date range (see Partner Organizations &rarr; Trial Agreement (HP-3) ). Naming the invariant lets us discuss the DS-2019 guard and the host onboarding modal precisely without adding rows, columns, or lookup values.

The invariant that makes this concept safe (partial unique index, single predicate, closed audit reason codes, test coverage matrix, and rollout sequence) lives at Partner Agreements &rarr; Operating Agreement Invariant Enforcement .

> **Parent / Child Inheritance Note**
>
> Host orgs may belong to a parent host org and may opt in (per-field) to inherit the parent’s operating agreement. When a child has the agreement-inheritance toggle on, the predicate above resolves against the parent’s standard row and the child is treated as covered by the DS-2019 guard and the onboarding modal. The Host Portal renders an “Inherited from ” read-only badge on the Operating Agreement panel in that case; sponsor staff edit the link and toggles from the admin-console Parent / Children panel only. Full data model, single-level rule, suspension semantics, and audit events live at Partner Organizations &rarr; Parent / Child Hierarchy ; the creation-time picker is in Org Creation &rarr; Parent Organization (Optional) .

### Operating Agreement — Portal Acceptance

In addition to the underlying agreement row that drives the DS-2019 guard, the Host Portal enforces an explicit acceptance step. Before any host user can perform meaningful work in the portal, the host org must have accepted the current operating agreement.

> **Library-linked legal content — no free-text fallback for new agreements**
>
> Newly created host agreements are linked to a published Agreement Library entry at creation time (engine_agreement_code set from the start, validated server-side as published-only, host_agreement type, correct brand scope — see Agreements Engine &rarr; Host Agreement Library Linkage). For these agreements, portal gating, the acceptance evidence recorded at sign-off, and the signed-PDF rendering all resolve to the linked library version's HTML — there is no free-text fallback. Legacy free-text host agreements (legal content in partner_agreements.terms, including rows backfilled under host_agreement_migrated_* codes) remain grandfathered and continue to gate, evidence, and render exactly as before.

#### Acceptance Audit Contract

The acceptance write is a mutation, so it follows the platform audit contract end-to-end. In the same request that records the acceptance evidence row, the Hub emits one audit record — action `operating_agreement.accept` — through the accept route's auditMutation middleware, which writes it best-effort after the success response. The audit record carries:

| Audit dimension | Value at acceptance |
| --- | --- |
| action | `operating_agreement.accept` |
| entityType / entityId | `operating_agreement` / the accepted host agreement row's id |
| actor | Resolved by the audit middleware from the request's actor context (the authenticated host request and its organization); the accept route itself requires the X-Host-User-Email attribution header (alongside X-Host-Org-Id for the org context) |
| timestamp | The time the audit row is written, immediately after the acceptance succeeds |
| payload | The success response body (PII-redacted), which for library-linked agreements includes the engineAcceptances entries (acceptanceId, agreementCode, versionId, contentHash), so the audit entry names exactly which legal text version was accepted |

**No anonymous acceptances.** Acceptance is a mutation, so the general mutation rule applies with no exception: an acceptance POST without a valid X-Host-User-Email header is rejected with 400 before any write occurs — neither the evidence row nor the audit record is created. There is no fallback actor and no org-level attribution shortcut for acceptance.

**Re-signs audit independently.** When a new library version is published and the host re-accepts (re-sign), the re-sign emits its **own** `operating_agreement.accept` audit entry — attribution and timestamp of the re-signing user — and its evidence row chains to the superseded acceptance via supersededByAcceptanceId. The original acceptance's audit entry and evidence row are never modified; the audit trail accumulates one entry per acceptance act.

**Two records, one act.** The acceptance evidence row (agreement_acceptances — immutable, forensic, carries contentHash / IP / user agent) and the audit record (audit trail entry, queryable from the Audit Console) are two distinct, linked records of the same acceptance. See Agreements Engine &rarr; Acceptance Evidence & Compliance for how the evidence ledger relates to the audit log.

> **Signature lifecycle & per-send acceptance matching (Aug 2026)**
>
> The portal signing gate is driven by an enforced signature lifecycle (shipped Aug 10–11 2026): creating a new host agreement activates it and sends it for signature in a single action (one-click activate-and-send) — no intermediate draft status, no separate Activate step, and no separate Send step. Staff surfaces show a derived signature state (not sent / awaiting signature / signed), and the portal prompts only for active, **sent** agreements — a never-sent agreement cannot be accepted. Acceptance matching is **per-send**: when staff send a new agreement, signatures recorded before the latest send no longer satisfy the gate, signed status is forced false while the gate is blocking, and the signing modal reliably reappears. The host-portal agreements summary returns the full library legal content per linked agreement (name, version label, effective date, format, published HTML body — graceful nulls for unlinked rows), and the sign modal is guaranteed never to render without complete published legal text. Full contract: Agreements Engine &rarr; Host Agreement Signature Lifecycle & Portal Signing.

> **Acceptance refused when the library terms cannot be resolved**
>
> An acceptance is only valid if it can be evidenced. Before the Hub stamps `accepted_at` on a library-linked operating agreement, it asserts that the agreement's Agreement Library entry (`engine_agreement_code`) resolves to a published version. If it does not, the accept is refused up front with `409 CONFLICT`, `details.code` `OPERATING_TERMS_UNAVAILABLE` (plus the `engineAgreementCode`), and nothing is stamped. The same check protects the replay of an already-accepted row that never received its engine linkage. As a second line of defence, if the library version becomes unpublished between that check and the evidence write, the Hub rolls back only its own `accepted_at` stamp (matched on the exact value it wrote, so a concurrent winner is never clobbered) and returns the same conflict; the accept can then be retried once the version is published. Rows that already carry complete engine linkage skip the check, so a later unpublish cannot break an idempotent re-accept, and rows with no `engine_agreement_code` at all are unchanged. Separately, an acceptance whose signed archive can never be produced (no resolvable accepted version, or evidence that does not match it) now returns a terminal `409 CONFLICT` (`SIGNED_AGREEMENT_ARCHIVE_BLOCKED`, not retryable) instead of a retryable `503`; transient render or storage failures keep `503 SIGNED_AGREEMENT_ARCHIVE_FAILED`.

> **Revision-bound acceptance (development HEAD)**
>
> Each commercial offer a host is asked to accept is snapshotted in `host_agreement_revisions`: `agreementId` (cascades with the `partner_agreements` row), `commercialTerms` (a JSON snapshot of the effective offer, including child quotas and extensions, so it survives later edits), `termsHash`, the agreement-library `versionId` and `contentHash`, `createdAt`, `sentAt`, `dispatchStatus` (default `pending`), `dispatchError`, `acceptedAt` and `acceptanceId`, indexed on `agreementId` plus `createdAt`. `partner_agreements.currentRevisionId` points at the current revision. A new revision is written only for an active agreement with a library agreement code that has been sent (`lastSentAt` set, unless the caller allows unsent), and only when the terms hash, library version or content hash differ from the current revision; otherwise the current revision is reused. If the linked agreement has no servable legal version for the host, preparing a revision fails with `409 CONFLICT` and `details.code` `HOST_AGREEMENT_VERSION_UNAVAILABLE`.
>
> `POST /api/partner/host/operating-agreement/accept` takes four optional body fields that identify the revision the host was shown: `revisionId`, `termsHash`, `versionId` and `contentHash`. They must be submitted together or not at all (the request is rejected by validation otherwise). When the acceptance is a revision acceptance, the legal acceptance evidence is written first and the revision is then marked accepted; the legacy `acceptedAt`, `acceptedByEmail` and engine linkage columns are only written while `acceptedAt` is still empty, so a later revision never overwrites the first acceptance marker. Failures are `409 CONFLICT` with `details.code` and `details.agreementId`: `HOST_AGREEMENT_REVISION_REQUIRED` (no revision details were submitted), `HOST_AGREEMENT_REVISION_EVIDENCE_MISSING` (no matching legal acceptance evidence for the current revision) and `HOST_AGREEMENT_REVISION_STALE` (the acceptance could not be recorded against the current revision: it changed, its terms, version or content hash no longer match, or the agreement is no longer active; refresh and accept the current terms).
>
> For a trial agreement review (`trialReview` in the body) the Host Portal client only treats the acceptance as done when the response carries `trialAcceptanceVerified: true`; otherwise it raises `TRIAL_ACCEPTANCE_UNVERIFIED` (status 502, message "The trial agreement acceptance could not be verified by the agreement service."). This is thrown by the portal client, not returned by the Hub or the portal server: a failure the portal server detects reaches the client as `502 ACCEPTANCE_STATE_UNVERIFIED`, so `TRIAL_ACCEPTANCE_UNVERIFIED` appears only when a success response lacks the flag.

> **Host Portal accept handling (development HEAD)**
>
> The Host Portal's `POST /api/v2/profile/operating-agreement/accept` never trusts the Hub's success echo: after the call it re-reads the gate-exempt status endpoint and only succeeds when the Hub positively reports `isSigned`; otherwise it retries once against the agreement the Hub actually holds as active and finally answers `502 ACCEPTANCE_STATE_UNVERIFIED`. If the Hub reports that it recorded the acceptance but failed to produce the signed archive (a `503 INTERNAL_ERROR` archive delay, or a `409` saying the acceptance is recorded but no signed copy can be rendered), the portal rechecks the status a bounded number of times and converts the failure to success only when the status confirms the intended agreement is signed — it never re-submits the acceptance. When the Hub refuses because the agreement's library version has no published copy (`409`, acceptance rolled back), the portal relays the stable code `AGREEMENT_VERSION_NOT_PUBLISHED` with a plain-language message instead of upstream prose. While a host's agreement lifecycle can still change externally (trial standing, or a gated context read with an acceptance outstanding) the agreement gate re-checks every 60 seconds and on window focus, rate-limited to one re-check per 30 seconds; once a full agreement is accepted the synchronisation stops. For a host with more than one program the gate considers the full program scope of every agreement (`programIds`), so a single unaccepted draft agreement no longer blanks the portal for a host who has accepted on another program; single-program hosts keep the strict behaviour.

> **Signed agreement on the Documents tab**
>
> The Documents → Legal tab shows a Host Company Agreement card driven by the operating-agreement status (not by uploaded templates). Once the agreement is signed it shows the signed date and a Download button that streams the signed PDF through `GET /api/v2/profile/operating-agreement/signed-pdf`. For camp hosts the card is always shown, carries a Required badge, and before signing reads "Not signed yet — sign it when prompted at sign-in"; signing never happens by uploading a file. For other hosts the card stays hidden until the agreement is signed.

### Trial Agreement — Host Portal Behaviour (HP-3)

A host may be onboarded with a trial agreement (agreement_type = 'trial') before signing a full operating agreement. The Host Portal detects this state via the upgradeRequired flag on the status endpoint and restricts the visible page set accordingly. For write enforcement the portal treats the Hub's own gate-exempt status as authoritative: `trialRestricted: true`, or a non-empty `activeTrialAgreementId`, means the host is still trial-restricted; `trialRestricted: false` means the restriction is lifted; and the context agreement rows are consulted only when the Hub says neither (they can lag an upgrade, or disagree in the other direction).

operating / trial_agreement_upgrade_required / superseded
> **Cross-reference: Trial Agreement canonical spec**
>
> The full trial agreement type definition, the TRIAL_AGREEMENT_TYPE constant, the PARTNER_AGREEMENT_TYPES enum, and the trial-to-operating transition lifecycle are documented canonically in Partner Organizations &rarr; Trial Agreement (HP-3) .

#### Trial Browse-Only Gate & Upgrade Flow (Aug 2026)

Shipped Aug 10–11 2026 (Host Testing Fixes Batches 8 & 9), trial hosts are **browse-only** until they sign the operating agreement:

- **Write gate (narrowed Aug–Sep 2026)** — the browse-only guard (`requireFullAgreement`) is attached only to the four reservation mutations: reservation create, cancel, hire, and extension request (an extension request must carry `applicationId`, with `jobOrderId` optional, because the Hub's own schema requires it even though its published contract lists it as optional). A trial host attempting one of them is rejected with `403 TRIAL_AGREEMENT_UPGRADE_REQUIRED`, with `trialAgreementId` and, when known, the `upgradeAgreementId` surfaced in the block details (the upgrade modal no longer depends on the latter). Profile and company-administration writes are deliberately not guarded — overview, addresses, contacts, housing, arrivals, cultural activities, activity sites, structured offer details, job offer contract uploads, host-user management and company documents — because completing the profile is exactly the work a trial host is expected to do before upgrading. Reads remain available per the HP-3 restricted page set above.
- **Fresh, fail-closed standing** — the guard reads the host's standing from the Hub on every guarded write (no cache), following the Hub's `trialRestricted` / `activeTrialAgreementId` over the context rows when they disagree. If the standing cannot be established (context or status read fails) the write is rejected `503 AGREEMENT_STATUS_UNAVAILABLE` rather than allowed through unverified; if the Hub's acceptance gate itself refuses the context read, that gate error is relayed so the first-login acceptance flow takes over.
- **Recovery-route exemption** — the operating-agreement recovery routes (status, accept, dismiss and upgrade under `/api/partner/host/operating-agreement/`, mirrored by the Host Portal BFF under `/api/v2/profile/operating-agreement/`) carry no trial guard, so a trial host can always see and complete the upgrade that unblocks them.
- **Library-first agreement resolution** — the trial-upgrade operating agreement resolves its agreement code library-first: a valid inherited code is kept; otherwise the published, brand-compatible host agreement is used (brand-specific beats global). Missing or ambiguous cases surface actionable errors through the status endpoint and the trial-gate block details — never a silent fallback.
- **Explicit upgrade selection** — the upgrade is a host action: a host admin (agreements.write) calls POST /api/partner/host/operating-agreement/upgrade. GET status is read-only and never drafts a successor; it reports `upgradeEndpoint` while the host is trial-restricted and returns the `pendingOperating*` fields (null until an upgrade is selected). The upgrade call drafts the operating successor idempotently, validates the published Host Company Agreement Library terms and a signer contact email **before** activation, then activates it (terminating program-scope-overlapping active trials in the same activation transaction) and sends it for signature in one call, returning status active, signatureState awaiting_signature and the published `legalContent`. A dispatch failure returns 502 OPERATING_UPGRADE_SIGNING_DISPATCH_FAILED and leaves the successor active-but-unsent; retrying the upgrade re-sends it. Status polling keeps returning the prepared operating successor (`pendingOperatingLegalContent`, with the pinned Host Company library metadata) after activation has cleared the trial restriction, so the signing prompt keeps the operating legal copy.
- **Acceptance stays separate** — the host then accepts through the accept endpoint, which validates the signature policy and records versioned signing evidence. Idempotent re-accept is supported. A still-draft successor that was prepared and sent under the earlier flow is activated (and the trial terminated) atomically when it is accepted.
- **Trial acceptance gate** — activating a trial agreement validates the published trial Library entry and a signer email first, then activates it as acceptance-required and sends it for signature (a dispatch failure returns 502 TRIAL_AGREEMENT_SIGNING_DISPATCH_FAILED and deferred host invites are not released). Until the org's acceptance is recorded, every host-portal route except GET status, POST accept and POST upgrade returns 403 AGREEMENT_ACCEPTANCE_REQUIRED (details.waitingForAdmin for non-admin users).
- **Host Portal BFF guard** — in HostPortalPrototype, only the four reservation mutations (create, cancel, hire, extension request) get a BFF trial guard (403 TRIAL_AGREEMENT_UPGRADE_REQUIRED). It follows the Hub's operating-agreement status over host-context rows, fails closed with 503 AGREEMENT_STATUS_UNAVAILABLE, and deliberately does not guard profile writes. Reservation create derives the agreement server-side in order: the job order's own agreement, the agreement the Hub reports as signed, the requested id validated against the host's own agreement rows, then the host's active operating agreement; an id that is a trial, out of force or not held by the host, or a host with no active operating agreement, is refused with 403 RESERVATION_AGREEMENT_NOT_PERMITTED.

#### Portal-initiated upgrade (Hub upgrade route)

A trial host can start the upgrade from the portal with `POST /api/v2/profile/operating-agreement/upgrade`. The portal does not assume that accepting a full agreement activates it (acceptance is additive and never changes the staff-controlled lifecycle, so accepting a `draft` agreement would strand it, unsigned, with a dangling acceptance). Instead it reads the Hub's gate-exempt status, decides which call performs the upgrade, writes once, and verifies by re-reading state:

- **The call.** The confirmed Hub route is `POST /api/partner/host/operating-agreement/upgrade`: POST-only, empty JSON body, the authenticated host organisation plus `X-Host-User-Email`. It activates the successor agreement and sends it for signature; it does not record an acceptance. A plain acceptance is used only when the Hub explicitly designates it. An endpoint the Hub advertises must be a relative path inside `/api/partner/host/`; anything else is refused. If the Hub's status is unreadable, names an unknown action or no action, or advertises an unsafe endpoint, no write is made and the portal answers 502 with `UPGRADE_CONTRACT_UNAVAILABLE`, `UPGRADE_ACTION_UNSUPPORTED`, `UPGRADE_ENDPOINT_REJECTED` or `UPGRADE_NOT_DESIGNATED`.
- **Refusals before any write.** `409 UPGRADE_NOT_AVAILABLE` when no full (non-trial) agreement has been provisioned; `409 UPGRADE_AGREEMENT_NOT_ACTIVATABLE` when the full agreement is in a lifecycle state (for example `draft`) that acceptance cannot rescue.
- **Awaiting signature is a success.** After the Hub upgrade, the expected end state is: restriction lifted, successor in force (`active` or `probation`), not yet signed. The portal reports this as a normal `200` with `upgraded: false`, `activated: true`, `awaitingSignature: true`, `acceptanceRequired: true` and the successor's `agreementId`; the host then signs through the acceptance gate. A fully completed upgrade returns `upgraded: true` with `via` `hub-upgrade` or `acceptance`.
- **Verification failures.** When the re-read end state is wrong, the portal relays a per-condition code: `UPGRADE_AGREEMENT_MISSING` (502), `UPGRADE_AGREEMENT_NOT_ACTIVATED` (409), `UPGRADE_TRIAL_STILL_ACTIVE` (409), `UPGRADE_ACCEPTANCE_NOT_RECORDED` (502), `UPGRADE_AWAITING_SIGNATURE` (409, used when an acceptance-plan upgrade ends recorded but unsigned), `UPGRADE_TRIAL_STANDING_UNVERIFIED` (502) and `UPGRADE_SIGNATURE_UNVERIFIED` (502). Trial standing is judged by the Hub's status first; success requires positive confirmation that the restriction is lifted.

### Trial → Operating: First-Time Upgrade

The Renewal API below only applies once a host already has an active operating agreement — it explicitly rejects hosts with none (see the no_operating_agreement_to_renew precondition). A host still on its auto-created trial agreement uses a different, first-time path: staff open the host's Agreements tab and use the "Upgrade to Operating Agreement" action, which drafts and activates a genuine operating-or-standard-typed replacement row in the same atomic draft &rarr; validate &rarr; activate shape as the renewal endpoint below. A host admin can start the same upgrade from the Host Portal (see Trial Browse-Only Gate & Upgrade Flow above). Reservation-attach fails with 422 POLICY_VIOLATION / HOST_AGREEMENT_NOT_FOUND against a trial-only host until this upgrade is completed.

### Operating Agreement — Renewal API

Renewing a host’s operating agreement is a single atomic operation: POST /api/hosts/:id/operating-agreement/renew runs draft &rarr; validate &rarr; end prior &rarr; activate successor in one database transaction. There is no two-step “create draft, then publish” flow and no window in which both rows are simultaneously active.

s end_date is set to today and the successor row is inserted with status = / inside the same transaction. If any validation step fails, the transaction rolls back and the prior row remains live — there is never a moment where a host has zero active operating agreements as a side-effect of a failed renewal.,
Validation Failure Leaves Prior Live / Scope coverage, date sanity, and lock-compatibility checks run before the cutover. A failed check returns a structured 422 naming the offending predicate and the prior agreement continues to serve the DS-2019 guard unchanged.,
Pre-Condition Error: no_operating_agreement_to_renew / If the host has no currently-active operating agreement (agreement_type= / ) when renewal is requested, the endpoint returns 422 no_operating_agreement_to_renew. Renewal is not an onboarding path — onboarding is a separate first-time issuance flow.,
Notification: host.operating_agreement.renewed / On successful cutover, a single host.operating_agreement.renewed event is emitted to the host org
> **Notification catalog cross-reference**
>
> The host.operating_agreement.renewed event is registered in the canonical host event catalog — see Employer Notifications &rarr; Operating Agreement Renewed for delivery rules, and Notification Model &rarr; Host Event Types . Branding: the renewal notice renders under the **successor** agreement's Agreement Library brand scope — a renewal onto a Wild Packs-scoped agreement is announced in Wild Packs branding, per Notification Model &rarr; Agreement-Derived Brand Resolution (Wild Packs); sends enqueued before the cutover keep their recorded brand snapshot.

## Compliance Document Tracking

Host organizations must maintain compliance documents throughout their lifecycle. Globus tracks document submission, review status, expiration, and renewal requirements. The Host Portal provides a document center for hosts to upload and monitor their compliance documents.

> **Source of truth: Host Document Center contract**
>
> The endpoint set, signed-URL upload flow (host-facing; staff uploading on a host's behalf use a single authenticated multipart POST /api/admin/partners/organizations/:orgId/host-documents with partners.documents.manage, where the server writes storage itself and returns 503 STORAGE_UNAVAILABLE if the file could not be saved, and the staff signed-URL initiate-upload/finalize-upload pair is retired), bidirectional visibility model (visibleToHost), permissions (host.documents.view / host.documents.manage), validation, and the document.uploaded / document.replaced / document.deleted events are specified canonically in Integration Contract &rarr; Host Document Center . The categories and lifecycle states below are the host-facing view of the same model. Staff read routes under `/api/admin/partners/organizations/:orgId/host-documents` need `partners.view` and org scope (403 outside it): `/categories` returns the category keys with program-conditional labels, the type groups, `allowedMimeTypes` and `maxFileSizeBytes`; `/requirements` returns the requirement checklist with each requirement's matching document; `/:id` returns one document (404 if it is not the org's); `/:id/review-history` returns the per-transition review audit rows as `history`. Upload, review and delete need `partners.documents.manage`.

### Document Types

| Document Type | Required | Renewal | Description |
| --- | --- | --- | --- |
| Business License | Yes | Annual | Valid business license or registration for the operating jurisdiction |
| Workers Compensation Insurance | Yes | Annual | Proof of workers compensation coverage for all exchange visitor positions |
| Camp Accreditation | Conditional | Per accreditation cycle | Current accreditation from a recognized camp accrediting body. Required for Camp and Camp SWT program types. |
| General Liability Insurance | Yes | Annual | General liability insurance meeting minimum coverage thresholds |
| Housing Inspection Report | Conditional | Per season | Required when host provides housing. Must pass safety and habitability standards. |
| Site Visit Report | Yes | Per schedule | Results from sponsor-conducted site visits. Generated by sponsor staff, not uploaded by host. |
| W-9 / Tax Documentation | Yes | On change | Federal tax identification documentation for the host organization |
| Safety Certifications | Conditional | Varies | Industry-specific safety certifications (e.g., food handling, lifeguard, CDL) for applicable programs |

> **Camp verification evidence — the Rule Library owns the pass decision**
>
> For Camp and Camp SWT hosts, four artifacts double as evidence facts for the Rules Engine's Camp Host Verification Policy: Business License, Workers Compensation, Camp Accreditation, and the accepted Terms and Conditions (the operating-agreement acceptance record — not an upload). The three document artifacts are submitted under the Document Center categories business_license, workers_comp, and camp_accreditation respectively — the Document Center category values used for these artifacts. The Document Center remains the owner of upload, review, and expiry tracking for the document artifacts, but it never derives the verification outcome — the Rule Library does, and the camp DS-2019 guard consumes that derived result. There is no separate, season-scoped verification checklist: the policy is bound to the camp program types, so season changes neither recreate these requirements nor invalidate a valid document. Expiry warnings, review states, and compliance-hold behavior below are unchanged. The compliance-side review surface — where reviewers approve, reject, or request replacement of these artifacts and record section-level verification decisions behind the Sponsor Review Dashboard — is specified on Host Verification &amp; Approved Terms; document decisions there set the review states this tracking model already owns.

> **Host Portal Documents tab — what is marked Required (development HEAD)**
>
> The portal's Documents page shows its own requirement badges and Requirements Summary, which are a display over the portal's document catalog and do not replace the Hub-side verification decision above (the Rule Library still owns the camp pass outcome). For a standard (non-camp) host every catalog item is Required: Business License, Articles of Incorporation, General Liability Insurance, Workers' Compensation Insurance, Tax Documents, Accreditation Membership, Non profit status and Site visit documentation (8 Required). A company counts as a camp host when any of its programs is camp-related; for a camp host the portal marks only two things Required — the Host Company Agreement (signed through the agreement flow, never uploaded, and counted in the summary whether or not it is signed yet) and Accreditation Membership. Every other catalog item stays visible and uploadable but is labelled Optional (7 Optional, 0 Recommended for a camp host), and the camp catalog shows Workers Compensation in place of Workers' Compensation Insurance. The Required badge is red, Recommended yellow and Optional neutral grey.

> **Expiry dates can be set and edited after upload**
>
> A document's `expiresOn` is no longer settable only at finalize-upload time. `PATCH /api/partner/host/documents/:id/expiry` (canonical mount only; not added to the deprecated `/api/host/documents` alias) sets, changes or clears the expiry on an already-finalized document with body `{ "expiresOn": "YYYY-MM-DD" | null }`; past dates are allowed, and an impossible calendar date is a `400 INVALID_BODY`. It requires the host Admin role with the `host.documents.manage` scope and `X-Host-User-Email`, is org-scoped (a foreign id is `404 DOCUMENT_NOT_FOUND`, never 403), and returns `409 DOCUMENT_LOCKED_HOST_SUSPENDED` for a locked document. It writes only `expiresOn` — it never touches `reviewState`, `reviewedAt` or `reviewedByEmail`, so a date correction does not reopen review; the derived `effectiveReviewState` moves on its own because it is computed from `expiresOn` at read time (an approved document past its date reads as expired). Each change emits `document.expiry_updated` (carrying `previousExpiresOn`), deliberately distinct from `document.replaced`. This supplies the input the expiry-compliance derivation (`expiring_soon` / `expired`), the portal expiration dashboard and the hourly `host.document_expiring_soon` / `host.document_expired` notification jobs read. The Host Portal exposes it as `PATCH /api/v2/company-documents/:id/expiry`, a pass-through to the Hub route.

### Document Lifecycle

> **Expired Documents Block Operations**
>
> If a required document expires and is not renewed, Globus may automatically place the host on compliance hold, blocking new job order creation until the document is renewed and approved.

**Reviewing a host document.** `POST /api/partner/host/documents/:id/review` (host Admin role, `host.documents.manage` scope, `X-Host-User-Email` header) takes `decision` (`approved`, `rejected` or `resubmission_requested`) and optional `reviewNotes` (up to 2000 characters); a locked document is refused before the transition is checked. The review state machine allows `pending_review` to any of the three decisions and `rejected` or `resubmission_requested` back to `pending_review`; `approved` and `expired` are terminal. Because the route can only apply the three decisions, only a document currently in `pending_review` can be decided through it: any other document, or a decision equal to the current state, returns `409` with `error.code` `INVALID_REVIEW_TRANSITION` and `error.details` carrying `fromState` and `toState`. The staff route `POST /api/admin/partners/organizations/:orgId/host-documents/:id/review` returns the same code; an unknown document is `404` and an invalid body is `400 INVALID_BODY`.

## Job Order Lifecycle

A job order is the unified demand-side entity in the Globus ecosystem. It represents a specific job opportunity created by a host organization, encompassing the role definition, matching requirements (must-have vs nice-to-have), compensation (wage in dollars), SEVIS classification, capacity management, and housing details. Job orders have built-in capacity tracking (position count and fill rate) and support pause/resume/cancel controls.

> **Only Placement/Hybrid Partners Can Create Job Orders**
>
> Job order creation is restricted to host organizations with a partner type of “placement” or “hybrid.” Recruitment-only partners cannot create job orders — they can only view and reserve positions on approved job orders.

> **Host Management Job Orders tab — visible statuses (QA, 2026-07 — Task #1938)**
>
> The staff Host Management Job Orders tab filters rows through a client-side visible-status whitelist. `active` is now on that whitelist (with a status badge) — previously, activated job orders silently disappeared from the tab. `archived` and `cancelled` remain intentionally hidden. Visibility of `paused` / `revision_requested` is explicitly deferred (follow-up #1939).

### Capacity Management

Each job order has built-in capacity tracking. Position count and fill rate are managed directly on the job order — there is no separate entity for individual positions.

### Job Order &rarr; Placement

The relationship between job orders and placements is direct — job orders manage their own capacity:

s remaining capacity.,
Placement / Placement Context / 0-many per job order / The confirmed match between an applicant and a job order position. Increments the job order
Job Order (1, with capacity) &rarr; Reservation Hold (0-many) &rarr; Placement (0-many, up to position count)

## Interactive Phase (Placement & Operations)

During active operations, hosts participate in placement and program management. Hosts see recruiter-originated reservation holds alongside their own, providing a unified view of job order activity. The following actions are available depending on agreement terms and role permissions:

Any future “Activity” UI tab on Placements, Arrivals, or Job Orders should source its data from GET /api/partner/host/activity-log — that endpoint section in the Integration Contract is the source of truth for its scope, RBAC, query parameters, and event vocabulary. The feed is now the participant-lifecycle stream and also surfaces contact-change events emitted from the host-portal contact CRUD routes. Any earlier shorter host-activity path is no longer documented — this canonical endpoint is the only supported reference.

### Dynamic Applicant Data Rendering

The Host Portal dynamically consumes the published workflow version to render applicant profile and application data. Rather than hardcoding screens for each workflow step, the Host Portal reads the Host Portal Contract defined in the Workflow Studio and renders each step’s completed output accordingly.

> **Required contract: Dynamic Field Surface Spec**
>
> The Host Portal's talent card and applicant profile are the two named host surfaces (host.talent_card, host.profile) defined by the Dynamic Field Surface Spec . The dynamic renderer described here is constrained by that spec: it auto-renders only Studio fields tagged for the surface (not every step in the workflow), groups them under an “Additional Information” section on the profile view, and respects each field's authored label. Audience filtering is applied by the step-level data_scope plus the platform PII registry — there is no per-field redaction enum and no partner_visibility predicate; both were explicitly rejected and are recorded in DEPRECATED_DATA_ACCESS_TERMS (shared/contracts/dataAccessContract.ts). Untagged fields fall through to the workflow_tab_only fallback and do not appear in the host's view.

This means when a workflow adds a new step (e.g., a “Hobbies” form in Phase 1), the Host Portal displays it without any development work — matching the same dynamic pattern the Applicant Portal already uses for intake rendering.

### Enforcement Layers

All actions during the interactive phase are governed by five enforcement layers. These layers consume the structured knobs defined on the canonical agreement (see canonical agreement enforcement ) plus the host-only operating agreement predicate gating DS-2019 issuance.

## Housing Management

Host organizations that provide housing for participants must manage housing records through the Host Portal. Housing management covers property registration, unit assignment, inspection tracking, and compliance monitoring.

### Housing Features

### Housing Leads vs Host-Managed Inventory

> **Two distinct housing concepts — do not conflate**
>
> host_housing — host-managed inventory. The properties the host has registered with Globus and operates over time (assignments, inspections, occupancy). Backed by /api/partner/host/housing in server/routes/hostPortal/housing.ts. This is the inventory the “Housing Features” section above describes. housing_leads + job_order_housing_leads — per-job-order candidate properties reviewed under the housing approval gate. housing_leads is the property-level row; job_order_housing_leads is the many-to-many join to a specific job order. Backed by /api/partner/host/housing-leads in server/routes/hostPortal/housingLeads.ts. A property typically starts as a housing_lead attached to one or more job orders during their housing-gate review, then graduates into host_housing once placements are made and the host is operating it as inventory. See the Job Orders &rarr; Housing Vocabulary section for the job-order-side perspective.

Staff-created and staff-edited housing leads (Host Management Housing dialog; POST /api/admin/housing-leads and PATCH /api/admin/housing-leads/:id) also carry three additive household amenity fields: `linensAndTowelsProvided` (boolean, default false), `bathroomsDescription` (free text or null; on update null clears the stored description) and `staffLiveWithChildren` (boolean, default false). Legacy leads read as No / empty, and on update an omitted field is left unchanged.

## Settings & Preferences

The Host Portal includes a settings and preferences module that allows Host Admins to configure their organization's portal experience. Settings are organization-scoped and apply to all users within the host organization.

### Settings Modules

> **Settings Are Organization-Scoped**
>
> All settings apply to the host organization, not to individual users. Individual users inherit the organization's settings. User-level preferences (e.g., notification opt-out) may be added in future iterations but are not part of the initial release.

## Post-Program Phase (Read-Only)

After program completion, hosts transition to read-only observers. They maintain visibility into historical placements and program outcomes but can no longer take operational actions.

> **Hosts Retain Historical Access**
>
> Hosts can view historical placement records, completed evaluations, and program outcomes for participants they hosted. This supports compliance audits and future planning without requiring sponsor staff to manually relay information.

### Host Restrictions

Hosts explicitly cannot:

## Governance Principle

Hosts are operational participants in the placement ecosystem, but authority remains centralized in Globus at every phase. The host's role ranges from job order creation to placement confirmation — but never extends to autonomous decision-making over compliance, eligibility, or SEVIS operations.

> **Core Governance Rule**
>
> Globus decides. Host Portal reflects. This principle applies uniformly across job order creation, capacity management, placement confirmation, and program operations. There is no phase in which a host gains autonomous authority over sponsor-controlled processes.

| Phase | Host Role | Authority |
| --- | --- | --- |
| Pending Verification | None (program types other than Camp / Camp SWT) / Active participant (Camp or Camp SWT, agreement accepted) | Organization created, undergoing automated verification and sponsor review. Includes profile setup, agreement acceptance, and compliance document upload. **Camp / Camp SWT:** with an accepted operating agreement, the host operates the portal fully while pending — verification gates DS-2019 issuance only (403 `HOST_VERIFICATION_REQUIRED`). See Camp Program Divergence — Verification Gates DS-2019 Only. |
| Job Order Creation | Active participant | Create and submit job orders for review |
| Capacity Management | Active participant | Monitor fill rates, pause/resume job orders, view reservations |
| Placement | Active participant | Confirm placements, report arrivals |
| On-Program | Active participant | Submit evaluations, manage housing |
| Post-Program | Read-only observer | View historical records only |

## Host Autonomy Decision Framework

Host autonomy uses the same A–D framework as the recruiter side, defined on the canonical agreement (see canonical agreement restrictions & autonomy ). The autonomy level is a structured field on the host agreement extension and determines the Host Portal feature set, the job order approval workflow, capacity controls, and the governance overhead required from sponsor operations.

> **Strategic Decision**
>
> The architecture supports all four levels below. The sponsor must decide which level each host class operates at — and that decision should be made before building host-facing features beyond basic job order submission.

**Changing a host's tier.** `PATCH /api/admin/partners/organizations/:id/autonomy-tier` (permission `partners.manage`) takes `tier` (`A`, `B`, `C` or `D`) and a required non-blank `reason`. An unknown organization returns 404. Setting the tier the host already has returns 200 with the current row and writes no audit row. A real change updates `partner_organizations.autonomy_tier` and writes an `autonomy_tier_changed` audit row with the previous tier, new tier and reason (fire-and-forget, so an audit failure does not fail the request).

### Autonomy Levels

## Host Portal User Management

Host Portal access is relationship-based, not role-based like internal Globus RBAC. Host users can only see data belonging to their own organization, and their capabilities are determined by the combination of their user role and their organization's agreement terms. The Host Portal provides a complete user management system including identity bootstrap, CRUD operations for team members, and per-user role enforcement.

### /me Endpoint & Bootstrap

Every Host Portal session begins with a bootstrap call to GET /api/partner/host/context which resolves the user's identity, organization, role, agreement status, and available scopes:

"organization": id / uuid / name / ... / status / active / type / host,

"user": id / uuid / email / ... / role / host_admin / name,

"agreement": id / uuid / status / active / programs / seasons,

"capabilities": ["create_job_orders", "manage_team", "manage_housing", "confirm_placements"],

"verification_status": "verified"

> **Bootstrap Drives the Entire Session**
>
> The /me endpoint response determines what the user sees and can do. If the organization is in pending_verification, the portal shows a waiting screen — **except for the camp program types (camp, camp_swt)**: a Camp or Camp SWT host in pending_verification with an accepted operating agreement bootstraps into the full operational portal, with `verification_status: "pending"` in the context payload and a persistent, non-blocking banner reading "operational, verification pending — DS-2019 blocked". If suspended, the portal enters read-only mode. The portal never makes independent access decisions.

> **Camp verification states on /api/partner/host/context**
>
> For Camp and Camp SWT hosts, `verification_status` on the bootstrap payload takes one of three portal-visible values: `verified` (no banner, DS-2019 issuance open), `pending` (full operational access, DS-2019 issuance refused with 403 `HOST_VERIFICATION_REQUIRED`, banner shown), or `failed` (blocking flag open — operations halted, placements frozen, portal renders the blocked state). What happens to confirmed placements when verification ultimately fails — freeze plus the sponsor remediation/reassignment path — is specified in Camp Verification Failure Path. Hosts of program types other than Camp and Camp SWT never bootstrap while pending, so this field is only ever `verified` for them at session time. The mapping from the Rules Engine's Camp Host Verification Policy decisions onto these three portal values is defined normatively on the Rules Engine page (Derived Verification Outcome & DS-2019 Consumption).

### User CRUD Operations

Host Admins can manage their team through the following CRUD operations. All operations are scoped to the host organization and require Host Admin role:

> **Quota is not required to add host users (Aug 2026)**
>
> Adding host users no longer requires an allocated quota — host-user creation is decoupled from the quota model entirely. The governance rules that remain in force are the admin-exists rule and the active-agreement rule. See Quota Model &rarr; What Quotas Do Not Control.

| Operation | Endpoint | Description | Required Role |
| --- | --- | --- | --- |
| List Users | GET /api/partner/host/users | List all team members in the host organization with roles and status | Any host role |
| Invite User | POST /api/partner/host/users | Send an email invitation to a new team member with a specified role | Host Admin |
| Update Role | PATCH /api/partner/host/users/:id | Change a team member's role (e.g., promote Read-Only to Hiring Manager) | Host Admin |
| Deactivate User | DELETE /api/partner/host/users/:id | Deactivate a team member's access. Does not delete — preserves audit trail. | Host Admin |
| Reactivate User | PATCH /api/partner/host/users/:id with isActive true | Restore a previously deactivated team member's access | Host Admin |
| Resend Invitation | POST /api/partner/host/users/:id/reissue-invite | Revoke any outstanding invite token for this user (consumed_at-stamped with reason 'superseded'), mint a fresh 7-day invite token, and re-send the host.user_invited email. Row action surfaced only for users in the Not invited or Linked-pending-first-sign-in states; hidden once the user has activated or been deactivated. Throttled by a 60-second per-user cooldown (429 with code 'INVITE_RESEND_COOLDOWN' and a Retry-After header). See /docs/host-user-invitation#resend-invite. | Host Admin |

### Users Tab — Resend Invitation Visibility

The Resend Invitation row action on the Users tab (sponsor-side Host Management view and the equivalent Host Portal "Team" view) is gated by the user's current Portal Access state. Same visibility rules apply to both surfaces — the underlying endpoint is the only gate, and both UIs read it identically:

| Portal Access State | Action Visibility | Behavior |
| --- | --- | --- |
| Not invited | Shown, enabled | User row exists with password_hash = null and no live invite token. Clicking Resend mints a fresh invite token and dispatches the email — functionally the same as a first invite from this user's row. |
| Linked — pending first sign-in | Shown, enabled | User row exists with password_hash = null and an outstanding (possibly expired) invite token. Clicking Resend revokes any prior purpose='invite' token (consumed_at-stamped with reason 'superseded'), mints a new one, and re-sends the email. |
| Linked — activated | Hidden | User has accepted the invite and has a linked Auth0 identity (acceptedAt and auth0Sub are both set). The endpoint also rejects the call with 409 ALREADY_ACTIVATED for defense-in-depth if the UI is bypassed. |
| Deactivated | Hidden | User has been deactivated via DELETE /api/partner/host/users/:id. Resend is hidden because re-inviting a deactivated account would silently re-grant access without an explicit Reactivate decision. Host Admins must Reactivate first, which restores the row to its prior Linked state — Resend then becomes available again if the user had not previously activated. |

> **Throttle: 60-second cooldown per user**
>
> The endpoint enforces a per-user cooldown of 60 seconds. Calls within that window return 429 code INVITE_RESEND_COOLDOWN with a Retry-After header and details.retryAfterSeconds. The cooldown is enforced server-side only; there is no frontend countdown. The throttle protects the invitee's inbox (no "resend storm" from a frustrated admin clicking repeatedly) and keeps the audit log readable. See Host User Invitation — Resend Invitation for the full contract.

> **Invite write-boundary guard: ORG_NOT_HOST_EMPLOYER**
>
> Every host-user invite surface (host-portal admin invite and reissue-invite, staff add-user and invite-admin on an organization, and the org-create first-admin invite) goes through one invite primitive, which first checks that the organization has an active `employer` classification. An organization without one is rejected with HTTP 422 and code `ORG_NOT_HOST_EMPLOYER` (details carry `partnerOrgId` and `requiredTypeCodes`), because a host user attached to it would be invisible to Host Portal employer discovery. The number of program associations plays no part in the check. The permanent host delete, including its 409 `ORG_DELETE_DEPENDENCY` conflict, is documented on the Partner Organizations page.

### Host User Roles

| Role | Created By | Capabilities |
| --- | --- | --- |
| Host Admin | Sponsor (via invite) | Full org access: manage team, create job orders, confirm placements, view all data, manage housing, manage invoices and billing |
| Hiring Manager | Host Admin | Create and manage job orders, view capacity and fill rates, confirm placements, submit evaluations |
| Finance | Host Admin | Read-only across the Host Portal (profile, positions, reservations, placements, organisation and team views); no manage access on any surface, including invoice / billing / payment. No team management; no operational notifications by default. The role is labelled Finance Manager in the `partner_user_role` lookup. On the Hub it is an active, recognised host role: employer discovery and selected-host authorization accept it, the legacy role guard treats it as equivalent to Read Only (any route that allows `readonly` also allows `finance`), and its persisted capabilities are limited to `host.org.view`, `host.users.view`, `host.profile.view`, `host.positions.view`, `host.reservations.view` and `host.placements.view`. No manage capability is granted, and write guards that list only `admin` and `recruiter` stay closed to it. |
| Read-Only | Host Admin | View placements, job order status, and participant information — no write actions |

### Per-User Role Enforcement

Role enforcement is applied at the API level on every request. The Host Portal middleware validates the user's role against the requested action before forwarding to Globus:

| Action | Host Admin | Hiring Manager | Finance | Read-Only |
| --- | --- | --- | --- | --- |
| View placements & participants | | | | |
| View job orders | | | | |
| Create / edit job orders | | | | |
| Confirm placements | | | | |
| Confirm arrivals | | | | |
| Submit evaluations | | | | |
| Manage housing | | | | |
| Manage team members | | | | |
| Upload compliance documents | | | | |
| View invoices & billing | | | | |
| Manage invoices & billing | | | | |
| Configure settings | | | | |

> **Enforcement Is Server-Side**
>
> The Host Portal hides UI elements that the user's role does not permit, but the actual enforcement happens server-side. Even if a user bypasses the UI, Globus will reject unauthorized requests with a 403 response.

### Host Scope Model

Host users are scoped to their organization. The scope hierarchy ensures complete data isolation:

s organization ID (X-Host-Org-Id). Cross-org data access is impossible.,
Program Scope / Hosts only see programs they are authorized for via their agreement. Unauthorized programs are invisible.,
Season Scope / Within authorized programs, hosts only see seasons defined in their agreement scope.,
Participant Scope / Hosts see only participants placed at their organization. No visibility into unplaced applicants or other hosts

**Program visibility diagnostic.** `GET /api/admin/partners/organizations/:orgId/program-visibility` (permission `partners.view`) is a read-only staff diagnostic for a host that sees no programs. An unknown organization returns 404. Otherwise it returns `orgId`, a `verdict`, `authorizedPrograms` (what the Host Portal lists, from the agreement-scope resolver), `associatedPrograms` (active program associations staff assigned), `associatedNotAuthorized`, `authorizedNotAssociated`, an `agreementScope` summary (active agreement count, effective org and parent inheritance, unscoped agreement ids, `scopeEmpty` and its reason) and `evaluatedAt`. Verdicts: `programs_visible`, `no_program_associations`, `no_active_agreement`, `agreement_scope_missing_programs`, `agreement_scope_excludes_associations`.
### Host Enforcement Model

Host Portal access control differs from internal Globus RBAC. It uses relationship-based authorization rather than role-based access:

| Layer | What It Controls | Enforced By |
| --- | --- | --- |
| Host Agreement | Which programs, seasons, and capacity limits the org has | Globus agreement engine |
| User Role | What actions the individual user can take (Admin/Hiring Manager/Finance/Read-Only) | Host Portal middleware |
| Organization Ownership | Data isolation — users only see their own org's data | API middleware (X-Host-Org-Id) |
| Globus Enforcement | Business rules, compliance gates, capacity limits | Server-side validation on every mutation |

## Performance & Risk Scoring

Sponsors must monitor host quality over time. Performance scoring ties directly into the Host Agreement model — a host with declining quality metrics may have their agreement downgraded, placed on probation, or suspended entirely.

### Monitored Metrics (Host Slice of the 10-Metric Model)

The full 10-metric performance model (5 shared, 2 recruiter-only, 3 host-only) and the Critical / Warning / Acceptable tiering live on the canonical page at Partner Agreements &rarr; Performance & Adherence . Hosts are scored on the 5 shared metrics plus 3 host-only metrics (8 total).

| # | Metric | Origin | Host Read |
| --- | --- | --- | --- |
| 1 | Submission Quality | Shared | Rate of host-submitted records (job orders, evaluations) that pass review without rework. |
| 2 | Compliance Document Health | Shared | Currency of host-side compliance documents (housing inspections, certifications, insurance). |
| 3 | Reservation Discipline | Shared | Fill / expiry rate on reservations against the host's job orders. |
| 4 | Time-to-Action | Shared | Median time from sponsor request to host response on review items, evaluations, and arrival confirmations. |
| 5 | Audit Hygiene | Shared | Guard denials per 1k actions — high denial rate signals scope or capacity drift. |
| 6 | Placement Fill Rate | Host-only | Filled vs. requested positions across active job orders. |
| 7 | Site Visit Outcomes | Host-only | Pass rate on scheduled and unscheduled site visits. A failed visit is a probation-recommendation trigger. |
| 8 | Participant Welfare Signals | Host-only | Composite of evaluations, check-in compliance, escalations, and housing inspection results. |

> **Recommendation, Not Verdict**
>
> Tiered metrics produce a status recommendation. Lifecycle transitions still require an approver, a 10-character reason, and an audit entry. The system never autonomously downgrades a host agreement.

### Host Status Model

Host status maps to the canonical agreement state machine ( active &rarr; probation &rarr; suspended &rarr; expired &rarr; terminated ). New host agreements are created in `active` status immediately (one-click activate-and-send — no draft or pending-approval interim state). Probation cuts the effective capacity to 50% (rounded down) of the allocation and blocks DS-2019 issuance via the operating-agreement guard. Original allocation is preserved for reactivation. Auto-expiry runs at most every 15 minutes; every transition requires an approver and a 10-character reason.

| Status | Meaning | Capabilities | Trigger |
| --- | --- | --- | --- |
| draft | Agreement being prepared — legacy path only; new host agreements are created active | No portal access. Sponsor staff editing terms. | Legacy creation path only — new host agreements auto-activate on create |
| pending_approval | Awaiting sponsor approval — legacy path only | No portal access. Verification + approval workflow in flight. Transitions to: active, draft (returned for edits), terminated. | Legacy path only: draft submitted; reason ≥10 chars required on transition |
| active | Operating agreement in force | Full portal access, job order creation, placement confirmation. DS-2019 guard passes. Transitions to: probation, suspended, expired, terminated. | Sponsor admin creates new host agreement (auto-activated and sent for signature in one action); predicate (active + endDate ≥ today + scope covers Brand+Program+Season) satisfied |
| probation | Operating agreement under review | Existing placements continue, new job orders require enhanced review, capacity cut to 50% of original allocation (rounded down). DS-2019 guard BLOCKS new issuance. Transitions to: active, suspended, expired, terminated. | Failed site visit, high incident rate, performance threshold breach, or compliance flag (reason ≥10 chars) |
| suspended | Operating agreement halted | Read-only access to existing data, no new job orders or placements. DS-2019 guard blocks. Transitions to: active (via reactivation), terminated. | Repeated probation, critical compliance violation, or sponsor decision (reason ≥10 chars) |
| expired | Term reached endDate without renewal | Read-only. DS-2019 guard blocks. Terminal state. | Auto-expiry job (≤15 min) when endDate passes without renewal/extension |
| terminated | Permanently ended | No portal access. Historical records preserved for audit. Terminal state. | Agreement cancellation, legal action, or regulatory requirement (reason ≥10 chars) |

For the full RBAC model including internal sponsor roles, see RBAC & Permissions . For the parallel partner governance model, see Partner Portal Governance .

## Suspension & Offboarding Lifecycle

When a host organization is suspended, terminated, or their agreement expires, there are active job orders, placements, and on-site participants that must be handled. Host offboarding is more operationally complex than partner offboarding because hosts may have participants physically present at their location.

> **Participant Safety Is the Priority**
>
> In all host offboarding scenarios, the primary concern is participant welfare and safety. Active participants on-site must have continuity of support, housing, and supervision regardless of the host's organizational status.

### Suspend / Terminate Cascade Contract

When a host transitions to Suspended or Terminated, Globus runs a single transactional cascade that touches every dependent surface — active placements, host-uploaded compliance documents, and outstanding job orders — and writes a per-effect row to the host's lifecycle audit. The four cascade effects (plus deactivation of the host's active users, reversed on reinstatement), the confirmation-token requirement on terminate, and the permanent-termination guard are the contract; portals and ops tooling must build and test against them as a single behaviour, not as four independently-shipped features. The wire-level error envelopes for each effect are mirrored on Integration Contract &rarr; Host Suspend / Terminate Cascade .

| Cascade Effect | Trigger & Behavior | Error on Disallowed Mutation |
| --- | --- | --- |
| Placement Freeze | On entry to Suspended or Terminated, every placement currently in "Active" at this host is flagged as frozen in the same transaction. Reads continue. Any subsequent mutation (status transition, edit, cancel) on a frozen placement is rejected. Releases the freeze flag only via reactivation back to Active (suspend path only). | HTTP 409 placement_frozen_host_suspended |
| Host Document Read-Only Mode | Every host-uploaded document (host_documents row with uploadedSource = host_portal) is flagged read-only. GET / list / download / preview continue to succeed; POST replace, PATCH metadata, and DELETE return 409. Sponsor-staff-uploaded artifacts are untouched. Document expiration jobs continue to fire — the read-only flag does not pause expiry timers. | HTTP 409 document_locked_host_suspended |
| Outstanding Job Order Flag | Every job order at the host currently in "Submitted" or "In Review" is flagged for sponsor review (the sponsor ops queue surfaces them with a host-suspended badge). The flag does not auto-reject or auto-approve — it routes the work item to staff. Job orders in Draft, Approved/Active, or Closed are handled by the Job Order Freeze Behavior table below. | No new error code — surfaces in the sponsor ops review queue |
| Termination Confirmation Token | The terminate action requires an explicit confirmation_token body field. Calling the transition endpoint without it returns 400 and no state change occurs. Once Terminated, the host is permanently terminated: any attempt to transition back to Active (or any other state) is rejected. | HTTP 400 termination_confirmation_required · HTTP 422 termination_is_permanent on reactivation attempt |

> **Cascade Audit Shape**
>
> The cascade is a single transaction but produces one audit row per cascaded action on the host's lifecycle audit log, not a single “cascade completed” row. Reading the audit for Host A after a suspend yields the parent transition row plus N placement-freeze rows, M document-lock rows, and K job-order-flag rows. The action names are stable: partner.org.status_change — parent row (host org status transition: from/to + reason + actor) placement.frozen — one per placement, payload host_document.locked — one per host-uploaded document, payload job_order.flagged_for_sponsor_review — one per Submitted / In Review job order, payload partner_user.deactivated_by_suspension — one per active host user deactivated by the cascade (reversed on reinstatement by partner_user.reactivated_by_reinstatement). There is no `host.cascade` action prefix; query by these action names in the Audit Console.

> **Reactivation After Terminate Is Not Supported**
>
> Reactivation from Suspended &rarr; Active is the normal path and clears the placement-freeze and document-lock flags it created. Reactivation from Terminated is rejected with HTTP 422 termination_is_permanent regardless of caller, reason, or confirmation token. The re-onboarding path for an organization that was previously terminated is documented under Re-Activation Path and runs as a new host org with new IDs, not as a transition on the terminated record.

**Lifecycle enforcement error codes (Hub).**

- `HOST_TRIAL_CLASSIFICATION_UNAVAILABLE`, `HOST_PORTAL_MUTATION_RULE_UNAVAILABLE` and `HOST_PORTAL_MUTATION_RULE_INDETERMINATE` come from the trial browse-only middleware on Host Portal routes. It resolves the host's trial standing first; a host not classified as restricted is allowed without evaluating the rule. For a restricted host it evaluates the `host.portal.mutation_gate` rule and fails closed with `503`, the code in `error.code` and a diagnostic in `error.details`. `HOST_TRIAL_CLASSIFICATION_UNAVAILABLE`: the standing lookup failed, or the host is restricted but no trial agreement id resolved. `HOST_PORTAL_MUTATION_RULE_UNAVAILABLE`: rule evaluation failed or the rule returned no decision. `HOST_PORTAL_MUTATION_RULE_INDETERMINATE`: the decision outcome was neither `pass` nor `fail`. The host is shown a generic retry message, not the rule detail.
- `MISSING_REASON_CATEGORY` (400): a transition to `suspended` or `terminated` without a `reasonCategory` of `non-compliance`, `voluntary_withdrawal`, `contract_expiration` or `other`.
- `HOST_AGREEMENT_CAPACITY_EXCEEDED` (409): a job order submit (admin create that is not a draft, and the submit transition) whose positions plus the host's committed positions for the program and season exceed the ceiling on the covering active host agreement. `details` carry `agreementId`, `programId`, `seasonId`, `ceiling`, `committed` and `requested`. No ceiling configured means no check.
- `HOST_ORG_LIFECYCLE_TRANSITION_FAILED` (409): submitting an active host for verification (`POST /api/admin/host-verifications/:id/submit`) aborts because the active to pending_verification lifecycle transition failed.

### Job Order Freeze Behavior

When a host is suspended or terminated, job order operations are frozen:

| Job Order Status | On Suspension | On Termination | On Expiry |
| --- | --- | --- | --- |
| Draft | Frozen. Host cannot edit or submit. | Auto-discarded after 14-day hold. | Frozen during grace period. Auto-discarded after. |
| Submitted / In Review | Review paused. Sponsor can approve or reject. | Auto-rejected with system reason. | Review continues. Approved before expiry = valid. |
| Approved (capacity activated) | Job order paused. Removed from matching pool. | Job order cancelled. Unfilled positions released. | Job order remains active until hold timers expire. |
| Active (positions being filled) | New reservations blocked. Existing reservations continue. | Unfilled positions cancelled. Existing reservations reviewed by sponsor. | Continues normally until season end. |
| Closed | No change. Historical record. | No change. Historical record. | No change. Historical record. |

### Active Placement Continuity

Confirmed placements and on-program participants are never abandoned:

### Reservation Release & Capacity Re-Pooling

Unconfirmed reservations and unfilled positions are handled based on the offboarding type:

On Suspension: Job orders are paused (removed from matching pool). Existing reservations are frozen — sponsor can confirm or release individually using the structured reason catalog below. / On Termination: All unfilled job orders are cancelled. Unconfirmed reservations are released after a 48-hour sponsor review window using the structured reason catalog below; released capacity re-pools to other hosts' job orders under that same vocabulary. / On Expiry: Job orders follow their normal lifecycle. No special release behavior — the standard hold policy timers apply. / Cancelled job order capacity does not transfer to other hosts. Other hosts must create new job orders to fill demand. / Applicants who had reservations at a terminated host are flagged in the sponsor ops queue for re-matching.
#### Release Reservation — Reason Category Catalog

The Release Reservation dialog on the staff-facing host management screen (Applicants tab) requires a structured Reason Category on every release. The catalog below is the canonical option list — in this exact order and wording:

- Participant interviewed poorly
- Unsatisfactory skill level
- Participant declined
- Participant non responsive
- No longer need this skill
- No show to interview
- Other
Reason Category is required to release a reservation — the dialog cannot submit without one selected. / The note is optional for every reason, including Other (the dialog enforces no minimum length and the server treats the note as optional). / The chosen reason is recorded on the released-reservation audit row alongside the optional note, the actor, and the released capacity delta.
This catalog is the canonical source for any portal, queue, or report that surfaces a release-reservation reason. Strings must match exactly — no portal-side relabels, no localised synonyms, no BI-side remapping.
### Participant Welfare Handoff

Because hosts have participants physically on-site, offboarding requires explicit welfare handoff procedures that do not apply to recruitment-only partners:

> **Participant Welfare Is Non-Negotiable**
>
> Host offboarding cannot proceed without a documented welfare handoff plan for every active participant. The system blocks host termination until the sponsor confirms that welfare checks are complete and alternative arrangements are documented.

### Data Handoff Procedures

Host data access changes immediately upon status change:

| Data Type | Host Access After Offboarding | Sponsor Access |
| --- | --- | --- |
| Job Orders | No access (API returns 403) | Full read/write. Can reassign to new host. |
| Job Order Capacity | No access | Full access. Can cancel existing or create replacement job orders. |
| Placement Records | No access | Full access. Active placements managed by sponsor. |
| Participant Data | No access | Full access. Evaluations and check-ins managed by sponsor. |
| Housing Records | No access | Full access. Housing compliance managed by sponsor. |
| Site Visit History | No access | Historical records retained permanently. |
| Audit Trail | No access | Complete audit trail retained permanently. |

### Pipeline Cleanup Automation

Scheduled jobs resolve orphaned records after host offboarding:

### Notification Flows

Host offboarding triggers notifications to multiple stakeholders:

| Recipient | On Suspension | On Termination | On Expiry |
| --- | --- | --- | --- |
| Host Admin | Immediate email: access suspended, reason, next steps | Immediate email: agreement terminated, data retention policy | 30-day warning, 7-day warning, expiry confirmation |
| Host Users | Next login shows suspension notice | Next login shows termination notice | No direct notification |
| Active Participants | Direct sponsor notification about new point of contact | Direct sponsor notification about transfer/continuation plan | No notification if program continues normally |
| Recruitment Partners | Notification that job orders at this host are paused | Notification that job orders at this host are cancelled | No notification if job orders close normally |
| Sponsor Ops Team | Dashboard alert + email: host suspended, N active participants, N open job orders affected | Dashboard alert + email: host terminated, welfare handoff required for N participants | Dashboard alert: host agreement expiring, renewal action needed |

### Grace Period Rules

Host offboarding grace periods account for the physical presence of participants:

### Re-Activation Path

A suspended host can be reinstated after corrective actions:

> **Terminated Hosts Cannot Be Re-Activated**
>
> Termination is permanent. If a previously terminated host needs to resume operations, a new Host Organization and Host Agreement must be created, including a fresh site visit. Historical records from the terminated entity are retained but not linked to the new organization.

## Open Ecosystem Alignment Items for Globus Engineering

Open inconsistencies between this docs site and the canonical engineering reference attached_assets/hosts-and-job-orders.md that need an engineering decision in the Globus codebase before this site can mirror them without a follow-up update. Each item links to a placeholder Asana ticket; please file the real ticket and replace the link as work is scheduled.

### Naming / Contract Drift

- Activity-log endpoint name. Earlier drafts of these docs referred to a shorter host-activity path under /api/host/...; the canonical reference is GET /api/partner/host/activity-log. Site is updated; engineering should retire any older path or document a permanent alias. [Asana #placeholder-218-01]
- Notification key prefix — resolved. The canonical prefix is host.* (shared/hostNotificationTypes.ts), matching the codebase dispatcher, templates, and host-portal subscriber. The docs site has been aligned: the legacy employer.* prefix is retired and all event keys now read host.*. [Asana #placeholder-218-02]
- Reservation expiry timing. Earlier copy named this reservation_3_day_warning; the canonical event host.reservation_expiring fires at 24h pre-expiry. Confirm engineering intent matches 24h. [Asana #placeholder-218-03]
- Document expiry event names. Site previously used plural documents_expired / documents_expiring_soon; canonical events are singular document_expired / document_expiring_soon (one per doc). Confirm delivery semantics (per-doc vs digest). [Asana #placeholder-218-04]
### State-Machine / Contract Hazards

- partially_approved status spelling. The canonical reference uses a verb-form spelling in one place inside the state diagram, while the rest of the codebase consistently reads the past-participle status partially_approved. The docs site has standardized on the past-participle form; confirm the source code matches and remove the verb-form spelling from the diagram. [Asana #placeholder-218-05]
- revert_to_draft semantics. Reference says it resets all three approval gates to pending while preserving history. Confirm history-preservation behavior is actually implemented (gate audit log + amendments untouched) and surface a tested expectation. [Asana #placeholder-218-06]
- Auto-transition to approved requires host-verification pass. Spec is clear, but there is no documented behaviour for the case where host_verifications.status = needs_review while all three gates are approved. Today the order would stall in in_review with no UI signal. [Asana #placeholder-218-07]
- Repeat-host site-visit exemption scope. getCompletedSiteVisitForHostOrg matches on host + program. There is no documented age cap on the prior visit; a 5-year-old pass would satisfy the gate. Decide whether to add a recency window. [Asana #placeholder-218-08]
- Capacity counters auto-restore on un-pause. When a paused job order is resumed after a host comes off probation/suspension, the docs say it is “not auto-restored” but the counters are. Confirm behaviour for half-filled paused orders. [Asana #placeholder-218-09]
### Domain-Model Boundary Issues

- host_housing vs housing_leads. Two distinct concepts (managed inventory vs per-job-order candidate properties). Site now distinguishes them — engineering should ensure storage modules and route trees do not cross-pollinate (no joins from host_housing into job_order_housing_leads outside of an explicit “graduate to inventory” flow). [Asana #placeholder-218-10]
- Host-document mount path. Resolved. Host document endpoints are now documented under the canonical /api/partner/host/documents* prefix on the Host Document Center. The legacy /api/host/documents* mount is retained as a deprecated alias wired to the same handlers (no redirect) that adds Deprecation, Sunset and Link successor-version headers (RFC 8594, sunset 2026-10-31) and will be removed once portal traffic on it reaches zero. [Asana #placeholder-218-11]
- Cross-context joins. Domain-model skill says routes never touch Drizzle directly for these aggregates, but there is no automated lint/check enforcing that. Consider a test or rule that catches a Drizzle import in a route file under server/routes/jobOrders/*. [Asana #placeholder-218-12]
### Lifecycle Terminology Drift

- Operating-agreement portal acceptance vs DS-2019 predicate. Two layers of the same control surface (per the new subsection above). Confirm the canonical reference is updated to call out both layers explicitly so portal teams and SEVIS teams use the same vocabulary. [Asana #placeholder-218-13]
- participant_recommended vs participant_recommended_digest. Reference enumerates both events but does not specify default delivery. Site documents per-event as immediate and digest as opt-in default; engineering should confirm default-delivery configuration and per-host opt-out semantics in notificationJobs.ts. [Asana #placeholder-218-14]
## Host Account Manager Assignment

Every host org carries an optional Account Manager — a single sponsor-side staff user responsible for that host’s overall partner relationship: agreement renewal cadence, escalation triage, periodic check-ins, and the human contact named on outbound host-targeted notifications. The AM sits on the host org row itself, not on the host’s portal users; it is orthogonal to agreement scope (an AM may own hosts across brands and programs) and orthogonal to host-portal RBAC (the AM never logs into the Host Portal — their identity is a Globus staff user, gated by sponsor-side roles documented in RBAC and Governance Model ).

**No participant cascade.** Host Account Manager assignment does not cascade to participants. Changing a host Account Manager (org-level or program-scoped), region Account Manager reassignment, host redistribution, and staff Reserve/Hire leave participant Account Managers untouched; participant ownership changes only through the participant assignment flow, and historical cascaded assignments are not reversed. Region changes update non-overridden host organizations only.

> **Manual-only in v1**
>
> Assignment is always a named human action. There is no auto-assign rule, no load-balancer, and no round-robin. The Applicant Assignment model on Applicant Lifecycle &rarr; Assignment Model ships with an optional auto-assign path; the host-org AM model does not. Auto-assignment of AMs is explicitly out of scope — revisit only after the manual model has lived in production for one full season.

> **Source column traces applicants back to the recommending AM**
>
> The Host Management Applicants tab carries a Source column whose Recommended by AM value is derived from the participant_recommendations row that attached the applicant to this host. That row already names the recommending sponsor-staff user — so AM-recommended applicants are traceable back to the recommending AM without a new column on the recommendations table or on the applicant. Full column contract, enum, derivation rule, and filter facet on Partner Organizations &rarr; Applicants Tab — Source Column .

### Data Model & State Machine

Two additive columns on partner_organizations plus one append-only history table. No new lookup tables; the AM is a pointer into the existing users table.

| Column / Table | Definition |
| --- | --- |
| partner_organizations.account_manager_user_id | Nullable FK → users.id. The currently-assigned AM. NULL means Unassigned. Constrained to staff users (a foreign-key check fails for host_user / partner_user rows). |
| partner_organizations.account_manager_assigned_at | Nullable timestamp. Set whenever account_manager_user_id transitions from NULL → value or value → different value. Cleared back to NULL when account_manager_user_id is cleared. Indexes the staleness column on the roster. |
| host_organization_assignment_history (table) | Append-only ledger mirroring the applicant_assignment_history pattern documented on /docs/applicant-lifecycle#assignment-history-audit. Columns: id, host_organization_id (FK), program_id (nullable; NULL = org-wide change), from_account_manager_id (nullable), to_account_manager_id (nullable), changed_by_user_id (nullable FK, the actor), changed_at, reason (nullable text). No rows are ever updated or deleted; correction is by writing a new row. |
| host_account_managers (table) | Program-scoped account manager assignments: host_organization_id and program_id (both cascade FKs), account_manager_user_id (cascade FK to users) and assigned_at. Unique per host and program, so a host running several programs can have a different account manager per program. Removing a program-scoped account manager redistributes only that program's participants. |
| account_manager_regions (table) | Regions with a stable machine key and display name (both unique), an optional account_manager_user_id (set null when the staff user is deleted) and sort_order. Assigning or changing a region's account manager cascades to every non-overridden host in the region and their active participants through server/services/applicantAssignment.ts. |
| account_manager_region_states (table) | State to region membership: region_id (cascade) and a USPS state_code that is unique, so each state belongs to exactly one region. |
| account_manager_region_members (table) | A region may have several account managers: region_id and account_manager_user_id (both cascade) plus is_primary. Unique per region and user, with at most one primary per region; the primary is kept in step with account_manager_regions.account_manager_user_id and drives the host cascade. |
| host_organization_support_members (table) | Many-to-many Support Team for a host: host_organization_id, staff_user_id, assigned_by_user_id, reason and assigned_at. Unique per host and staff user. |
| host_organization_support_assignment_history (table) | Append-only ledger of support member changes: host_organization_id, staff_user_id, action (assigned or unassigned), changed_by_user_id, reason and changed_at. Written only by server/services/hostSupportAssignment.ts in the same transaction as the membership insert or delete. |

Three derived states — not stored as a column, computed from the two fields above:

- Unassigned — account_manager_user_id IS NULL. Default for every newly-created host org until an AM is named.
- Assigned — account_manager_user_id IS NOT NULL and no prior non-NULL row exists in host_organization_assignment_history. First-time assignment.
- Reassigned — account_manager_user_id IS NOT NULL and at least one prior non-NULL row exists in the history table (the AM has changed at least once since the org was first assigned).
> **Clearing is a first-class transition**
>
> Setting account_manager_user_id back to NULL is supported (off-boarding the prior AM with no immediate successor) and writes the host_organization.account_manager.unassigned audit action documented below. The derived state returns to Unassigned; the history row preserves the prior owner for audit purposes.

### Roster Column, Filter & Roll-Up Counts

The sponsor-side host roster (the host-org list surface inside Sponsor Org Operations Console ) gains one column and one filter facet driven by the data model above.

| Surface | Contract |
| --- | --- |
| Account Manager column | Renders the AM's displayName as a link to the AM Workload drill-in. Empty cell with muted 'Unassigned' label when account_manager_user_id IS NULL. Column is sortable by AM display name (NULLs sort last); the column header carries the same filter affordance as the rest of the roster. |
| Account Manager filter facet | Single-select dropdown: 'All Account Managers', a server-recognised 'Unassigned' option (matches account_manager_user_id IS NULL; the API accepts accountManagerUserId=unassigned\|null\|all\|`<userId>`), then each staff user as a specific account manager. There is no 'Me' preset. Composes with the existing brand / lifecycle / autonomy facets via AND. |
| Roll-up counts (drill-in only, not roster cells) | The drill-in surfaces four derived counts per AM, each a filtered read with no new persistence: active placements (placements.status = 'active' joined through job_orders.host_org_id), open job orders (job_orders.status IN ('submitted', 'in_review', 'open')), applicants on-program (applications.status = 'on_program' joined through placements), and open queue items (queue_memberships.exited_at IS NULL across host-scoped queue definitions). Counts are page-level computed on read; staleness is bounded by the page cache TTL documented on the drill-in section. |

Filter persistence and saved-view semantics follow the existing roster pattern; nothing AM-specific is invented here. The roster never displays the roll-up counts inline — surfacing them per-row would require a fan-out join on every roster read and is intentionally pushed into the drill-in.

### Account Manager Workload Drill-In

*Status: design spec, not built. No `/staff/account-managers/:userId` route or per-AM workload endpoint exists in the back office; a Team Workload tab exists under Command Center instead.*

Clicking an AM name anywhere in the staff back-office routes to a per-AM workload page. The page is entirely a filtered view over existing tables — no new persistence layer, no denormalised workload table, no scheduled rebuild job.

URL shape

`/staff/account-managers/ `
The userId path segment is the staff user’s primary key. A 404 is returned when the id does not resolve to an active staff user; the page never silently degrades to an empty drill-in.

The drill-in renders five panels, top to bottom. Every panel is a read-side composition over an existing source of truth — the panels are named here purely for layout/labelling consistency, not as new domain objects.

| Panel | Source of truth | Contract |
| --- | --- | --- |
| Header — Workload Roll-Up | Derived counts (see Roster section above) | Four roll-up tiles: Active Placements, Open Job Orders, Applicants On-Program, Open Queue Items. Each tile is a link that pivots to a filtered list of that entity scoped to AM-owned hosts. |
| Hosts in Portfolio | partner_organizations WHERE account_manager_user_id = :userId | Paginated list of every host org this AM currently owns. Same row shape as the main host roster (lifecycle, autonomy, brand badge) plus a small days-since-assignment chip per row sourced from account_manager_assigned_at. |
| Recent Host Notes | host_organization_notes WHERE host_organization_id IN (portfolio host ids) AND deleted_at IS NULL ORDER BY COALESCE(updated_at, created_at) DESC LIMIT 50 | Filtered view over the Host Notes log already documented on /docs/host-camp-overview#host-notes. Each entry links back to the source host org. Read-only here; create/edit happens on the host org page itself. |
| Recent Audit Activity | audit_logs WHERE userId = :userId OR (entityType = 'host_organization' AND entityId IN (portfolio host ids)) ORDER BY timestamp DESC LIMIT 100 | Filtered view over the same audit_logs table backing /docs/audit-console. Two stripes: actions the AM took (left-aligned) and actions taken on portfolio hosts by anyone else (right-aligned), interleaved by timestamp. Cross-links each row to the Audit Console with the same filter pre-applied. |
| Open Queue Items | queue_memberships JOIN queue_definitions WHERE entityType = 'host' AND exited_at IS NULL AND host_id IN (portfolio host ids) | Filtered view over the Queue Workspace contract documented on /docs/queue-system. Surfaces the host-scoped queue rows whose primary owner is one of the AM's portfolio hosts. No new queue category — composes with the existing Ready / Waiting / Blocked / Aging axis. |

> **No parallel data model**
>
> Every panel above resolves against an existing table. There is no account_manager_workload materialised view, no nightly rebuild job, and no separate workload-events stream. The drill-in is intentionally a read composition so it can never drift from the underlying systems of record.

### Audit Vocabulary

Every AM mutation writes one row to host_organization_assignment_history and one row to audit_logs via the shared recordAudit() helper documented on the Audit Console . Every action below is written against entityType: partner_organization.

| Action | Emitted when |
| --- | --- |
| host_organization.account_manager.assigned | An account manager is set on a host org: a first assignment (NULL to value) or a change to a different AM (there is no separate reassigned action). The audit row carries the new account manager user id; program-scoped changes also carry the program id. |
| host_organization.account_manager.unassigned | The account manager is cleared (value to NULL). When the removal attempts automatic redistribution and finds no eligible program manager, the row records why (reason no_eligible_program_manager). |
| host_organization.account_manager.redistributed | Automatic redistribution to a program manager after the AM is removed (trigger auto_redistribution). |
| host_organization.account_manager.reverted_to_region | A host is reverted to its region default account manager, clearing the manual override flag. |

The Audit Console Contract block (client/src/pages/audit-console.tsx) names the host_organization.* family and cross-links back to this section.

### Notification — host.account_manager_assigned

A single staff-targeted notification fires on AM assignment changes so the incoming AM has a system-of-record signal that a new host now sits in their portfolio. The host itself is not notified — AM is a sponsor-internal relationship and announcing it to the host is a human-curated email handled outside this contract.

| Field | Value |
| --- | --- |
| Event key | host.account_manager_assigned |
| Audience | Staff (the newly-assigned AM only — the prior AM is not notified). Recipient resolution: account_manager_user_id after the write. |
| Channel | In-app notification (staff back-office). No email and no SMS in v1. |
| Fires on | Fires whenever host_organization.account_manager.assigned is written, for both a first assignment and a change to a different AM. An automatic redistribution to a program manager fires host.redistributed_to_program_manager instead. Does not fire when the account manager is cleared (.unassigned; no one to notify) and does not fire when the request names the AM who is already assigned (no-op). |
| Payload | host_organization_id, host_organization_name, assigned_by_user_id, assigned_at, reason (nullable) — enough for the in-app card to render without a follow-up fetch. |
| Delivery model | Post-commit, single fire per assignment transaction; fan-out failure never rolls back the underlying assignment write. Mirrors the delivery rule documented on /docs/host-governance#operating-agreement-renewal-api. |

Catalogued on Employer Notifications &rarr; host.account_manager_assigned and listed in the host-event-types table on Notification & Communication Model .

### Implementation Follow-up — P12-P1 Asana Tickets

This page is a docs-site spec extension only — no runtime code lives in this repo. The five tickets below are drafted in the house format documented in docs/asana-ticket-standard.md so a P12-P1 owner can copy-paste straight into Asana when the work is picked up. All five route to P12-P1 (Phase 1: Partner: Employer (Host) Management).

P12-P1 Ticket A

Host management: account_manager_user_id column + host_organization_assignment_history ledger

FOR PRODUCT OWNERS / STAKEHOLDERS
Invisible to end users, but it is the foundation under every other Account Manager
ticket. Without the column and the history ledger, the roster filter, the workload
drill-in, the audit vocabulary, and the in-app notification all have nothing to read.
----------------------------------------
BACKGROUND
"partner_organizations" today carries no notion of a sponsor-side Account Manager.
Workload is tracked informally in spreadsheets, and when an AM departs there is no
durable record of who used to own which host. The applicant-side has a comparable
pattern documented in "applicant_assignment_history" but it has no host equivalent.
WHAT THIS TICKET DOES
Adds two additive columns to "partner_organizations" — "account_manager_user_id"
(nullable FK to users.id, constrained to staff users) and "account_manager_assigned_at"
(nullable timestamp). Adds the new append-only "host_organization_assignment_history"
table mirroring the "applicant_assignment_history" shape. No backfill — existing
host orgs are Unassigned at migration time. Companion tickets: "Host management:
roster column + AM filter facet + workload roll-up counts" and "Host management:
host_organization.account_manager.* audit vocabulary wired through recordAudit()".
ACCEPTANCE CRITERIA (Gherkin)
Feature: Host org Account Manager data model
Scenario: account_manager_user_id is constrained to staff users
Given a host org row "H1"
When a write sets "account_manager_user_id" to a "host_user" id
Then the write is rejected with a foreign-key check failure
Scenario: account_manager_assigned_at tracks every assign transition
Given host org "H1" with "account_manager_user_id" NULL
When "account_manager_user_id" is set to staff user "U1"
Then "account_manager_assigned_at" equals the transaction commit timestamp
Scenario: history ledger is append-only
Given a row in "host_organization_assignment_history" for host "H1"
When an UPDATE on that row is attempted
Then the database denies the update
And the only supported mutation is INSERT
Scenario: Clearing the AM preserves the prior owner in history
Given host org "H1" with "account_manager_user_id" = "U1"
When "account_manager_user_id" is set back to NULL
Then a new "host_organization_assignment_history" row exists with
"previous_user_id" = "U1" and "new_user_id" = NULL
And the host org row has "account_manager_assigned_at" = NULL
P12-P1 Ticket B

Host management: roster Account Manager column + filter facet ("Unassigned" / "Me" presets) + workload roll-up counts

FOR PRODUCT OWNERS / STAKEHOLDERS
Sponsor staff finally get a one-screen view of "who owns which host" — they can
filter the host roster by AM, jump straight to their own portfolio with the "Me"
preset, and see which hosts have no owner at all so coverage gaps stop hiding.
----------------------------------------
BACKGROUND
The sponsor-side host roster surfaces lifecycle, autonomy, brand, and compliance
columns but has no concept of AM ownership. There is no way to list "Unassigned
hosts" or "hosts I personally own" without an offline spreadsheet, and the roll-up
counts (active placements, open job orders, applicants on-program, open queue items)
have to be re-computed by hand for performance reviews.
WHAT THIS TICKET DOES
Adds an "Account Manager" column to the host roster rendering "users.displayName"
as a link to the AM Workload drill-in, with a muted "Unassigned" empty state.
Adds a multi-select filter facet over the distinct AM set; the picker pins two
server-owned presets at the top — "Unassigned" (matches account_manager_user_id
IS NULL) and "Me" (resolved per-request to the viewer's userId, never baked into
the saved view). Adds the four roll-up counts described in the docs page section
"Roster Column, Filter & Roll-Up Counts" — each is a derived read, no new table.
Companion ticket: "Host management: Account Manager workload drill-in page".
ACCEPTANCE CRITERIA (Gherkin)
Feature: Host roster Account Manager column & filter
Scenario: Column renders the AM as a link
Given host org "H1" has "account_manager_user_id" = "U1" with displayName "Karin"
When the staff host roster renders row "H1"
Then the AM cell shows "Karin" as a link to "/staff/account-managers/U1"
Scenario: Unassigned cell shows muted label
Given host org "H2" has "account_manager_user_id" NULL
When the staff host roster renders row "H2"
Then the AM cell shows "Unassigned" in muted styling
Scenario: "Me" preset filters to the viewer's portfolio
Given staff viewer "U7" owns hosts "H1" and "H5"
When the viewer selects the "Me" preset on the AM filter facet
Then the visible roster is exactly "H1" and "H5"
Scenario: "Unassigned" preset never collides with a real AM named "Unassigned"
Given a staff user with displayName "Unassigned" exists
When the viewer selects the "Unassigned" preset
Then the filter resolves to "account_manager_user_id IS NULL"
And does not match hosts assigned to the staff user named "Unassigned"
P12-P1 Ticket C

Host management: Account Manager workload drill-in page (/staff/account-managers/ )

FOR PRODUCT OWNERS / STAKEHOLDERS
Gives sponsor leadership a single-link view of an Account Manager's entire host
portfolio — every host they own, every recent note across those hosts, every audit
event touching them, and every open queue item across the portfolio.
----------------------------------------
BACKGROUND
AM workload today is reconstructed by joining the host roster, the host notes log,
the audit console, and the queue workspace by hand. There is no per-AM landing page
that composes those four sources, and the existing pages cannot be filtered by AM
because "account_manager_user_id" did not exist until the foundation ticket.
WHAT THIS TICKET DOES
Adds a new staff page at "/staff/account-managers/ " that renders five
panels exactly as documented in the docs page section "Account Manager Workload
Drill-In": a roll-up header (four count tiles), Hosts in Portfolio, Recent Host
Notes, Recent Audit Activity, and Open Queue Items. Every panel is a filtered
read over an existing source-of-truth table — no materialised view, no nightly
job, no parallel workload model. A 404 is returned when the userId does not
resolve to an active staff user.
ACCEPTANCE CRITERIA (Gherkin)
Feature: Account Manager workload drill-in
Scenario: Page resolves the AM and renders the five panels
Given staff user "U1" owns hosts "H1", "H2", "H3"
When a viewer opens "/staff/account-managers/U1"
Then the page renders panels "Header — Workload Roll-Up", "Hosts in Portfolio",
"Recent Host Notes", "Recent Audit Activity", and "Open Queue Items"
Scenario: Unknown userId returns 404
Given no active staff user with id "999" exists
When a viewer opens "/staff/account-managers/999"
Then the response status is 404
And the page does not render an empty drill-in
Scenario: Hosts in Portfolio panel matches account_manager_user_id
Given staff user "U1" owns exactly hosts "H1" and "H2"
When the drill-in renders for "U1"
Then the Hosts in Portfolio panel lists exactly "H1" and "H2"
Scenario: Recent Host Notes panel excludes notes from non-portfolio hosts
Given staff user "U1" owns host "H1" but not host "H9"
And both "H1" and "H9" have recent notes
When the drill-in renders for "U1"
Then notes from "H9" do not appear in the Recent Host Notes panel
P12-P1 Ticket D

Host management: host_organization.account_manager.* audit vocabulary wired through recordAudit()

FOR PRODUCT OWNERS / STAKEHOLDERS
Invisible to end users, but it gives the Audit Console a complete, named record of
every AM ownership change — who reassigned which host to whom, and when. Without
this vocabulary, AM changes would be silent and post-incident reviews would have
nothing to read.
----------------------------------------
BACKGROUND
The shared "recordAudit()" helper already covers host overview edits, host
organization notes, Asana mutations, and MCP heartbeats. The applicant-assignment
family ("applicant.assignment.created" / ".reassigned" / ".auto_assigned") is the
closest analogue but writes against "entityType: applicant". There is no comparable
vocabulary for host-org AM changes.
WHAT THIS TICKET DOES
Adds action strings against "entityType: host_organization": "host_organization.account_manager.assigned" (a first assignment or a change to a different AM), "host_organization.account_manager.unassigned" (AM cleared) and "host_organization.account_manager.redistributed" (automatic redistribution). The endpoint that
mutates "partner_organizations.account_manager_user_id" writes one row to
"host_organization_assignment_history" and one row to "audit_logs" via
recordAudit() in the same transaction; the audit write is best-effort and never
fails the underlying mutation. Extends the Audit Console Contract block to name
the "host_organization.*" family and cross-link back to this docs section.
ACCEPTANCE CRITERIA (Gherkin)
Feature: Host AM audit vocabulary
Scenario: First-time assignment writes .assigned
Given host "H1" has "account_manager_user_id" NULL
When staff actor "U7" sets it to "U1"
Then audit_logs contains a row with action "host_organization.account_manager.assigned",
entityType "host_organization", entityId "H1", userId "U7", before null,
after carrying U1
Scenario: Changing AM writes .assigned
Given host "H1" has "account_manager_user_id" = "U1"
When staff actor "U7" sets it to "U2"
Then audit_logs contains a row with action "host_organization.account_manager.assigned"
And before carries U1
And after carries U2
Scenario: Clearing writes .unassigned
Given host "H1" has "account_manager_user_id" = "U2"
When staff actor "U7" sets it to NULL
Then audit_logs contains a row with action "host_organization.account_manager.unassigned"
And before carries U2
And after is null
Scenario: Audit write failure does not roll back the mutation
Given the audit_logs writer is throwing
When staff actor "U7" sets host "H1" AM from NULL to "U1"
Then the host_organization_assignment_history row is committed
And "partner_organizations.account_manager_user_id" is "U1"
P12-P1 Ticket E

Host management: host.account_manager_assigned in-app notification to the newly-assigned AM

FOR PRODUCT OWNERS / STAKEHOLDERS
The incoming Account Manager gets an in-app card the moment a host lands in their
portfolio, so they have a system-of-record signal instead of waiting for someone
to tell them by email or chat.
----------------------------------------
BACKGROUND
Notification fan-out for host-targeted events is already documented in
"shared/hostNotificationTypes.ts" and the Notification & Communication Model.
There is no staff-targeted host event today, and the AM has no automatic signal
when a new host is assigned to them — discovery is entirely informal.
WHAT THIS TICKET DOES
Adds a new staff-audience event "host.account_manager_assigned" wired to the
in-app channel only (no email, no SMS in v1). Fires post-commit of the AM
mutation on the .assigned and .reassigned transitions; does not fire on
.unassigned. Payload carries . Fan-out failure never rolls back
the underlying assignment write. Catalogued on the Employer Notifications page
and listed in the host-event-types table on the Notification & Communication
Model page.
ACCEPTANCE CRITERIA (Gherkin)
Feature: host.account_manager_assigned in-app notification
Scenario: Fires to the new AM on first assignment
Given host "H1" has "account_manager_user_id" NULL
When staff actor "U7" sets it to "U1"
Then exactly one in-app notification with event "host.account_manager_assigned"
is delivered to recipient "U1"
And no notification is delivered to "U7"
Scenario: Fires to the new AM on reassignment, not the prior one
Given host "H1" has "account_manager_user_id" = "U1"
When staff actor "U7" sets it to "U2"
Then exactly one in-app notification with event "host.account_manager_assigned"
is delivered to recipient "U2"
And no notification is delivered to "U1"
Scenario: Does not fire on clear
Given host "H1" has "account_manager_user_id" = "U2"
When staff actor "U7" sets it to NULL
Then no "host.account_manager_assigned" notification is emitted
Scenario: Fan-out failure does not roll back the assignment
Given the in-app notification delivery is throwing
When staff actor "U7" sets host "H1" AM from NULL to "U1"
Then "partner_organizations.account_manager_user_id" is "U1"
And the audit_logs row for "host_organization.account_manager.assigned" is present
## Host Management Company Activity — Audit Read-Model Contract

> **Implemented (development HEAD)**
>
> The staff-only company activity read model is live in Globus. `GET /api/admin/partners/organizations/:orgId/activity` returns the paged feed and `GET /api/admin/partners/organizations/:orgId/last-staff-action` returns `{ lastStaffAction }` for a host; the host detail page renders a staff-only **Company activity** panel (filters for entity, actor, action code, date range and search, with Load more paging). It was specified by the *Host Management Audit Contract* (`docs/features/host-management-audit-contract.md`); the contract below still holds except where noted in the bullets. Both routes answer staff sessions only (an API key or host-portal credential gets `403`) and return `404` for an org that is not an active employer-type host.

Host Management needs a staff-only view of what happened to a host company, who did it, and when a human member of staff last touched it. Globus `audit_logs` is the source for host-company activity; the existing Host Portal activity log (`GET /api/partner/host/activity-log`) is a separate host-facing participant lifecycle feed and must not be repurposed as the staff feed.

### Association, Inclusion & Last Staff Action

- **Company association is resolved before pagination.** An event belongs to a host when its entity is the host organisation or its persisted relationship resolves to it, across these families: host identity (`partner_organization`); contacts and users (`partner_contact`, `partner_user`); agreements and quotas (`partner_agreement`, `partner_quota`, `quota`); documents and verification (`host_document`, `host_verification`, `host_verification_attempt`); job orders and amendments; housing and activity sites (`host_housing`, `activity_site`, `site_visit`); reservations and placements (`reservation_hold`, `placement`); and application / applicant / extension / work-item events only when their placement, reservation or job order resolves to this host. An event with no unambiguous host association is excluded, never guessed.
- **Included:** successful staff mutations and consequential staff decisions, labelled with the shared friendly vocabulary (*Host status changed*, *Job order updated*, *Agreement activated*, *Verification submitted*, *Placement cancelled*); the raw action code stays available to diagnostic consumers but is never the primary label. **Excluded:** reads, page views, searches, exports and filter changes; automated jobs, workers, scheduled notifications and reconciliation; portal-API-key and system/integration actors; duplicate delivery/outbox rows. As implemented, actions taken by host-portal users (other than reads) do appear in the Company activity feed, labelled with actor kind `host_user` ("Host company user" versus "Globus staff" in the panel) and filterable with `actorKind=host_user`; they never count as a staff touch. Automated events may still appear in an explicitly labelled system/verification history where one already exists, but never count as a staff touch.
- **Item shape:** `id`, `action`, `label`, `eventTime` (the immutable audit timestamp, not a read time), `entity {type,id,label}`, `actor {kind,id,name,identifier}`, `attribution {mode: direct | impersonated | proxy, performedBy, impersonatedUser}`, and concise redacted `details` (controlled reason plus meaningful before/after field changes — no raw payloads, secrets or unnecessary PII). Actor display resolves in batch at read time: staff name → email → preserved deleted-user identity → `[Deleted User]` + stored identifier → `System`/system role → `Integration (…last4)` (never presented as staff). Impersonated and proxy actions must make the initiating human unmistakable ("Alex Morgan acting as Jordan Lee" / "Alex Morgan via delegated access").
- **`lastStaffAction` has one derivation rule:** the newest event ordered by `(eventTime DESC, id DESC)` whose actor resolves to a human Globus staff account (direct, impersonated or proxy), that belongs to the host, and that is not a read, automated/system event or host-portal-user action. `null` renders **No staff activity recorded**. The host roster row summary and the host detail header consume the *same* projection and must agree for the same host and read time.

### Permissions, Pagination & Nexus Boundary

- **Both permissions required:** `partners.view` (Host Management) **and** `audit_log.view`, then the shared `enforceOrgScope` resolver for the host. Either permission alone → `403 FORBIDDEN` with no activity data; an out-of-scope host uses the platform's non-disclosing not-found/forbidden behaviour. A host-portal credential is never a substitute for staff authorisation.
- **Filters and paging:** `entityFamily` (`host`, `agreement`, `job_order`, `verification`, `participant`), `actionType`, `actorKind` (`staff`, `host_user`, `impersonated`, `proxy`), `from`, `until` (ISO timestamps with offset; `from` must not be after `until`), and free-text `search` over label, entity label, actor name/identifier and controlled reason; invalid filters return a validation error and filters never broaden scope. Opaque cursor carrying the event-time/id tie-breaker; newest-first; server-enforced max page size of 25 (`limit` 1–25, default 25); `nextCursor = null` ends the feed and `hasMore` reports whether more remain; changing filters starts a new cursor chain.
- **UI states:** roster row shows a compact **Last staff action** (label, relative time, actor) with skeleton / *No staff activity recorded* / non-blocking *Last staff action unavailable*; detail page shows a staff-only **Company activity** panel with loading, populated, empty (*No company activity recorded*), retryable *Company activity unavailable*, and access-denied states (no event data rendered when denied).- **Nexus boundary:** this is a Globus projection over Globus `audit_logs`. The Nexus audit store and documentation service remain separate systems with their own retention, permissions and queries — there is no unified cross-store timeline, and no promise that a Nexus row appears in Company activity. General audit documentation may link here, but must not describe the feed as a Nexus table or API.

## Implementation Tickets — P12-P1 (Sponsor-Side Host Management)

Every governance behaviour documented above — the agreement spine, host CRUD, lifecycle state machine, activation gate, host-typed extensions, operating-agreement invariant + DS-2019 guard, renewal flow, autonomy A–D, RBAC, query-layer data boundaries with PII masking, 5-metric host scoring, and the suspend / terminate cascade — is delivered by the twelve tickets below, in execution order, on Asana project P12: Phase 1: Partner: Employer (Host) Management . The prior P12-P1 ticket set has been retired; these tickets replace it 1:1 and are dependency-wired in the order listed, so the dev portfolio rollup on Delivery Status / Delivery Overview shows them with no orphan stage rows.

| # | Ticket | Scope |
| --- | --- | --- |
| 1 | Partner agreements: shared spine (scope, dates, quotas) 1214895401573237 | partner_agreements table with scope (brand+program+season), start/end dates, quota, status (draft/active/ended). Quota enforced at write time; scope-mismatch and agreement-expired errors on downstream submissions. |
| 2 | Host org CRUD: sponsor-side create / edit / list employer partner organizations 1214895441985767 | Sponsor staff create / edit / list host orgs scoped by brand. Cross-scope create returns HTTP 403. All edits emit an audited before/after diff. Hosts rendered with a visible Host type badge in the staff list. |
| 3 | Host org lifecycle: state machine (Pending Verification → Active → Probation → Suspended → Terminated) 1214894915360537 | Codified transitions; Active ↔ Probation reversible; Terminated terminal. POST /api/partner-orgs/id/transition rejects invalid moves with invalid_lifecycle_transition. Immutable lifecycle audit log. |
| 4 | Host activation gate: 5-condition Pending Verification → Active transition + partner_organization.activated event 1214895380937181 | Five-check gate (EIN, identity completeness, primary contact, clean verification report, zero blocking flags). Publishes canonical partner_organization.activated event exactly once via idempotency key; activation_gate_failed payload lists failed_conditions on rejection. |
| 5 | Partner agreements: host-typed extensions (capacity, compliance, verification, adherence, site visits, housing) 1214895996629466 | host_agreement_extensions table 1:1 with partner_agreements. Future-date validation on site visits. Recruiter-only fields rejected with field_not_applicable_to_host. POST/PATCH under /api/admin/partners/agreements/:agreementId/host-extension. |
| 6 | Host operating-agreement invariant + DS-2019 issuance guard 1214895442034656 | Partial unique index over (partner_org_id) WHERE agreement_type='operating' AND status='active'. API rejects second-active with operating_agreement_invariant_violation; DS-2019 issuance refuses on race; atomic end-then-activate path supported. |
| 7 | Host operating-agreement renewal flow 1214895997653791 | POST /api/hosts/id/operating-agreement/renew runs draft → validate → end prior → activate successor in one transaction. Validation failure leaves prior live. no_operating_agreement_to_renew when no active operating agreement exists. |
| 8 | Host autonomy framework (per-host autonomy_tier A–D) + capability gating 1214895401804964 | autonomy_tier column on partner_organizations with per-program default. Capability.requireAutonomy(tier) middleware gates server-side; below-tier requests return autonomy_tier_insufficient. Tier changes recorded on the lifecycle audit. |
| 9 | Host RBAC enforcement: Host Admin / Hiring Manager / Finance / Read-Only (sponsor-side capability matrix) 1214895441994171 | role_capabilities seeded for the four host roles. GET /api/rbac/capabilities?role=&partner_org_id= used by both Host Portal UI and every server-side check. Denials return capability_denied with the missing capability identifier. |
| 10 | Host data-boundary enforcement at the query layer + PII masking 1214895997401930 | Server-pushes partner_org_id into WHERE for every host-facing list endpoint — supplied parameters cannot widen scope. redactForRole() masks SSN / financial fields unless an explicit grant is present. CI regression matrix covers endpoint × same host, other host, no host . |
| 11 | Host performance scoring (5 host-side metrics: compliance, capacity utilization, evaluation quality, on-time submission, retention) 1214895996409777 | Nightly HostScoring.run(season_id) job. Append-only host_scores rows (host_org_id, season_id, score, metric_breakdown). Hosts with zero activity get no synthetic score. GET /api/hosts/id/scores returns latest revision plus breakdown. `season_id` is a required query parameter (400 if missing); no recorded score returns 404. `GET /api/hosts/:id/scores/revisions?season_id=&limit=` returns past revisions newest first (limit 1 to 100, default 20). `POST /api/admin/host-scoring/run` with body `{ seasonId }` runs the job on demand (400 on an invalid body). All three accept a staff session or the `x-admin-key` header matching the server session secret; there is no permission key. |
| 12 | Host suspend / terminate cascade (placement freeze + document read-only + outstanding job-order flag) 1214895401573253 | Suspend / Terminate transactionally freezes active placements, locks host-uploaded documents (reads still succeed), and flags outstanding submitted/in-review job orders. Terminate requires confirmation_token; reactivation after terminate returns termination_is_permanent. Every cascaded action audited. Full contract — four cascade effects, error envelopes, and audit row vocabulary — documented in Suspend / Terminate Cascade Contract above (#host-suspend-terminate-cascade). |

> **Sequencing & cross-references**
>
> Each ticket depends on the one above it. Tickets 1, 5, 6, and 7 are the agreement-spine chain referenced from Partner Agreements &rarr; Typed Extension Records . Tickets 3, 4, and 12 implement the lifecycle state machine + activation gate + suspend/terminate cascade documented above. Ticket 10 is the canonical owner of the host-side data-boundary contract referenced from the Host Portal docs.


## References

[1]: /docs/portal-boundaries "Portal Overview and Boundaries"
[2]: https://github.com/Alliance-Strategies/HostPortalPrototype/blob/development/server/routes-modular.ts "HostPortalPrototype modular v2 route composition"
[3]: https://github.com/Alliance-Strategies/HostPortalPrototype/blob/development/client/src/App.tsx "HostPortalPrototype development client route registry"
[4]: https://github.com/Alliance-Strategies/HostPortalPrototype/blob/development/server/middleware/jwt-auth.ts "HostPortalPrototype JWT middleware"
[5]: https://github.com/Alliance-Strategies/HostPortalPrototype/blob/development/server/routes/v2/identity.ts "HostPortalPrototype v2 identity router"
[6]: https://github.com/Alliance-Strategies/HostPortalPrototype/blob/development/server/routes/v2/job-orders.ts "HostPortalPrototype v2 job-orders router"
[7]: /docs/integration-contract "Portal Integration Contract"
