Navigation & Return-State Architecture
1. Overview
The Registration Portal hosts multiple flows (event registration, membership registration, and future flows) that share common screens — linked-person, questions, payment. Without a consistent contract for "where do I return to after this shared screen?", flows leak into each other and users land in the wrong place.
This document defines:
-
The canonical screen inventory and the generic-vs-flow-specific boundary.
-
The three-tier return-state contract that shared screens honour on exit.
-
The
SessionServiceAPI andFlowContexttype that implement the contract. -
Implementation patterns for entering a flow, navigating to a sub-flow, returning, and exiting.
-
A step-by-step guide for adding a new flow on top of these rails.
The decision history — alternatives considered, trade-offs made — lives in the design journal at design-journal/2026-04/registration-portal-unified-navigation.adoc. This document is the canonical reference; the journal is the record of how we got here.
2. Screen Inventory
The portal has seven canonical screen roles. Each screen is either generic (flow-agnostic, reusable) or flow-specific (knows which flow it belongs to).
| Screen | Purpose | Scope |
|---|---|---|
Landing |
Public org-branded entry page; browse events and memberships for an organisation |
Generic (future) |
Authentication & Identity Selection |
Guest, OIDC, or external hash auth entry; auto-select guest when no other options configured |
Generic — specific per auth method |
Person list (event) |
Linked-person selection for a specific event; the event flow’s entry screen |
Event-specific |
Person list (membership) |
Linked-person selection for a specific membership period; the membership flow’s entry screen |
Membership-specific |
Add / Edit a person |
Progressive match, create, or edit a linked person |
Generic (shared) |
Questions / Summary |
Step-by-step question workflow driven by a |
Generic (shared) |
Payment selection & process |
Order display, method selection (online / EFT / manual), method-specific capture |
Generic (shared) |
3. Return-State Contract (Three-Tier Model)
When a caller navigates to a shared screen, it records where to return. When the shared screen exits, it consumes that record. The contract defines three complementary tiers that together handle the normal case, the session-expiry case, and the worst-case fallback.
3.1. Tier 1 — Exact return URL
-
Storage: in-memory on
SessionService -
Lifetime: until consumed or full-page reload
-
Use: normal case — user completes a sub-flow in the same SPA session and returns to the exact URL they left (including all query params).
3.2. Tier 2 — Flow context
-
Storage: browser
sessionStorage— survives full-page reloads; scoped per tab -
Lifetime: from flow entry until flow completion, home navigation, logout, or entry into a different flow
-
Shape:
FlowContext = { flow: 'event' | 'membership', landingUrl: string } -
Use: session-expiry recovery. In-memory Tier 1 is lost when re-auth involves a full-page redirect (external hash, OIDC). Tier 2 persists. The user returns to the flow landing screen, which renders updated state as its own confirmation.
3.3. Tier 3 — Org landing
-
Storage: always derivable from tenant context (
orgIdfrom host) -
Lifetime: always
-
Use: ultimate fallback when Tiers 1 and 2 are both absent.
-
Current state: the org landing feature at
/landing/:organisationIdis parked as future scaffolding. Tier 3 is a stub that returns/today; it swaps to/landing/{orgId}when the landing feature activates — a one-line change inSessionService.
3.4. Fallback resolution
When a shared screen exits, it calls consumeReturnUrl(), which resolves in order:
-
Tier 1 is always cleared on consumption.
-
Tier 2 is not cleared on return-from-sub-flow — only on flow completion, home nav, logout, or flow switch.
-
Tier 3 is derivable, never stored.
4. SessionService API
4.1. FlowContext type
export type FlowId = 'event' | 'membership';
export interface FlowContext {
flow: FlowId;
landingUrl: string;
}
New flows extend FlowId with their own identifier.
4.2. Methods
| Tier | Method | Semantics |
|---|---|---|
1 |
|
Stores |
1 |
|
Explicit override for the rare case where the caller wants the callee to exit to a different URL than the caller’s own. Prefer |
2 |
|
Persists the flow context to |
2 |
|
Reads the flow context. Used by error screens (e.g. "Return to your event registration") and internally by |
2 |
|
Removes the flow context. Called on home navigation, logout, and entry into a different flow. |
— |
|
Resolves the exit URL via the three-tier fallback and clears Tier 1. Does not clear Tier 2. |
5. Flow-Specific vs Generic Screens
Generic screens (/linked-person/search, /form/:processInstanceId, /order/:orderId, /payment/) are *flow-agnostic. They know nothing about whether the user is registering for an event or renewing a membership. They read the contract on exit. Adding a new flow does not require changes to any generic screen.
Flow-specific screens are the entry point for their flow (/register for event, /membership/register for membership). They:
-
Set Tier 2 on every initialisation so session-expiry recovery works.
-
Set Tier 1 before navigating to a generic screen so the generic screen can exit back to the caller’s exact URL.
-
Render updated state on return from a sub-flow — the updated state is the confirmation. Successful payment or registration completion does not navigate to a dedicated "done" screen.
6. Implementation Patterns
6.1. Entering a flow
When a flow-specific entry screen initialises, it establishes Tier 2:
ngOnInit(): void {
this.sessionService.setFlowContext({
flow: 'event',
landingUrl: this.router.url,
});
// ... rest of screen init
}
This runs on every initialisation of the entry screen, not only the first visit — the user may arrive with different query params (different eventId, different periodId), and Tier 2 must reflect the current one.
6.2. Navigating to a generic sub-flow screen
Before navigating, the caller sets Tier 1:
onAddPerson(): void {
this.sessionService.setReturnToCurrent();
this.router.navigate(['/linked-person/search']);
}
Do not pass a string to setReturnUrl. The setReturnToCurrent() method reads the router’s current URL internally, eliminating the risk of storing a literal that silently drops route params.
6.3. Returning from a generic sub-flow screen
The generic screen exits via the contract:
onDone(): void {
this.router.navigateByUrl(this.sessionService.consumeReturnUrl());
}
The same call handles cancel and error paths. The generic screen never hard-codes a destination.
6.4. Exiting a flow
When the user signals they are leaving the flow — home navigation, logout, starting a different flow — clear Tier 2:
onGoHome(): void {
this.sessionService.clearFlowContext();
this.router.navigate(['/']);
}
Successful payment or registration completion does not clear Tier 2. The user returns to the flow landing, which renders the updated state as confirmation. They may register another person, modify, or exit deliberately.
6.5. Adding a new flow
Four steps. No changes to any generic screen.
-
Extend
FlowIdwith your flow’s identifier:export type FlowId = 'event' | 'membership' | 'myNewFlow'; -
Create the flow entry screen as a standard Angular component, with a route under
authGuard. -
Set Tier 2 on entry:
ngOnInit(): void { this.sessionService.setFlowContext({ flow: 'myNewFlow', landingUrl: this.router.url, }); } -
Use the contract for outbound navigation:
onNavigateToSubflow(): void { this.sessionService.setReturnToCurrent(); this.router.navigate(['/linked-person/search']); }
That’s it. Generic screens return via consumeReturnUrl() — the three-tier contract handles the rest.
7. Interaction with Authentication
The navigation contract and the authentication architecture are complementary, not overlapping.
-
Authentication’s existing
returnUrlon/login?returnUrl=…handles "where to land after re-auth completes". This is a URL-level mechanism that survives any redirect, and is owned by the auth interceptor. -
Tier 1 (in-memory) is lost when re-auth involves a full-page redirect (external hash, OIDC). This is expected.
-
Tier 2 (
sessionStorage) survives full-page redirects, so after re-auth the user can still consume a meaningful fallback even when Tier 1 is gone.
-
User on
/linked-person/search; Tier 1 is set to/register?eventId=5&orgId=3; Tier 2 is{ flow: 'event', landingUrl: '/register?eventId=5&orgId=3' }. -
Session expires. Interceptor detects 401.
-
Auth-method-specific recovery runs (see Security Architecture). User re-authenticates.
-
User lands back on
/linked-person/search(via the interceptor’s URL-levelreturnUrl). -
Tier 1 is gone (full-page reload cleared in-memory state). Tier 2 survives in
sessionStorage. -
User completes sub-flow.
consumeReturnUrl()returns Tier 2’slandingUrl. -
User lands on the event flow landing — same flow, correct context, updated state visible.
8. Related Documentation
-
Architecture Overview — portal-wide architecture context
-
Security Architecture — authentication and authorization, including auth-method-aware session recovery
-
API Proxy Design — gateway routing and backend integration
-
Phase 2 Registration Design — overall registration flow context
-
Event Registration — flow-specific design
-
Membership Registration — flow-specific design
-
LinkedPerson Management — shared screen used by all flows
For the decision history (alternatives considered, trade-offs made), see the design journal at design-journal/2026-04/registration-portal-unified-navigation.adoc.