Skip to content

Checking access...

Frontend Application Split (FAS) Architecture ​

⚠️ PARTIALLY SUPERSEDED (2026-04-11 / 2026-05-14 / 2026-07-16). Two of the components this page treats as live are gone: the auth-service canister was decommissioned 2026-04-11 (AUTH-005.1) — sessions are oracle-bridge PostgreSQL sessions, and no suite validates against a canister; and think-tank-suite is the frontend for Think Tank, deprecated 2026-05-14 (its proposal pipeline and SaaS layer both unified under FounderyOS), with the repository deleted 2026-07-16 (bl-1073, not archived). The FAS package/suite decomposition itself is still accurate. Treat the auth-flow diagrams and the think-tank-suite rows below as a historical record. (bl-1165)

This document describes the architecture of the Frontend Application Split project, which decomposes the monolithic frontend into independently deployable suites consuming shared packages.

Table of Contents ​

Overview ​

The FAS project replaces the single-repo frontend monolith with:

  • 3 shared packages published to GitHub Packages (@hello-world-co-op/api, auth, ui)
  • 6 independent suite repositories that consume these packages and deploy to IC asset canisters
  • Reusable CI/CD workflows in the .github org repo for consistent build/test/deploy pipelines

Each suite is a standalone React + Vite application that deploys to its own Internet Computer asset canister with a custom domain on .helloworlddao.com.

Total test coverage across all suites: 8,628 tests (0 failures, 0 skipped).

Package Architecture ​

@hello-world-co-op/api (v0.1.0) ​

IC canister client utilities. Provides createActor() factory, ApiResponse<T> type, ERROR_CODES, and checkServiceHealth().

  • Source: api repo
  • Dependencies: @dfinity/agent, @dfinity/candid, @dfinity/principal
  • Consumers: auth, ui, think-tank-suite, dao-suite, dao-admin-suite

@hello-world-co-op/auth (v0.2.0) ​

Authentication context, hooks, and RBAC. Provides AuthProvider, useAuth(), ProtectedRoute, RoleGuard, useRoles(), and token storage utilities.

  • Source: auth repo
  • Dependencies: @hello-world-co-op/api
  • Consumers: ui, think-tank-suite, dao-suite, dao-admin-suite

@hello-world-co-op/ui (v0.1.0) ​

Shared UI component library. Provides Button, Card, Modal, Input, Select, ErrorBoundary, cn() utility, and a Tailwind base configuration.

  • Source: ui repo
  • Dependencies: @hello-world-co-op/auth, tailwindcss, @radix-ui/*
  • Consumers: think-tank-suite, governance-suite, marketing-suite, dao-suite, dao-admin-suite

Known limitation: Packages use @dfinity v2. Suites may need v3 upgrade (tracked as deferred-dfinity-v3-upgrade).

Suite Architecture ​

Each suite follows the structure established by the suite-template:

<suite-name>/
├── src/
│   ├── main.tsx              # Entry point
│   ├── App.tsx               # Root component (ErrorBoundary, Router)
│   ├── components/           # Suite-specific components
│   ├── pages/                # Route-level pages
│   ├── hooks/                # Custom React hooks
│   ├── services/             # API clients and service layer
│   ├── stores/               # State management (nanostores)
│   ├── utils/                # Utility functions
│   └── types/                # TypeScript type definitions
├── e2e/                      # Playwright E2E tests
├── .github/workflows/        # CI/CD (ci.yml, deploy-staging.yml)
├── package.json              # Consumes @hello-world-co-op/*
├── vite.config.ts            # Vite build configuration
├── tailwind.config.js        # Extends @hello-world-co-op/ui config
├── dfx.json                  # IC asset canister config
├── .npmrc                    # GitHub Packages registry
└── .env.example              # Environment variable documentation

All Deployed Suites ​

SuiteCanister IDStaging DomainProduction DomainTestsBundle SizePattern
think-tank-suitewnfjk-biaaa-aaaao-a6dhq-caistaging-think-tank.helloworlddao.comthink-tank.helloworlddao.com4,642--Auth Bridge
governance-suitewkep6-mqaaa-aaaao-a6dha-caistaging-governance.helloworlddao.comgovernance.helloworlddao.com936--Auth Direct
marketing-suited5fe6-hqaaa-aaaao-a6t5q-caistaging.helloworlddao.comwww.helloworlddao.com83358KBStandalone
otter-camp-suitedzt3i-sqaaa-aaaao-a6uaa-caistaging-ottercamp.helloworlddao.comottercamp.helloworlddao.com2,12258KB initialStandalone
dao-suited6s54-7iaaa-aaaao-a6uaq-caistaging-portal.helloworlddao.comportal.helloworlddao.com806348KBAuth Bridge
dao-admin-suitedxrwa-jaaaa-aaaao-a6uba-caistaging-admin.helloworlddao.comadmin.helloworlddao.com11593KBAuth Bridge

Suite Patterns ​

Suites follow one of three authentication patterns:

Pattern 1: Auth-Integrated with Bridge (think-tank, dao, dao-admin) ​

Uses AuthProviderBridge to wrap nanostores state into the @hello-world-co-op/auth shared interface.

Consumes: @hello-world-co-op/ui, auth, apiRequires: oracle-bridge for cookie-based SSO

AuthProviderBridge → useAuth() hook
  ↓ reads from
nanostores ($isAuthenticated, $user, $isLoading)
  ↓ calls
authStore (login, logout, refreshSession)
  ↓ talks to
auth-service canister (via oracle-bridge cookie proxy)
  • AuthProviderBridge wraps the app root, providing @hello-world-co-op/auth's AuthContextValue backed by nanostores
  • useAuth() hook reads from nanostores and returns the shared interface
  • apiHelpers.ts re-exports createActor, ApiResponse, etc. from @hello-world-co-op/api
  • getAuthHeader() is cookie-auth aware (checks VITE_USE_COOKIE_AUTH)
  • Cookie SSO populates localStorage via authCookieClient bridge for legacy components

Suites using this pattern:

  • think-tank-suite (4,642 tests) -- full productivity app with Mission Control, Workspace, Chat, Fleet, Backlog, Capture, Settings
  • dao-suite (806 tests) -- member dashboard with proposals, voting, treasury, membership, token balance
  • dao-admin-suite (115 tests) -- admin dashboard with KYC review, member management, system monitoring. Adds AdminGuard RBAC component and admin-specific validateReturnUrl

Pattern 2: Auth-Integrated Direct (governance) ​

Uses nanostores directly (no bridge). Cookie-based auth via authStore.

Consumes: @hello-world-co-op/ui only Auth: Cookie-based session tokens validated against auth-service canister

nanostores authStore
  ↓ manages
cookie-based session tokens
  ↓ validates against
auth-service canister
  • ErrorBoundary from @hello-world-co-op/ui wraps the app
  • Auth state managed via nanostores (no AuthProvider bridge)
  • Build-time auth bypass prevention: vite.config.ts rejects VITE_DEV_AUTH_BYPASS in production

Suites using this pattern:

  • governance-suite (936 tests: 867 unit + 69 E2E) -- proposals, voting, discussions, deep links

Pattern 3: Standalone (marketing, otter-camp) ​

No auth dependency. May consume @hello-world-co-op/ui for shared components or use no shared packages at all. Independent functionality.

Consumes: @hello-world-co-op/ui (marketing) or no shared packages (otter-camp) Auth: None required

Suites using this pattern:

  • marketing-suite (83 tests) -- public marketing site with SEO pre-rendering, sitemap.xml, react-helmet-async, i18n (4 languages), client-side PII encryption
  • otter-camp-suite (2,122 tests: 2,115 unit + 7 E2E) -- Phaser.js game with character creation, camp exploration, multiplayer presence, combat system. Game reads user_data from localStorage passively (read-only, set by auth suites). No IC canister calls from game code.

When to Use Each Pattern ​

ScenarioPatternExample
Suite needs user login, session management, protected routesAuth Bridgedao-suite
Suite only needs to read auth state (cookie)Auth Directgovernance-suite
Suite is public-facing, no user-specific contentStandalonemarketing-suite
Suite has its own rendering engine (game, canvas)Standaloneotter-camp-suite
Suite needs admin-level RBACAuth Bridge + AdminGuarddao-admin-suite

Dependency Diagram ​

Suite and Package Dependencies ​

mermaid
graph TD
    subgraph "Auth-Integrated Suites (Bridge Pattern)"
        FOS["think-tank-suite<br/>4,642 tests"]
        DAO["dao-suite<br/>806 tests"]
        ADMIN["dao-admin-suite<br/>115 tests"]
    end

    subgraph "Auth-Integrated Suite (Direct Pattern)"
        GOV["governance-suite<br/>936 tests"]
    end

    subgraph "Standalone Suites"
        MKT["marketing-suite<br/>83 tests"]
        OC["otter-camp-suite<br/>2,122 tests"]
    end

    subgraph "Shared Packages (GitHub Packages)"
        UI["@hello-world-co-op/ui<br/>v0.1.0"]
        AUTH["@hello-world-co-op/auth<br/>v0.2.0"]
        API["@hello-world-co-op/api<br/>v0.1.0"]
    end

    subgraph "IC Canisters (Backend)"
        AS[auth-service]
        US[user-service]
        GOVCAN[governance]
        TRES[treasury]
    end

    subgraph "Off-Chain Services"
        OB["oracle-bridge<br/>staging-oracle.helloworlddao.com"]
    end

    FOS --> UI
    FOS --> AUTH
    FOS --> API
    DAO --> UI
    DAO --> AUTH
    DAO --> API
    ADMIN --> UI
    ADMIN --> AUTH
    ADMIN --> API

    GOV --> UI

    MKT --> UI

    UI --> AUTH
    AUTH --> API
    API --> AS
    API --> US
    API --> GOVCAN
    API --> TRES

    FOS -.->|cookie SSO| OB
    DAO -.->|cookie SSO| OB
    ADMIN -.->|cookie SSO| OB
    OB -.->|proxies auth| AS

Dependency rules:

  • Suites depend on packages. Packages depend on other packages in the chain api -> auth -> ui.
  • Suites NEVER depend on other suites.
  • Standalone suites (marketing, otter-camp) do not depend on auth or api packages.
  • oracle-bridge proxies authentication requests for cookie-based SSO.

CI/CD Flow ​

mermaid
graph LR
    subgraph "Package Publishing"
        TAG["Tag Push v0.x.x"] --> PPW["package-publish.yml"]
        PPW --> TEST1["npm test"]
        TEST1 --> BUILD1["npm run build"]
        BUILD1 --> PUB["npm publish<br/>GitHub Packages"]
    end

    subgraph "Suite Deployment"
        PUSH["Push to main"] --> CI["ci.yml"]
        CI --> TSC["tsc --noEmit"]
        TSC --> LINT["eslint"]
        LINT --> TEST2["npm test"]
        TEST2 --> BUILD2["npm run build"]
        BUILD2 --> DEPLOY["deploy-staging.yml"]
        DEPLOY --> DFX["dfx deploy<br/>IC asset canister"]
    end

Cross-Suite Auth Flow ​

mermaid
sequenceDiagram
    participant User
    participant Suite A (e.g. think-tank)
    participant oracle-bridge
    participant auth-service canister
    participant Suite B (e.g. dao-suite)

    User->>Suite A: Login (email/password or II)
    Suite A->>oracle-bridge: POST /auth/login
    oracle-bridge->>auth-service canister: Authenticate
    auth-service canister-->>oracle-bridge: Session token
    oracle-bridge-->>Suite A: Set-Cookie (domain=.helloworlddao.com, httpOnly, secure, sameSite=lax)
    Note over Suite A: Cookie stored for .helloworlddao.com domain

    User->>Suite B: Navigate to different suite
    Note over Suite B: Cookie sent automatically (same parent domain)
    Suite B->>oracle-bridge: GET /auth/session (cookie included)
    oracle-bridge->>auth-service canister: Validate session
    auth-service canister-->>oracle-bridge: Session valid, user data
    oracle-bridge-->>Suite B: 200 OK + user data
    Suite B->>Suite B: authCookieClient populates localStorage
    Suite B-->>User: Authenticated view

Key CI/CD Details ​

  • Authentication: OIDC via GITHUB_TOKEN (no PAT needed)
  • Action pinning: All GitHub Actions SHA-pinned for supply-chain security
  • Reusable workflows: Referenced via @main (not SHA) from Hello-World-Co-Op/.github
  • Permissions: Explicit minimal permissions (contents: read, packages: read)
  • Registry: npm.pkg.github.com with scope @hello-world-co-op

RBAC Architecture ​

Role-Based Access Control (RBAC) is implemented platform-wide via the auth-service canister, with role information propagated through the authentication flow to frontend suites.

Role Storage and Management ​

Role storage: Roles are stored in the auth-service canister state:

rust
// auth-service canister state
struct State {
    users: HashMap<Principal, UserProfile>,
    user_roles: HashMap<Principal, Vec<Role>>,  // Principal -> ["Admin", "Member"]
    sessions: HashMap<String, UserSession>,
}

Role types:

  • Admin - Full system access (KYC review, member management, system monitoring)
  • Moderator - Content and community management
  • Member - Standard authenticated user (default)

Assignment rules:

  • New users automatically receive Member role on registration
  • Admin and Moderator roles are manually assigned by existing Admins or the canister controller
  • Roles are additive (user can have multiple roles)

Session Enhancement with Roles ​

When a session is created (login), the auth-service canister includes the user's roles in the UserSession struct:

rust
struct UserSession {
    user_id: Principal,
    email: String,
    roles: Vec<String>,        // ["admin", "member"]
    access_token: String,
    refresh_token: String,
    created_at: u64,
    expires_at: u64,
}

Roles are cached in the session token for performance. Important: Role changes take effect on the user's next login (sessions are not updated retroactively).

Oracle-Bridge Role Propagation ​

The oracle-bridge service exposes roles via the session endpoint:

Endpoint: GET /api/auth/session

Response:

json
{
  "authenticated": true,
  "user_id": "2vxsx-fae",
  "roles": ["admin", "member"]
}

This endpoint is called by frontend suites on app load to populate the auth state, including roles.

Auth Package RBAC Support ​

The @hello-world-co-op/auth package (v0.2.0+) provides React components and hooks for role-based UI:

Key exports:

  • useRoles() - Hook providing { roles, isLoading, hasRole, isAdmin }
  • <RoleGuard role="admin"> - Conditional rendering component
  • hasRole(role: string) - Utility function for role checks
  • isAdmin - Boolean convenience property

Example usage:

typescript
import { RoleGuard, useRoles } from '@hello-world-co-op/auth';

function Dashboard() {
  const { isAdmin, hasRole } = useRoles();

  return (
    <div>
      <h1>Dashboard</h1>

      {/* Admin-only section */}
      <RoleGuard role="admin">
        <AdminPanel />
      </RoleGuard>

      {/* Admin or Moderator */}
      <RoleGuard role={["admin", "moderator"]}>
        <ModerationTools />
      </RoleGuard>

      {/* Conditional logic */}
      {isAdmin && <button>System Settings</button>}
    </div>
  );
}

Security note: Frontend role guards are UX conveniences only. Always verify roles server-side in canister methods.

Canister Role Verification ​

Backend canisters validate roles by calling auth-service::validate_session_with_role:

rust
#[update]
async fn admin_only_method(access_token: String) -> Result<String, String> {
    let auth_service_id = Principal::from_text(AUTH_SERVICE_CANISTER_ID)?;

    let result: Result<(Result<SessionInfo, String>,), _> = call(
        auth_service_id,
        "validate_session_with_role",
        (access_token, "admin".to_string()),
    ).await;

    match result {
        Ok((Ok(session_info),)) => {
            // User is authenticated and has Admin role
            perform_admin_action(session_info.user_id)
        }
        Ok((Err(e),)) => Err(format!("Access denied: {}", e)),
        Err((code, msg)) => Err(format!("Auth service error: {:?} - {}", code, msg)),
    }
}

This pattern centralizes role management and eliminates the need for each canister to maintain its own role state.

RBAC Data Flow ​

mermaid
sequenceDiagram
    participant User
    participant Suite as Frontend Suite
    participant OB as oracle-bridge
    participant AS as auth-service
    participant Canister as Backend Canister

    Note over User,AS: Authentication + Role Assignment
    User->>Suite: Login
    Suite->>OB: POST /auth/login
    OB->>AS: authenticate(email, password)
    AS->>AS: Validate credentials
    AS->>AS: Load user roles from state
    AS-->>OB: Session + roles
    OB-->>Suite: Set-Cookie + session data
    Suite->>Suite: Store roles in $userRoles atom

    Note over User,Canister: Role-Based UI Rendering
    Suite->>Suite: useRoles() reads $userRoles
    Suite->>Suite: RoleGuard conditionally renders
    Suite-->>User: Role-appropriate UI

    Note over User,Canister: Server-Side Role Verification
    User->>Suite: Admin action (e.g., approve KYC)
    Suite->>Canister: approve_kyc(token, submission_id)
    Canister->>AS: validate_session_with_role(token, "admin")
    AS->>AS: Verify token + check user has Admin role
    alt Has Admin Role
        AS-->>Canister: Ok(SessionInfo)
        Canister->>Canister: Perform admin operation
        Canister-->>Suite: Success
        Suite-->>User: Action complete
    else Missing Role or Invalid Token
        AS-->>Canister: Err("Role not found")
        Canister-->>Suite: Access denied
        Suite-->>User: Error message
    end

Key points:

  1. Roles are assigned in auth-service canister state
  2. Roles are cached in session tokens at login time
  3. oracle-bridge exposes roles via /api/auth/session endpoint
  4. Frontend suites read roles from session and render role-appropriate UI
  5. Backend canisters verify roles via validate_session_with_role inter-canister call
  6. Role changes take effect on next login (sessions are not updated retroactively)

Role-Aware Suite Implementations ​

SuiteRBAC ImplementationProtected Routes/Features
dao-admin-suiteRoleGuard role="admin" wraps all routes except /login, /unauthorizedKYC review, member management, system monitoring, governance oversight, treasury management, content moderation
think-tank-suiteAdminGuard uses canister-level check (not auth-service RBAC)Admin-specific features (deferred migration to auth-service RBAC)
dao-suiteRoleGuard available for future role-gated features(Planned: proposal moderation, member directory filtering)
governance-suiteRoleGuard available for future role-gated features(Planned: proposal category restrictions, emergency proposals)

Migration notes:

  • think-tank-suite uses a canister-based AdminGuard that checks admin status directly in the dao-admin canister (not auth-service). Migration to auth-service RBAC is deferred for future work.
  • Other suites can adopt RBAC by upgrading to @hello-world-co-op/auth@^0.2.0 and using <RoleGuard> components.

Age Gate and Registration Flow ​

Status: Implemented (Epic BL-011)

The registration flow implements a COPPA-compliant age gate that verifies user age before any personal information is collected. This is implemented in the marketing-suite (public registration) and enforced server-side in the user-service canister.

For complete compliance details, see the COPPA Compliance documentation (available in the architecture/compliance directory).

Registration Flow with Age Gate ​

mermaid
flowchart TD
    A[User visits /register] --> B[DOB Input Displayed]
    B --> C{Age Calculation}
    C -->|Under 13| D[Block Screen]
    C -->|13-17| E[Form Revealed - Minor]
    C -->|18+| F[Form Revealed - Adult]
    D --> G[localStorage block flag set - 24h TTL]
    D --> H[Return to Home button only]
    E --> I[Fill PII: Name, Email, Password]
    F --> I
    I --> J[Client encrypts PII with AES-256-GCM]
    J --> K[Submit to user-service canister]
    K --> L{Server validates age}
    L -->|Under 13| M[Reject - COPPA block]
    L -->|13+| N[Create account]
    N --> O{Age >= 18?}
    O -->|Yes| P[is_age_verified_18_plus = true]
    O -->|No| Q[is_age_verified_18_plus = false]
    P --> R[Email verification]
    Q --> R
    R --> S[Mint Registered SBT]

Key Design Decisions ​

DOB as first field: The Date of Birth input is rendered before any PII fields (name, email, password). PII fields are conditionally rendered only after age is verified as 13+. This ensures COPPA compliance by preventing data collection from children under 13.

Two-phase membership minting:

  • Phase 1 (Registration): mint_membership_registered() creates an SBT with MembershipStatus::Registered. No expiration. Cannot vote.
  • Phase 2 (Upgrade): upgrade_to_active() transitions Registered to Active when user is 18+ and has paid dues. Sets December 31 expiry.

DOB as encrypted PII: DOB is encrypted client-side with AES-256-GCM (same master recovery key as name/email) and stored in the user-service canister as dob_encrypted. It is NOT stored on-chain in the membership SBT. A derived boolean is_age_verified_18_plus is cached for access control decisions.

Anti-circumvention: Client-side localStorage block flag (__hw_age_block, 24-hour TTL) deters casual re-attempts. Server-side validation in validate_age_from_dob() is the authoritative control and cannot be bypassed by client manipulation.

Age-Based Access Matrix ​

FeatureUnder 1313-17 (Registered)18+ (Registered)18+ (Active)
RegistrationBlockedAllowedAllowedN/A
Platform accessNoneLimitedLimitedFull
Otter Camp gameNoYesYesYes
Governance votingNoNoNoYes
Proposal creationNoNoNoYes
Upgrade to ActiveN/ABlocked (age < 18)Pay duesN/A
  • COPPA Compliance -- Full COPPA compliance document with FTC rule mapping (see architecture/compliance directory)
  • Epic BL-011 -- Epic plan for registration + COPPA (see bmad-artifacts/planning-artifacts directory)

Cross-Suite Navigation ​

Cross-suite navigation allows users to seamlessly switch between different suite applications while maintaining their authentication state. This is implemented via the SuiteSwitcher component in @hello-world-co-op/ui@0.2.1.

SuiteSwitcher Component Overview ​

The SuiteSwitcher is a shared component that provides a consistent navigation experience across all authenticated suites. It displays a dropdown menu with links to other suite applications.

Location: @hello-world-co-op/ui package Component: SuiteSwitcherVersion: Available since v0.2.1

Architecture ​

The cross-suite navigation system follows a shared component, per-suite integration pattern:

  1. Shared Component: The SuiteSwitcher UI component is defined once in the @hello-world-co-op/ui package
  2. Environment Configuration: Each suite defines its own environment variables pointing to other suite URLs
  3. Props-Based Integration: Suites pass their specific configuration via props to the shared component
  4. Role-Based Visibility: The admin suite link is only shown to users with the Admin role

Role-Based Visibility ​

The SuiteSwitcher component integrates with the platform RBAC system to conditionally display suite links based on user roles:

  • Admin Suite: Only visible to users with the Admin role (checked via useRoles() hook)
  • Other Suites: Visible to all authenticated users

Example implementation:

tsx
import { SuiteSwitcher } from '@hello-world-co-op/ui';
import { useRoles } from '@hello-world-co-op/auth';

function Layout() {
  const { isAdmin } = useRoles();

  return (
    <SuiteSwitcher
      currentSuite="think-tank"
      suiteUrls={{
        portal: import.meta.env.VITE_DAO_FRONTEND_URL,
        founderyOs: import.meta.env.VITE_THINK_TANK_URL,
        governance: import.meta.env.VITE_GOVERNANCE_SUITE_URL,
        otterCamp: import.meta.env.VITE_OTTER_CAMP_URL,
        ...(isAdmin && { admin: import.meta.env.VITE_ADMIN_SUITE_URL }),
      }}
    />
  );
}

Environment Variable Pattern ​

Each suite that implements cross-suite navigation defines the following environment variables:

VariableDescription
VITE_DAO_FRONTEND_URLURL for the DAO portal (main member dashboard)
VITE_THINK_TANK_URLURL for Think Tank productivity suite
VITE_GOVERNANCE_SUITE_URLURL for governance proposals and voting
VITE_OTTER_CAMP_URLURL for Otter Camp game suite
VITE_ADMIN_SUITE_URLURL for admin dashboard (admin users only)

Example values (staging):

bash
VITE_DAO_FRONTEND_URL=https://staging-portal.helloworlddao.com
VITE_THINK_TANK_URL=https://staging-think-tank.helloworlddao.com
VITE_GOVERNANCE_SUITE_URL=https://staging-governance.helloworlddao.com
VITE_OTTER_CAMP_URL=https://staging-ottercamp.helloworlddao.com
VITE_ADMIN_SUITE_URL=https://staging-admin.helloworlddao.com

Integration Pattern ​

Suites integrate the SuiteSwitcher component by:

  1. Installing @hello-world-co-op/ui@^0.2.1 (or later)
  2. Defining suite URL environment variables
  3. Importing and rendering the component with appropriate props

Code example:

tsx
import { SuiteSwitcher } from '@hello-world-co-op/ui';

function AppLayout() {
  return (
    <div className="app-layout">
      <header>
        <SuiteSwitcher
          currentSuite="think-tank"  // 'portal' | 'think-tank' | 'governance' | 'otter-camp' | 'admin'
          suiteUrls={{
            portal: import.meta.env.VITE_DAO_FRONTEND_URL,
            founderyOs: import.meta.env.VITE_THINK_TANK_URL,
            governance: import.meta.env.VITE_GOVERNANCE_SUITE_URL,
            otterCamp: import.meta.env.VITE_OTTER_CAMP_URL,
            admin: import.meta.env.VITE_ADMIN_SUITE_URL,
          }}
        />
      </header>
      {/* ... */}
    </div>
  );
}

Suite Implementation Status ​

SuiteHas SuiteSwitcherReason
think-tank-suiteYesAuthenticated productivity app
dao-suiteYesAuthenticated member dashboard
governance-suiteYesAuthenticated governance app
dao-admin-suiteYesAdmin-only dashboard with role-based access
marketing-suiteNoPublic-facing marketing site (no authentication)
otter-camp-suiteNoStandalone game with independent auth pattern

Rationale for exclusions:

  • marketing-suite: Public site with no user authentication — cross-suite navigation is not applicable
  • otter-camp-suite: Standalone game with passive auth (read-only localStorage). Game does not actively manage sessions or make authenticated canister calls, so cross-suite navigation would create confusion

Cross-Suite Authentication ​

Authentication flows through the shared auth-service canister via oracle-bridge. The oracle-bridge acts as a proxy that sets httpOnly cookies on the .helloworlddao.com domain, enabling single sign-on across all suites.

PropertyValuePurpose
Domain.helloworlddao.comShared across all subdomains
SameSitelaxAllows cross-subdomain navigation
SecuretrueHTTPS only
HttpOnlytrueNot accessible via JavaScript

Critical: The leading dot in .helloworlddao.com is required. Without it, cookies are scoped to a single subdomain and SSO breaks.

Auth Endpoints (via oracle-bridge) ​

EndpointMethodPurpose
/auth/loginPOSTAuthenticate and set session cookie
/auth/sessionGETValidate session, return user data
/auth/logoutPOSTClear session cookie (requires CSRF token)
/healthGETHealth check

Staging URL: https://staging-oracle.helloworlddao.comLocal URL: http://localhost:3000

localStorage Bridge ​

For suites using the Auth Bridge pattern, cookie-based session data is synchronized to localStorage via authCookieClient. This is needed because some legacy components read auth state from localStorage rather than cookies.

Flow: Cookie (set by oracle-bridge) -> authCookieClient reads session -> populates localStorage.user_data

Migration Patterns ​

When migrating an existing suite to consume shared packages, two patterns are used:

Re-export Pattern (API) ​

Suite wraps shared package exports in a local module for gradual migration:

typescript
// src/services/apiHelpers.ts
export { createActor } from '@hello-world-co-op/api';
export type { ApiResponse, ActorOptions } from '@hello-world-co-op/api';
export { isSuccess, isError, ERROR_CODES } from '@hello-world-co-op/api';

Existing code continues importing from ../services/apiHelpers -- no import path changes needed.

Bridge Pattern (Auth) ​

Suite creates a context bridge that maps internal auth state to the shared interface:

typescript
// src/components/auth/AuthProviderBridge.tsx
import type { AuthContextValue } from '@hello-world-co-op/auth';
// ... bridges nanostores auth -> AuthContextValue

Direct Consumption (UI) ​

Components imported directly from packages:

typescript
import { ErrorBoundary } from '@hello-world-co-op/ui';
import { cn } from '@hello-world-co-op/ui';

Architecture Decision Records ​

Standalone vs Shared Package Decision Matrix ​

When creating a new suite, use this matrix to decide whether it should consume shared packages or remain standalone:

FactorUse Shared PackagesStay Standalone
User authenticationSuite requires login, sessions, protected routesSuite is public-facing, no user-specific content
IC canister interactionSuite calls multiple canisters (governance, treasury, etc.)Suite has no or minimal canister interaction
UI consistencySuite must match the authenticated app experienceSuite has a distinct visual identity (game, marketing)
Update frequencySuite benefits from coordinated auth/API updatesSuite has an independent release cycle
Bundle size sensitivityBundle size is not a primary concernEvery KB matters (games, public landing pages)
Rendering strategyClient-side SPA with runtime authSSR/SSG pre-rendering for SEO

Decision rule: If a suite requires user authentication or makes IC canister calls on behalf of users, use shared packages (Auth Bridge or Auth Direct). If the suite is public-facing or has its own rendering engine, stay standalone.

Validated examples:

  • Shared (Auth Bridge): think-tank-suite, dao-suite, dao-admin-suite — complex authenticated apps with many canister calls
  • Shared (Auth Direct): governance-suite — reads auth state via cookies, uses @hello-world-co-op/ui only
  • Standalone with UI: marketing-suite — public site with SEO pre-rendering, uses @hello-world-co-op/ui for visual consistency but no auth
  • Standalone without packages: otter-camp-suite — Phaser.js game engine, no shared package dependencies

Marketing Suite: Standalone Architecture Decision ​

The marketing-suite intentionally deviates from the shared package pattern. This is a deliberate architectural decision, not an oversight.

Rationale:

  • Leanness: Marketing site serves static/SEO content. Adding auth and api packages would inflate the bundle with unused dependencies.
  • Different lifecycle: Marketing content changes independently of app features. Decoupling from shared package versions prevents unnecessary coordination.
  • Minimal canister interaction: The marketing site makes zero IC canister calls. The @hello-world-co-op/api package provides no value here.
  • Pre-rendering compatibility: SSG (vite-ssg) with ReactDOMServer.renderToString() works best with minimal dependencies. Auth context and IC agent code add hydration complexity.

What it does use: @hello-world-co-op/ui for shared Tailwind config and base components (Button, Card, etc.) to maintain visual consistency with the authenticated suites.

Game Suite Extraction Pattern ​

The otter-camp-suite extraction from the frontend monolith established patterns for game-specific suites:

Bundle optimization: Phaser.js (~340KB gzipped) is extracted to a standalone lazy-loaded chunk. The game page is lazy-imported in App.tsx, keeping the initial bundle at ~58KB. Phaser and game code load only when the user navigates to /otter-camp.

React-Phaser bridging: Game suites use nanostores for state management instead of React Context. This enables:

  • Game engine (Phaser) to read/write state without React re-renders
  • React UI overlays (HUD, menus) to reactively update from game state
  • Clean separation between game loop and React lifecycle

ESLint configuration: Game code requires relaxed linting rules compared to standard React:

  • no-param-reassign: off — Phaser mutates game objects in place
  • class-methods-use-this: off — Phaser lifecycle methods (create, update) don't always use this
  • no-bitwise: off — Common in game math (color manipulation, flags)

Auth integration: Game suites read user_data from localStorage passively (read-only). Auth state is set by auth-integrated suites (dao-suite, think-tank). The game never performs auth operations itself — it only checks if a user is logged in to unlock member-only features.

WebSocket integration: Multiplayer presence uses WebSocket connections managed by the game engine, not React. The connection lifecycle is tied to the Phaser scene, not React component mount/unmount.

IC Canister Communication Patterns ​

Anti-Pattern: fetch() with no-cors for Canister Queries ​

Never use fetch() with mode: 'no-cors' for Internet Computer canister queries. This is a common mistake when developers encounter CORS errors and attempt to work around them.

Why this fails:

  • IC boundary nodes require proper CORS headers for canister queries
  • no-cors mode makes responses opaque (unreadable) — you cannot access the response body or status
  • The IC HTTP Gateway expects Candid-encoded requests, not plain HTTP
  • You will get silent failures or empty responses that are impossible to debug

Example of incorrect pattern:

typescript
// ❌ ANTI-PATTERN - Do not use
const response = await fetch(`https://ic0.app/api/v2/canister/${canisterId}/query`, {
  mode: 'no-cors',  // Makes response opaque and unreadable
});
// response.json() will throw - response body is inaccessible

Correct Pattern: HttpAgent for Canister Queries ​

Use @dfinity/agent's HttpAgent and Actor for all canister communication:

Anonymous queries (for public query methods):

typescript
import { HttpAgent, Actor } from '@dfinity/agent';
import { IDL } from '@dfinity/candid';

const IC_HOST = import.meta.env.VITE_IC_HOST || 'https://ic0.app';
const CANISTER_ID = import.meta.env.VITE_CANISTER_ID;

// Define minimal IDL for the methods you need
const idlFactory = ({ IDL }) => {
  return IDL.Service({
    icrc1_balance_of: IDL.Func(
      [IDL.Record({ owner: IDL.Principal, subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)) })],
      [IDL.Nat],
      ['query'],
    ),
  });
};

// Anonymous agent - safe for public queries
const agent = HttpAgent.createSync({ host: IC_HOST });
const actor = Actor.createActor(idlFactory, {
  agent,
  canisterId: CANISTER_ID,
});

// Query the canister
const balance = await actor.icrc1_balance_of({ owner, subaccount: [] });

Authenticated update calls (require session token):

typescript
import { HttpAgent, Actor } from '@dfinity/agent';

// Create an authenticated agent with identity
const agent = HttpAgent.createSync({
  host: IC_HOST,
  identity: yourIdentity, // From @dfinity/identity
});

const actor = Actor.createActor(idlFactory, { agent, canisterId });
const result = await actor.update_method(args);

When to Use fetch() vs HttpAgent ​

ScenarioToolExample
IC canister queryHttpAgent + ActorFetching token balance, blog posts, membership status
IC canister updateHttpAgent + ActorCreating proposals, minting NFTs, transferring tokens
oracle-bridge REST endpointfetch() with credentials: 'include'/api/auth/login, /api/auth/session, /api/auth/logout
External HTTP APIfetch()Third-party APIs, webhooks, external services

Key rule: If you're calling an IC canister method defined in a .did file, use HttpAgent. If you're calling a REST endpoint (oracle-bridge, external API), use fetch().

Real-World Examples ​

Blog post listing (anonymous query):

typescript
// From: marketing-suite/src/services/blogCanister.ts
const idlFactory = ({ IDL }) => {
  return IDL.Service({
    list_posts: IDL.Func(
      [IDL.Record({ page: IDL.Nat32, page_size: IDL.Nat32 })],
      [IDL.Variant({ Ok: PaginatedPostResultType, Err: BlogErrorVariant })],
      ['query'],
    ),
  });
};

const agent = HttpAgent.createSync({ host: 'https://ic0.app' });
const actor = Actor.createActor(idlFactory, {
  agent,
  canisterId: import.meta.env.VITE_BLOG_CANISTER_ID,
});

const result = await actor.list_posts({ page: 1, page_size: 50 });

Token balance check (anonymous query):

typescript
// From: dao-suite/src/services/tokenService.ts
const icrc1BalanceOfIdl = IDL.Service({
  icrc1_balance_of: IDL.Func(
    [IDL.Record({ owner: IDL.Principal, subaccount: IDL.Opt(IDL.Vec(IDL.Nat8)) })],
    [IDL.Nat],
    ['query'],
  ),
});

const agent = HttpAgent.createSync({ host: IC_HOST });
const actor = Actor.createActor(() => icrc1BalanceOfIdl, {
  agent,
  canisterId: DOM_TOKEN_CANISTER_ID,
});

const balance = await actor.icrc1_balance_of({ owner, subaccount: [] });

Authentication (oracle-bridge REST endpoint):

typescript
// From: @hello-world-co-op/auth package
const response = await fetch(`${oracleBridgeUrl}/api/auth/login`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  credentials: 'include',  // Required for cookie-based auth
  body: JSON.stringify({ email, password }),
});

Testing and Development ​

For local development with PocketIC:

typescript
const agent = HttpAgent.createSync({
  host: 'http://127.0.0.1:4943', // Local PocketIC instance
});

// Must fetch root key for local replicas
if (import.meta.env.VITE_NETWORK === 'local') {
  await agent.fetchRootKey();
}

For production (IC mainnet):

typescript
const agent = HttpAgent.createSync({
  host: 'https://ic0.app', // IC mainnet boundary node
});
// Never call fetchRootKey() in production - uses hardcoded root key

Hello World DAO