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 permissionscustom:customerId— scopes data fetches to the user's customersub— unique user ID for API calls
Token Refresh¶
The useApiToken() hook returns a callback that:
- Returns the cached token if still valid
- Calls
refreshToken()from AuthContext if expired - 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¶
- Add an entry to the
tabsarray in the parent layout - Create a new page at
{basePath}/{href}/page.tsx - The tab will automatically appear in the SubTabNav bar
Data Fetching¶
- React Query (TanStack Query) for server state management
- Configured with
refetchOnWindowFocus: falseandretry: 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 |