Skip to content

Web App Structure

The web portal is a Next.js 14 application using the App Router, deployed to Cloudflare Pages.

Directory Structure

apps/web/app/
├── (auth)/                       # Unauthenticated route group
│   ├── login/page.tsx            # Email/password login
│   └── accept-invite/page.tsx    # Cognito invite acceptance
├── (dashboard)/                  # Protected route group
│   ├── layout.tsx                # Role-based navigation bar
│   ├── dashboard/page.tsx        # Customer user: time entries view
│   ├── my-time-and-tasks/        # Internal user workspace
│   │   ├── layout.tsx            # SubTabNav: Timebills | Time Entries | Tasks
│   │   ├── timebills/page.tsx
│   │   ├── time-entries/page.tsx
│   │   └── tasks/page.tsx
│   ├── admin/time-and-tasks/     # Admin view of all data
│   │   ├── layout.tsx            # SubTabNav: Timebills | Time Entries | Tasks
│   │   ├── timebills/page.tsx
│   │   ├── time-entries/page.tsx
│   │   └── tasks/page.tsx
│   ├── customers/page.tsx        # Customer management (CRUD, pillars, users)
│   ├── settings/page.tsx         # Profile, Xero, Slack channel mappings, wallet
│   ├── pillars/page.tsx          # Customer admin: pillar management
│   ├── customer-users/page.tsx   # Customer admin: user management
│   └── docs/page.tsx             # Embedded MKDocs viewer (iframe)
├── api/wallet-pass/              # Apple Wallet pass generation
├── layout.tsx                    # Root layout (fonts, metadata)
├── providers.tsx                 # React Query provider
└── page.tsx                      # Redirect to /dashboard

Role-Based Navigation

The dashboard layout ((dashboard)/layout.tsx) dynamically builds navigation based on custom:userRole:

flowchart TD
    A{User Role?} --> B["customer_*"]
    A --> C["internal_*<br/>(or unset)"]

    B --> D["Time Entries → /dashboard"]
    B --> E["Settings → /settings"]
    B --> F{customer_admin?}
    F -->|Yes| G["Pillars → /pillars"]
    F -->|Yes| H["User Management → /customer-users"]

    C --> I["My Time and Tasks → /my-time-and-tasks"]
    C --> J["All Time and Tasks → /admin/time-and-tasks"]
    C --> K["Customers → /customers"]
    C --> L["Docs → /docs"]
    C --> E

Auth Flow

sequenceDiagram
    participant User
    participant Browser
    participant AuthContext
    participant Cognito

    User->>Browser: Navigate to /dashboard
    Browser->>AuthContext: Check isAuthenticated
    alt Not authenticated
        AuthContext-->>Browser: Redirect to /login
        User->>Browser: Enter email + password
        Browser->>Cognito: authenticateUser()
        alt New password required
            Cognito-->>Browser: newPasswordRequired challenge
            User->>Browser: Set new password
            Browser->>Cognito: completeNewPasswordChallenge()
        end
        Cognito-->>Browser: JWT tokens
        Browser->>AuthContext: Store tokens, decode claims
    end
    AuthContext-->>Browser: Render dashboard

JWT Claim Extraction

The AuthContext decodes the Cognito ID token using window.atob() to extract:

  • custom:userRole — determines navigation items and API permissions
  • custom:customerId — scopes data fetches to the user's customer
  • sub — unique user ID for API calls

Token Refresh

The useApiToken() hook returns a callback that:

  1. Returns the cached token if still valid
  2. Calls refreshToken() from AuthContext if expired
  3. Throws an error if user is not authenticated

SubTabNav Pattern

The SubTabNav component provides tab navigation within sections.

Component: apps/web/components/SubTabNav.tsx

interface SubTabNavProps {
  basePath: string;
  tabs: { name: string; href: string }[];
}

Usage example (in a layout.tsx):

<SubTabNav basePath="/my-time-and-tasks" tabs={[
  { name: 'Timebills', href: 'timebills' },
  { name: 'Time Entries', href: 'time-entries' },
  { name: 'Tasks', href: 'tasks' },
]} />

Active state logic: Checks if pathname.startsWith(basePath + '/' + href).

Adding a New Tab

  1. Add an entry to the tabs array in the parent layout
  2. Create a new page at {basePath}/{href}/page.tsx
  3. The tab will automatically appear in the SubTabNav bar

Data Fetching

  • React Query (TanStack Query) for server state management
  • Configured with refetchOnWindowFocus: false and retry: 1
  • API calls go through server actions in apps/web/actions/api.ts
  • Errors are forwarded to Sentry

Styling

Element Value
CSS Framework Tailwind CSS
Font League Spartan (Google Fonts)
brand-background #F8E794 (light yellow)
brand-menubar #284139 (dark teal)
brand-tile #B86830 (brown)
brand-accent #809076 (sage green)
brand-foreground #111A19 (near-black)

Key Dependencies

Package Version Purpose
next 14.0.4 Framework
react 18.2.0 UI library
amazon-cognito-identity-js 6.3.12 Cognito authentication
react-query 3.39.3 Server state management
@heroicons/react 2.1.1 Icon library
tailwindcss 3.4.0 Utility-first CSS
@sentry/react 10.39.0 Error tracking
passkit-generator 3.5.7 Apple Wallet pass generation