---
title: "Agreement Service (Proposal)"
slug: agreement-service
section: Rules & Compliance
status: draft
description: "The agreements subsystem is being extracted into its own service with a TypeScript SDK."
last-updated: 2026-10-07
---

# Agreement Service (Proposal)

The agreements subsystem is being extracted into its own service with a TypeScript SDK. This page is a draft: it describes the decided target design and the assessment behind it, not how the platform behaves today. Current behaviour is documented in [Agreements Engine](/docs/agreements-engine); this page flips to stable when the service is live.

## Overview

Agreements are implemented inside the Globus Hub (`BackOfficePrototype`) as **two engines** joined by one column:

- **Agreement Library engine** — legal text, versions, targeting (assignments, scope specificity), pinned resolutions, signature policies, immutable acceptance evidence, re-sign obligations and the signed-PDF artifact queue. Schema in `shared/models/agreementLibrary.ts`.
- **Partner Agreement engine** — the commercial lifecycle of host agreements (`partner_agreements`: trial, operating, quota, probation, suspension, renewal). Linked to the Library by `partner_agreements.engine_agreement_code`.

Three consumers reach it, all through the Hub and the Globus SDK: the staff UI, the Applicant Portal (PPPToo: first-login terms gate and the Agreement workflow step) and the Host Portal (operating and trial agreement gate, accept, upgrade, signed PDF). n8n only delivers the notification emails from outbox events. The subsystem is roughly 27,000 lines of services and tests in the Hub plus about 45 data migrations that repair host-agreement library links.

> **Target design, not current state**
>
> Nothing on this page is live yet. The assessment of 2026-10-26 (Asana 1219899439997361) is kept below; the decision taken the same day is recorded next.

## Decision (2026-10-26)

Sam O'Regan decided on 2026-10-26 to build the Agreement Service and its SDK now, rather than the two-stage path recommended below. The binding record is [ADR 0001](https://github.com/Alliance-Strategies/agreements-service/blob/main/docs/adr/0001-agreements-service.md) in the new repository `Alliance-Strategies/agreements-service`; delivery is tracked on the Asana board [Agreements Service + SDK (Microservice Extraction)](https://app.asana.com/1/817430812898727/project/1219906270704861) (37 tickets across phases P0 contract and foundations, P1 service core, P2 runtime, P3 governance, P4 SDK, P5 Globus integration and migration, P6 cutover and hardening).

What the decision fixes, in the terms of this page:

- **Boundary.** The service owns the Agreement Library engine's tables (agreements, versions, version events, assignments, signature policies, runtime pins, acceptances, re-sign obligations, cycle mappings, resolve failures, signed-artifact requests). The Partner Agreement engine (`partner_agreements`, quotas, trial/operating lifecycle), applicants, applications, workflow placements and documents stay in the Hub, which links to the library by `engine_agreement_code` only.
- **Contracts to define first** (below) are adopted as written: resolve with typed failures and an explicit per-consumer failure policy (fail-closed by default); idempotent `accept` as a saga, with the service as the source of truth for acceptance; outbox events with idempotent consumers and a Hub read model instead of the rules-engine joins; service-to-service OIDC with the Hub asserting the subject; addressing by `code`, never UUID.
- **Option B is the SDK shape.** `@alliance-strategies/agreements-sdk` (generated from the service's OpenAPI contract, runtime and admin clients, typed errors, event verification, in-memory fake) is the only way the Hub reaches the service. Phase 1 keeps the Hub as the facade for the portals; portal BFFs may later call the service directly for the sign-in gate and host content/evidence, with step submit staying a Hub route, as configuration.
- **Stage 0 gaps** (atomic host accept, a guard for a missing host signature policy, an explicit failure policy for the Applicant Portal gate, the live re-check of production's agreements schema) are fixed in Globus before any environment cuts over.
- **Deployment.** One Cloud Run service, Cloud SQL instance and Pub/Sub topic per environment (development pool slots, QA pool slots, production; no staging, no shared instance), provisioned by Terraform; published legal text moves between environments only through an explicit export/import with the never-stomp guard.
- **Cutover.** Legacy, shadow-compare and service modes behind one Hub configuration switch; dev then QA then production, each after a soak, with a rehearsed rollback.

## Original Recommendation (superseded)

The assessment recommended not extracting everything at once. That sequencing was superseded by the decision above; the analysis is kept because every seam it names is a delivery ticket now.

Extract in two stages:

1. **Now: modular monolith.** Put a hard module boundary and an internal facade around the Library and Evidence engine inside the Hub. No network hop.
2. **Later: Agreement Service** containing Library, Assignments, Resolution, Acceptance evidence and Signed artifacts. `partner_agreements` (the commercial host lifecycle) stays in the Hub permanently.

Start stage 2 when one of these triggers appears:

- a second system outside Globus needs agreements (for example a recruiter or partner portal, or a non-Globus product);
- legal evidence must scale or deploy independently of the Hub;
- compliance requires the evidence store to be isolated from the Hub database.

## Why Not a Full Extraction Now

| Seam | Where it lives | Why it blocks a clean split |
|---|---|---|
| Single transaction across domains | `agreementWorkflowSubmission.ts` | Applicant acceptance locks the `applications` row, checks the pin, writes the acceptance and enqueues the artifact request in one transaction. A service boundary breaks that atomicity. |
| Hard foreign keys | `agreementLibrary.ts`, `partner.ts` | Library tables reference applicants, applications, brands, programs, seasons, partner organizations, partner users and partner agreements. `applicant_documents`, `job_orders` and `quotas` reference back. |
| Synchronous in-process callers | rules engine `factsResolver`, `coreStorage`, `applicantIdentity`, `partnerEnforcementGuard` | The rule WORKFLOW-AGR-001 gates `eligibility_cleared`; identity and program changes trigger re-sign; about 16 files read `partner_agreements` directly. Each becomes a network call. |
| Non-atomic host accept | `operatingAgreement.ts` | Recording the engine acceptance, updating `partner_agreements` and activating are separate transactions today. Fix before splitting. |
| Two engines | Library vs Partner Agreement | Only the Library side is a clean candidate. Splitting both together drags the host lifecycle, quotas and job-order gates along. |

## Why a Service Still Makes Sense

- Three consumers with different auth each re-implement verify-after-write and error mapping.
- Content promotion between environments is manual: a full-snapshot SQL migration with a never-stomp guard, or config import as a draft. Agreement content is data, not code.
- A missing configuration row has already caused an outage: on 2026-10-08 a missing host signature policy gated 61 host organizations and 73 users.
- The Applicant Portal first-login gate fails open when the Hub errors, so the failure policy for legal evidence is implicit rather than chosen.
- The domain churns heavily and is already a de facto bounded context.

## Current State

```
 Staff UI (Hub client)     PPPToo (NestJS BFF + React)     HostPortalPrototype (BFF + React)
 library / assignments     Keycloak JWT                    Auth0 JWT
 acceptances / resign      X-Portal-Access-Token           X-Host-Org-Id, X-Host-User-Email
        |                  X-Applicant-User-Id                   |
        |                       \--- globus-sdk (X-API-Key) ----/
        v                                  v                      v
 +------------------------------------------------------------------------+
 | Globus Hub / BackOfficePrototype (GKE, per-environment Cloud SQL)       |
 |                                                                        |
 |  Agreement Library engine           Partner Agreement engine           |
 |  agreements, versions, events       partner_agreements (+ extension)   |
 |  assignments, cycle versions        quotas, dismissals                 |
 |  signature policies                 trial / operating lifecycle        |
 |  runtime resolutions (pins)         host accept / upgrade / renew      |
 |  acceptances (immutable)            agreementAcceptanceGate            |
 |  resign obligations, failures             |                            |
 |  signed artifact queue -> PDF             | engine_agreement_code      |
 |        +----------------------------------+                            |
 |                                                                        |
 |  In-process callers: workflow engine (Agreement step), rules engine    |
 |  facts, identity and program hooks, enforcement guards, documents,     |
 |  notifications, audit, config transfer                                 |
 +-----------+----------------------------+---------------------------------+
             | outbox events              | signed PDFs
             v                            v
     n8n dispatcher (emails)         Cloud Storage
```

## Target Architecture

```
 Staff UI      PPPToo BFF      HostPortal BFF      Atlas / n8n / Gateway
    \             |                 |                    /
     +------------+--------+--------+-------------------+
                           v
        Hub (BackOfficePrototype) = system of record for applicants,
        applications, workflow, rules, host orgs, partner_agreements
           |  portal-facing routes unchanged (Hub is the facade)
           |  events in: identity changed, program changed, step submitted
           v
   +--------------------------------------------------+
   | Agreement Service (Cloud Run, own database)       |
   |  API: /library /assignments /resolve              |
   |       /acceptances /resign /artifacts             |
   |                                                   |
   |  Library + versioning     Targeting + resolution  |
   |  Pins + cycle mapping     Signature policy        |
   |  Evidence ledger (append-only, hash-chained)      |
   |  Re-sign engine           Artifact worker (PDF)   |
   |                                                   |
   |  References by opaque ids and scope codes:        |
   |  subject id (applicant or host org), context id   |
   |  (application), brand / program / season keys.    |
   |  No cross-database foreign keys.                  |
   +-------------+------------------+------------------+
                 | outbox events    | signed PDF to Cloud Storage
                 v                  v
     accepted / invalidated /     Hub projects the PDF into
     resign_required /            applicant_documents
     version_published
     -> Hub read model, queues, notifications, n8n
```

The Hub keeps the portal-facing endpoints, so PPPToo and the Host Portal do not change their SDK calls in the first stages.

## Option B: Portals Call the Service Through Its Own SDK

Instead of the Hub fronting every agreement call, each portal BFF calls the Agreement Service directly through a dedicated SDK (`@alliance-strategies/agreement-sdk`), the same way portals use `@alliance-strategies/globus-sdk` today. The SDK is server-side only; browsers never call the service.

```
 PPPToo BFF            HostPortal BFF          future portals
     \                      |                      /
      +---- @alliance-strategies/agreement-sdk ---+
                          |   per-portal credential, caller asserts subject
          +---------------+----------------+
          v (phase 1)                      v (phase 2 onward)
   Hub facade routes              Agreement Service
   (SDK-shaped, thin)             (own database, own API)
          \                                /
           +--- events: accepted, invalidated, resign_required ---> Hub read model
                (rules facts, DS-2019 guard, host gate, queues)
```

**Recommended shape: SDK first, strangler cutover.** Define the SDK contract now and implement it against Hub facade routes. When the service exists, repoint the SDK base URL per endpoint. Portals change once (adopt the SDK); every later move is configuration.

**What it gives**

- The Hub is off the hot path for the sign-in terms gate and the host gate that re-checks every 60 seconds.
- One contract replaces three hand-rolled integrations (staff UI, Applicant Portal, Host Portal), including verify-after-write and error mapping.
- New portals onboard by installing the SDK.
- Contract tests: PPPToo already checks its Hub calls against a contract snapshot; the SDK can carry the same check.

**What breaks if portals go fully direct**

| Problem | Why | Mitigation |
|---|---|---|
| Workflow Agreement step | The Hub owns application and step state. A portal calling both systems would coordinate a split write: acceptance recorded, step not completed. | Keep step-config and step submit as Hub routes that call the service. Only the sign-in gate and host content and evidence calls go direct. |
| Host gate state | It combines `partner_agreements` (Hub) with acceptance evidence (service). The Hub acceptance gate on every host route still needs acceptance state. | Hub read model fed by events, or a synchronous check; the Host Portal keeps its verify-after-write. |
| Resolution scope | Sign-in resolution uses the applicant brand, program and season, which only the Hub knows. | Caller passes scope codes from the Hub context, a Hub-signed scope token, or a service-side projection. |
| Identity | The Hub verifies the Keycloak claim (Applicant Portal) and the host side uses Auth0. The service should not duplicate that. | Trust the portal BFF credential plus an asserted subject, as the Hub does with API keys today. |
| Credentials | Each portal in each environment needs its own service credential. | Same per-environment key pattern as the Hub API key rows, with narrow scopes. |

**Recommended split if this option is chosen.** The SDK exposes resolve, accept and evidence or content. The Hub keeps step orchestration and the host commercial lifecycle.

## Contracts to Define First

1. **Resolve.** Input is subject, context and scope codes; output is the pinned version or a typed failure (`AGREEMENT_NOT_RESOLVABLE`, `NO_PUBLISHED_VERSION`, `POLICY_MISSING`). Each consumer declares fail-open or fail-closed explicitly.
2. **Accept.** Idempotent by subject, version and send or revision anchor; returns the evidence id and content hash. Replaces the single in-transaction write with a saga: the Hub marks the step as accepting, calls the service, and completes the step on the evidence id. The service is the source of truth for acceptance.
3. **Events.** Outbox, at-least-once, idempotent consumers. A Hub read model of accepted agreements per application replaces the rules-engine joins.
4. **Identity.** Service-to-service OIDC from the Hub. The service receives the subject identity; it does not verify applicants or hosts itself.
5. **Content addressing.** Agreements stay addressed by `code`, never by UUID, so content and seeds promote cleanly between environments.

## Staged Path

| Stage | Work | Risk |
|---|---|---|
| 0 | Fix existing gaps: atomic host accept, a guard for a missing host signature policy, an explicit failure policy for the Applicant Portal gate, Nexus drift (predicates and indexes that no longer exist) | Low |
| 1 | In-repo module with a facade; a lint rule forbids direct table access from outside; move the 16 direct `partner_agreements` readers behind a read model | Low |
| 2 | Outbox events and a Hub read model replace the rules-engine joins; dual-write evidence if needed | Medium |
| 3 | Run the service against its own schema and database role; route staff UI and portals through it | Medium |
| 4 | Split the database; build an export and import promotion tool with the never-stomp guard | High |

## Open Questions

Status after the 2026-10-26 decision:

- Is a non-Globus consumer planned? — Moot as a trigger: the service is being built now; the SDK is the onboarding path for any later consumer.
- Should acceptance evidence live outside the Hub for legal isolation? — Yes: the service holds the evidence ledger in its own per-environment database (ADR 0001, decisions 1 and 9).
- Prod state needs a live re-check (a July 2026 investigation found no agreements engine in prod while the 2026-10-08 prod host-policy fix shows the policy table exists). — Still open; carried as a stage-0 subtask of the Globus adapter ticket.
- Which consumer failure policy is correct for legal evidence? — Fail closed by default, declared explicitly per consumer in the SDK call (ADR 0001, decision 8).

## Related Pages

- [ADR 0001 — Agreements Engine becomes a standalone service with an SDK](https://github.com/Alliance-Strategies/agreements-service/blob/main/docs/adr/0001-agreements-service.md) — the binding decision record
- [Agreements Engine](/docs/agreements-engine) — current Library, targeting, signature and evidence behaviour
- [Host Portal Governance](/docs/host-governance) — host agreement structure, trial and operating agreements
- [Partner Organizations](/docs/partner-organizations) — canonical `partner_agreements` model
- [Portal Overview](/docs/portal-boundaries) — Hub as the authority behind every portal
