Skip to content

Frontend Architecture Overview

Paulo Gomes da Cruz Junior edited this page May 6, 2026 · 1 revision

Frontend Architecture — nr-forest-client

Overview

The nr-forest-client frontend is a single-page application (SPA) built with Vue 3 and TypeScript that serves as the Ministry of Forests' client management system. It supports three distinct user types — IDIR staff, BCeID Business external users, and BC Services Card (BCSC) individual users — each routed to a purpose-built workflow on login.


Purpose

The application enables external clients (companies and individuals) to apply for a forest client number, and empowers ministry staff to review submissions, manage client records, and search the client registry. It replaces legacy oracle-based workflows with a modern, accessible web interface backed by a Spring Boot REST API.


Structure

Layer Framework Version Purpose
UI Framework Vue 3.5.x Reactive component model, Composition API
Language TypeScript ~5.9.0 Static typing across all source files
Build Tool Vite 5.4.x Dev server, bundling, plugin ecosystem
Router Vue Router 5.0.x Client-side SPA routing with navigation guards
Authentication AWS Amplify + Cognito 6.x Federated SSO via IDIR, BCeIDBusiness, BCSC
HTTP Client Axios (via composables) 1.15.x REST API calls with auth headers
UI Components Carbon Design System @carbon/web-components 2.x Accessible, consistent BC Gov styling
Utilities VueUse 14.x Composable utilities (useEventBus, useLocalStorage, media queries)
HTML Sanitisation vue-dompurify-html 5.x Safe rendering of server-provided HTML
Input Masking vue-the-mask 0.11.x Phone, postal code, and custom input masks
Date Utilities date-fns 4.x Date parsing, formatting, and validation
JSON Patching fast-json-patch 3.x RFC 6902 patch generation for client edit operations

Behaviour

Application Routing Graph

graph TD
    Start["Browser Request"] --> Guard["Router Navigation Guard\n(beforeEach)"]
    Guard -->|"Not authenticated"| Landing["/landing — LandingPage"]
    Guard -->|"Authenticated: resolve user"| Dashboard["/dashboard — UserLoadingPage"]

    Landing -->|"BCeIDBusiness login"| BCeID["/new-client — FormBCeIDPage"]
    Landing -->|"BCSC login"| BCSC["/new-client-bcsc — FormBCSCPage"]
    Landing -->|"IDIR login"| Search["/search — SearchPage"]

    BCeID -->|"Form submitted"| Confirmation["/form-submitted — FormSubmittedPage"]
    BCSC -->|"Form submitted"| Confirmation

    Search --> Submissions["/submissions — SubmissionListPage"]
    Search --> ClientDetails["/clients/details/:id — ClientDetailsPage"]
    Search --> StaffForm["/new-client-staff — FormStaffPage"]

    Submissions --> Review["/submissions/:id — SubmissionReviewPage"]
    Review -->|"Approved"| StaffProcessing["/client-submitted/:submissionId\nFormStaffProcessingPage"]
    StaffForm -->|"Client created"| StaffConfirmation["/client-created — FormStaffConfirmationPage"]

    Guard -->|"Feature flag off or no role"| Error["/error — ErrorPage"]
    Guard -->|"Unknown path"| NotFound["/notfound — NotFoundPage"]
Loading

Diagram History

  • 2025-05-05: Initial diagram published

The router enforces authentication via ForestClientUserSession.loadUser() in the beforeEach guard. Each route carries a meta object declaring requireAuth, visibleTo (provider list or role object), and redirectTo (provider-keyed fallback routes). Staff-only routes additionally require one of the IDIR roles: CLIENT_VIEWER, CLIENT_EDITOR, CLIENT_SUSPEND, or CLIENT_ADMIN.

Feature-flagged routes include a featureFlagged meta key — if the flag is absent or falsy the guard redirects to the provider-appropriate fallback or the /error page.


Component Hierarchy

graph TD
    App["App.vue (Root)"]

    App --> Header["MainHeaderComponent\n(navigation, help, profile, logout)"]
    App --> RouterView["router-view (KeepAlive: SearchPage)"]
    App --> GlobalModals["cds-modal (global delete/confirm)"]
    App --> GlobalToast["cds-toast-notification (global)"]
    App --> Overlay["LoadingOverlayComponent"]

    RouterView --> PublicPages["Public / Auth Pages"]
    RouterView --> ExternalPages["External User Pages"]
    RouterView --> StaffPages["Staff Pages"]

    PublicPages --> LandingPage
    PublicPages --> UserLoadingPage
    PublicPages --> ErrorPage
    PublicPages --> NotFoundPage
    PublicPages --> LogoutPage

    ExternalPages --> FormBCeIDPage
    ExternalPages --> FormBCSCPage
    ExternalPages --> FormSubmittedPage

    FormBCeIDPage --> BCeIDWizard["bceidform/ wizard steps\n(BusinessInformation, Address, Contact, Review)"]
    FormBCSCPage --> BCeIDWizard

    StaffPages --> FormStaffPage
    StaffPages --> SearchPage
    StaffPages --> SubmissionListPage
    StaffPages --> SubmissionReviewPage
    StaffPages --> ClientDetailsPage
    StaffPages --> FormStaffConfirmationPage
    StaffPages --> FormStaffProcessingPage

    FormStaffPage --> StaffWizard["staffform/ wizard steps\n(BcRegistered, Combined, FirstNation,\nIndividual, Contacts, Locations, Review)"]
    ClientDetailsPage --> ClientDetailViews["client-details/ views\n(Summary, Location, Contact, History,\nRelationships, RegistrationNumber)"]
    SearchPage --> AdvancedSearch["search/AdvancedSearch.vue"]
Loading

Diagram History

  • 2025-05-05: Initial diagram published

App.vue is the root component. It wires the global event buses (modal-notification, toast-notification, overlay-event) and renders the <router-view> with <keep-alive> caching for SearchPage. The MainHeaderComponent is conditionally rendered based on $route.meta.hideHeader and only when a user session is active.


HTTP Request Flow

sequenceDiagram
    participant Component as Vue Component
    participant Composable as useFetchTo / useFetch
    participant Axios as Axios Instance
    participant API as Backend REST API
    participant EventBus as useEventBus (VueUse)
    participant App as App.vue

    Component->>Composable: useFetchTo(url, data, config)
    Composable->>Axios: axios.request({ baseURL, Authorization: Bearer token })
    Axios->>API: HTTP GET / POST / PATCH / DELETE
    API-->>Axios: 200 OK (JSON payload)
    Axios-->>Composable: result.data
    Composable-->>Component: reactive data, loading=false

    alt HTTP 4xx / 5xx
        Axios-->>Composable: AxiosError
        Composable->>EventBus: emit("error-notification", { fieldId, errorMsg })
        EventBus-->>App: listener fires
        App->>App: Open cds-toast-notification
    end
Loading

Diagram History

  • 2025-05-05: Initial diagram published

All HTTP communication flows through the useFetchTo composable in src/composables/useFetch.ts. It injects the Bearer token from ForestClientUserSession.token into every request and emits on the error-notification event bus for ERR_BAD_RESPONSE, ERR_NETWORK, or ERR_BAD_REQUEST error codes. The DataFetcher.vue component wraps useFetchTo as a renderless component for declarative data fetching inside templates.


Authentication & Session Flow

stateDiagram-v2
    [*] --> Unauthenticated
    Unauthenticated --> Redirecting : logIn(provider)
    Redirecting --> Authenticated : Cognito callback → /dashboard
    Authenticated --> SessionLoaded : loadUser() parses idToken
    SessionLoaded --> ProviderRouted : beforeEach redirectTo[provider]
    ProviderRouted --> [*] : User reaches their home route
    SessionLoaded --> Unauthenticated : logOut()
    Unauthenticated --> Landing : redirect to /landing
Loading

Diagram History

  • 2025-05-05: Initial diagram published

ForestClientUserSession (singleton, attached to app.config.globalProperties.$session) manages authentication state. signInWithRedirect triggers the Cognito-hosted UI for the selected provider (IDIR, BCeIDBusiness, BCSC). On callback, the router sends the user to /dashboard (UserLoadingPage), which calls loadUser() to parse the idToken JWT claims and populate user, token, and authorities. The navigation guard then redirects to the provider-appropriate home route.


Public Interface

Type Value
Base URL Configured via VITE_FRONTEND_URL
Entry Point src/main.ts
Routes (Public) /landing, /dashboard, /error, /notfound, /logout
Routes (BCeID/BCSC) /new-client, /new-client-bcsc, /form-submitted
Routes (IDIR Staff) /search, /submissions, /submissions/:id, /new-client-staff, /client-created, /client-submitted/:submissionId, /clients/details/:id
Feature Flags $features global property — populated from VITE_FEATURE_FLAGS env var
Cognito Redirect In {VITE_FRONTEND_URL}/dashboard
Cognito Redirect Out {VITE_FRONTEND_URL}/logout

Dependencies

  • Backend REST API: VITE_BACKEND_URL — all data requests proxied via useFetchTo
  • AWS Cognito: VITE_AWS_COGNITO_* — federated authentication for three providers
  • BC Sans Font: @bcgov/bc-sans — BC Government typography
  • nr-fsa-theme: @bcgov-nr/nr-fsa-theme — shared BC Gov styles
  • Carbon Web Components: @carbon/web-components, @carbon/styles, @carbon/icons-vue — UI system

Known Constraints

  • No server-side rendering: The application is a pure SPA; all routing is client-side. SEO is not a goal.
  • Token stored in CookieStorage: AWS Amplify tokens are persisted in cookies (not localStorage) to survive page refreshes, but this scopes the session to the cookie domain.
  • SearchPage cache: <keep-alive> is applied to SearchPage only; all other pages re-mount on navigation, which means form state is lost on route change unless the page handles it explicitly.
  • Feature flags are build-time environment variables: Flags are injected at container start via VITE_FEATURE_FLAGS JSON; a redeploy or pod restart is required to toggle flags in OpenShift environments.
  • No global state management library: Cross-component state relies on the useEventBus event bridge and Vue's globalProperties. Complex shared state between unrelated pages must use local storage or re-fetch patterns.
  • Carbon Web Components use Shadow DOM: Custom directives (v-masked, v-shadow) and scoped CSS cannot penetrate Shadow DOM boundaries without the v-shadow / ::part() workaround implemented in CustomDirectives.ts.

Change Impact

When this page changes, also update:


See Also

CLIENT application (The Ministry of Forests' client management system)

Architecture

Frontend

General

Development Conventions

Changelogs

Clone this wiki locally