---
title: "Direct vs Partner Entry"
slug: entry-flows
section: Applicant Journey
status: stable
description: "How applicants enter the ecosystem through the as-built Globus-first intake: n8n calls the Hub's provision endpoint, Globus creates the applicant, application and invite token in a single call, and the Applicant Portal onboards the appli…"
last-updated: 2026-10-07
---

# Direct vs Partner Entry

How applicants enter the ecosystem through the as-built Globus-first intake: n8n calls the Hub's provision endpoint, Globus creates the applicant, application and invite token in a single call, and the Applicant Portal onboards the applicant against that existing record.

## Core Principle

> **One Applicant, One Record, One Path**
>
> Regardless of how an applicant discovers the program — through marketing, a recruitment partner, or a direct referral — they converge into the same onboarding journey. Globus, the sponsor back office, owns the applicant record. Keycloak owns credentials. The Applicant Portal owns nothing persistent — it is a stateless BFF over Globus and Keycloak. The entry channel is recorded as attribution, not as a structural fork in the architecture.

Every applicant enters the ecosystem through one of two paths: Direct (self-initiated via marketing/HubSpot) or Partner-initiated (recruited by an international partner organization). Both paths produce the same result: an operational record in Globus — applicant, application and portal invite token — with a traceable origin, and, once the applicant activates, a portal login and application journey in the Applicant Portal. For the full authority separation between portals and Globus, see [Portal Functional Boundaries](/docs/portal-boundaries).

## Architecture Decision — Globus-First Intake

> **Globus-First: Globus Creates First, the Portal Onboards Against It**
>
> Globus is the first and only system to create the applicant record. A single provision call creates the applicant, the application, and the portal invite token. The Applicant Portal never creates an applicant. This is a deliberate architectural decision.

When a new applicant enters the ecosystem, n8n calls the Hub's provision endpoint (`POST /api/integrations/onboarding/provision`) and Globus mints the applicant, application and invite token in one transaction. The portal's onboarding flow then activates a login against that pre-existing record. This applies to both direct and partner entry.

Why Globus-First?

- **Single system of record.** There is exactly one applicant record, created once, in the system that staff already operate. No cross-system sync, no reconciliation, no split-brain between a portal copy and a back-office copy.
- **Deduplication at the door.** Per the [Globus Portal API reference](/docs/globus-portal-api), the provision call is idempotent on `hubspot_contact_id` + `program` — repeated calls for the same lead return the same applicant instead of creating duplicates.
- **The applicant is reachable before any portal account exists.** Staff can see, email and work the applicant in Globus from the moment of provisioning — portal activation is a later, optional-until-it-happens step, not a precondition for operations.

## Direct Intake

In the direct flow, the applicant discovers the program through marketing channels, expresses interest, and is pre-qualified through HubSpot. Once qualified, HubSpot triggers an n8n workflow that provisions the applicant in Globus. The resulting invite email brings the applicant to the Applicant Portal to activate their login and begin their application journey.

> **Direct Does Not Mean Unassigned**
>
> Direct does not mean "unassigned." Direct applicants can still be associated with an internal regional office for operational ownership. "Direct" only means "not initiated by an external recruiting partner."

### Technical Flow (Step-by-Step)

The following is the as-built system interaction sequence for direct intake.

1. **HubSpot** — a contact reaching the qualifying lifecycle stage fires a HubSpot workflow that calls an n8n webhook with the lead data.
2. **n8n -> Hub** — n8n calls `POST /api/integrations/onboarding/provision` on the Hub, authenticated with `x-api-key` (scope `integrations.onboarding`; auth and idempotency contract are Hub-side, per the [Globus Portal API reference](/docs/globus-portal-api)). The call is idempotent on `hubspot_contact_id` + `program` and returns an absolute `invite_url` (e.g. `https://development.applicants.allianceabroad.com/onboarding?token=...`), which n8n forwards verbatim into the invite email.
3. **Globus -> n8n fan-out** — Globus emits an `applicant.created` event to the `callback_webhook_url` supplied on the provision call. The n8n dispatcher consumes it and fans out: it writes the `portal_invite_url` back to the HubSpot contact and sends the invite email via SendGrid. n8n typically also pre-creates the Keycloak account at this point (random Keycloak user id, no password, with the `globusApplicantId` attribute set).
4. **Applicant** — the applicant clicks the invite link and lands on `/onboarding?token=...` in the Applicant Portal.
5. **Token preview** — the portal calls `POST /api/v1/onboarding/validate-token`, a non-burning preview: the BFF opens a Globus portal session (`POST /api/portal/session`), fetches the applicant, and returns the applicant's details plus an `alreadyActivated` flag.
6. **Activation state** — whether an applicant is "activated" is determined by Keycloak password existence, not by token burn. The Globus invite token never expires, so the invite link keeps working. If `alreadyActivated` is true, the portal redirects to `/sign-in` instead of re-onboarding.
7. **Completion** — `POST /api/v1/onboarding/complete` (password of at least 10 characters) performs a create-or-claim on the Keycloak user. In the typical flow the account already exists (pre-created by n8n with a random Keycloak id) and is claimed via its `globusApplicantId` attribute; only when no account exists does the portal create one, setting the Keycloak user id to the Globus `portal_user_id`. A mismatched `globusApplicantId` on an existing account returns `410 Gone`; an already-activated account returns `409`.
8. **First login** — the portal immediately calls `POST /api/v1/auth/login`, stores the Bearer token in sessionStorage, and routes the applicant to `/dashboard`.
9. **Agreement gate** — on first login the TermsGateModal calls `GET /api/v1/agreements/required` and blocks the portal until the applicant accepts (checkbox or typed-name signature, depending on the agreement). The modal is non-dismissable; a Decline button is shown only when the agreement permits declining — mandatory agreements are accept-or-abandon — and declining signs the applicant out.
10. **Throttling** — the entire onboarding controller is auth-throttled to limit token-guessing and credential-stuffing attempts.

**Invite-link base URL.** The origin the Hub uses when it builds portal invite links resolves in this order, first non-empty value wins: the admin-editable `integration_configs` entry `applicant_portal` / `base_url`; the `APPLICANT_PORTAL_BASE_URL` environment variable; the `APPLICANT_PORTAL_URL` environment variable; a default chosen by `CONFIG_ENV` (`development` gives `https://development.applicants.allianceabroad.com`, `qa` gives `https://qa.apply.allianceabroad.com`); otherwise the legacy pre-cutover default, which is what production still resolves to because production is not yet mapped in code. The result is a bare origin with no trailing slash, cached in-process for 30 seconds.

### Globus-Side Activation Stamp

Separately from the portal's Keycloak-based activation check above, Globus records its own activation fact on the applicant: `applicants.portal_activated_at`. It is written first-write-wins (the timestamp never moves once set) and audited once as `portal_activated`, with the evidence source recorded on the audit entry. Globus derives it from portal-origin traffic it already authenticates rather than waiting only on the portal's explicit `mark-activated` callback (which remains authoritative). The evidence sources are:

- the portal's `mark-activated` callback;
- an `applicant.registered` portal event (self-registration);
- any accepted portal event carrying the portal's signed-in user identifier (`X-Applicant-User-Id`); and
- an accepted, ownership-checked write the applicant makes through the portal — a step submission or a document upload.

Redeeming an access token does not count as activation, because nurture mail and portal invites carry the same magic-link token. The stamp is a best-effort side effect of these writes: a failure to stamp is logged and never fails the portal's own request. Applicants whose activity predated the change were backfilled by migration from the earliest portal-origin evidence in the audit log.

Once stamped, the incomplete-profile nurture cadence stops for that applicant: the campaign predicate `incomplete_portal_profile` v2 returns ineligible with stop reason `portal_activated` ("Activated their portal account"), checked before profile completeness, both when enrolling and again when each step comes due. The seeded campaign (Camp Wild Packs, `CAMP_WP`) now declares predicate v2, and a data migration moved existing `incomplete_portal_profile` campaign definitions from v1 to v2.

### HubSpot Webhook Configuration

The HubSpot team configures a workflow that fires when a contact reaches the qualifying lifecycle stage. The workflow does not call the portal or Globus directly — it calls an n8n webhook, and n8n owns the provision call, the callback handling, and the HubSpot write-back.

### Provision Request Payload

The n8n workflow sends the following fields to the Hub's `POST /api/integrations/onboarding/provision` endpoint. The payload schema is owned Hub-side — see the [Globus Portal API reference](/docs/globus-portal-api) for the authoritative contract. These are mapped from the HubSpot contact record.

| Field | Type | Notes |
| --- | --- | --- |
| hubspot_contact_id | string | HubSpot Contact Record ID. Part of the idempotency key (with program) and stored for attribution. |
| email | string | Primary contact email for the applicant record and the invite email. |
| firstname | string | HubSpot property: firstname. |
| lastname | string | HubSpot property: lastname. |
| program | string | Program name — the Hub matches by program code or name (e.g., "TEACH" or "Teach USA"), which must uniquely resolve to a program with an open season; ambiguous or closed-season names are rejected with a 422. Part of the idempotency key and determines the application created. |
| role | string | Optional. Set by the n8n workflow from the HubSpot property which_role_are_you_applying_for_. |
| callback_webhook_url | string | Where Globus sends the `applicant.created` event — the n8n dispatcher URL. |
| correlation_id | string | Caller-supplied ID for tracing the provision call through logs and the callback. |

### applicant.created Event

Globus emits an `applicant.created` event to the `callback_webhook_url` provided on the provision call. The n8n dispatcher consumes the event and fans out: it writes the `portal_invite_url` back onto the HubSpot contact and triggers the SendGrid invite email. See the [Webhook Event Registry](/docs/webhook-event-registry) for the event catalogue and delivery details.

### Pre-Populated Fields

When the applicant first logs into the portal, their profile and application forms already contain the data captured during pre-qualification. These values are sourced from the Globus applicant record — populated at provision time and by staff — and served to the portal through the BFF. This reduces friction and avoids asking the applicant to re-enter information they already provided.

| Field | Portal Form / Section | Notes |
| --- | --- | --- |
| first_name | About Me / Personal Info | Applicant can correct if needed. |
| last_name | About Me / Personal Info | Applicant can correct if needed. |
| email | Account / Login | Email is the identity key. Cannot be changed by the applicant (contact support to change). |
| phone | About Me / Contact Info | Applicant may update with a different number. |
| date_of_birth | About Me / Personal Info | Required for eligibility checks. If not captured at intake, left blank for applicant to complete. |
| nationality | About Me / Personal Info | Used in eligibility rules. |
| country_of_residence | About Me / Personal Info | May differ from nationality. Used for visa determination. |
| gender | About Me / Personal Info | Optional field. If not captured at intake, left blank. |
| brand | Application Context | Locked to the brand the applicant was provisioned under. Cannot be changed by the applicant. |
| program | Application Context | Locked. Determines which workflow and forms the applicant sees. |
| season | Application Context | Locked. Determines deadlines, eligibility rules, and compliance dates. |
| hubspot_contact_id | Not displayed | Stored on the Globus record for back-reference to HubSpot and attribution tracking. |
| notes | Staff-only | Visible to staff in Globus, not to the applicant. |

> **Locked Fields Cannot Be Changed by the Applicant**
>
> Brand, program, and season are locked because they determine the entire application journey — which workflow the applicant follows, which eligibility rules apply, and which compliance deadlines govern their timeline. If an applicant needs to switch programs, a new application must be created through the proper channel.

## Partner-Initiated Intake

Partners manage their recruited applicants through the Partner Portal, which integrates directly with Globus. Partner-recruited applicants enter through the Applicant Portal to complete their own application journey — provisioned in Globus with partner attribution instead of HubSpot attribution.

For details on how partner governance and lifecycle phases work, see [Partner Portal Governance](/docs/partner-governance).

### Partner Invite Trigger

A partner submits applicants for invitation through the Partner Portal (typically via CSV/Excel upload or one-by-one form entry). Each submitted applicant is created in Globus with partner attribution by the partner invite service (`POST /api/admin/partners/organizations/:orgId/invite`, or bulk-invite), not through the HubSpot provision call: the partner organisation comes from the URL rather than a `hubspot_contact_id` provision payload, and the service publishes `partner.invite.created`. From the moment the applicant clicks their invite link, their journey is identical to direct intake — the same token preview, activation, login and agreement gate described above.

> **Important Boundary**
>
> Partners do not view or manage applicants inside the Applicant Portal. Partners use the Partner Portal for applicant management. The Partner Portal reads milestone data from Globus but cannot modify the applicant's application journey.

### Recruitment Attribution

Attribution is captured at the moment the applicant is provisioned in Globus. For partner-invited applicants, the invite input (`inviteApplicantSchema`) must include:

- Program and Season (cohort): `programId` and `seasonId` are required, `brandId` is optional
- The Partner Organization ID is taken from the invite URL (`:orgId`), not from the payload

For direct applicants, attribution includes the HubSpot Contact ID and marketing context instead of a partner ID.

> **Attribution Lives in Globus**
>
> Attribution is stored on the Globus operational record at provision time. The back office always knows where the applicant came from — whether marketing or a partner — without any cross-system sync.

### What Happens If Attribution Is Missing?

Attribution fields are required, not optional. The provision endpoint validates every request before processing. If any required field is missing or invalid, the request is rejected.

> **No Silent Defaults**
>
> The system never silently fills in missing attribution. Every rejected request returns a specific error message identifying the missing or invalid field. For bulk CSV uploads, the Partner Portal returns a row-level error report so the partner can correct and re-submit.

### White-Label Application Links

> **Target State — Not Yet Implemented**
>
> White-label links are a planned mechanism. No `/apply` route exists in PPPToo today; this section describes the intended design, not shipped behaviour.

In addition to bulk CSV upload, partners will be able to generate unique tracking links that direct individual candidates to the Applicant Portal with pre-tagged attribution. This eliminates manual data entry for one-off referrals and enables partner-specific landing pages.

`/apply?partnerId=xyz&program=camp&brand=wildpacks&season=2026-summer`

When an applicant clicks a white-label link:

- Partner attribution is auto-applied from the URL parameters at provision time and locked (not editable by the applicant)
- The applicant lands on a program-specific registration page with partner branding context
- The partner receives a notification confirming the applicant has registered

> **Attribution Is Immutable**
>
> Once an applicant registers through a white-label link, the partner attribution cannot be changed by the applicant, the partner, or any non-sponsor user. Only sponsor staff with the appropriate RBAC role can reassign attribution — and that action is audit-logged with a required reason.

## Portal Architecture

All three external portals — the Applicant Portal, the Host Portal, and the Partner Portal — integrate directly with Globus. There is no legacy intermediary. Each portal serves a specific audience with a purpose-built experience layer, while Globus remains the single system of record and authority.

### Readiness Criteria

Each program is onboarded to the portal ecosystem on its own schedule rather than in a single cutover. Camp programs may go live before Teach programs, which additionally require the Partner Portal and partner governance to be in place — the dependency logic is described in [Partner Portal Governance](/docs/partner-governance).

For detailed milestones and timelines, see [Delivery Overview](/docs/delivery-overview).

## Governance Distinction

The entry channel determines attribution, not authority. Both direct and partner-initiated applicants are subject to the same governance rules once they enter the system. For full RBAC enforcement details, see [RBAC & Governance](/docs/rbac).

| Aspect | Direct Applicant | Partner Applicant |
| --- | --- | --- |
| Entry Source | HubSpot lifecycle stage -> n8n -> Hub provision call | Partner Portal submission -> Globus provision |
| Application | Self-initiated in Applicant Portal | Self-completed after invite |
| Attribution | hubspot_contact_id + marketing context | partner_org_id |
| Pre-Populated Data | From HubSpot payload | From partner invite payload |
| Compliance Rules | Same program rules apply | Same program rules apply |
| RBAC Governance | Sponsor-controlled via Globus | Sponsor-controlled via Globus |
| Workflow Engine | Same portal application journey | Same portal application journey |
| Globus Sync | Not applicable — Globus creates the record at provision time (attribution: hubspot) | Not applicable — Globus creates the record at provision time (attribution: partner) |
| Partner Visibility | N/A | Read-only milestone tracking via Partner Portal |

### Data Ownership Principle

Globus (Back Office) owns:

- The applicant record, the application, and the portal invite token
- Recruitment attribution and agreements
- Operational processing, workflow state, placement decisions, and compliance status
- Partner agreements, quotas, and governance
- Reporting, analytics, and audit trail

Keycloak owns:

- Applicant identity and credentials (never stored in Globus)
- The identity link to Globus is dual: the `globusApplicantId` attribute (surfaced as a token claim) is the authoritative binding to the applicant record; the token `sub` is the fallback. Only portal-created accounts have their Keycloak user id set to the Globus `portal_user_id` — n8n-provisioned accounts carry a random Keycloak id and are linked solely by the claim. This is the BFF's resolution order when it derives the applicant from a token: `globusApplicantId` claim first, then `sub`.

The Applicant Portal owns nothing persistent. It is a stateless BFF: every read and write passes through to Globus (via portal-scoped APIs) or Keycloak, and the portal holds no applicant database of its own.

> **Architectural Separation Guarantee**
>
> This separation ensures that Globus can evolve its back-office logic without rebuilding applicant identity or portal credentials, and the portal can evolve its UX without touching operational data. Credentials never live in Globus; identity links back to Globus through the `globusApplicantId` claim, falling back to the token `sub` (which equals `portal_user_id` only for portal-created accounts).

> **Governance is Uniform**
>
> Once an applicant record exists in the system, the entry channel is irrelevant to compliance, workflow, and RBAC enforcement. The same rules, the same application journey, and the same audit trail apply to every applicant regardless of how they entered.
