Company-Namespaced Portal Shell
Company-Namespaced Portal Shell
Introduced in v0.1.15, the company-namespaced route shell is the core architectural layer that makes each agency's myProp portal distinct. It handles authentication, company resolution, branding, and navigation — so that every page within /:company is automatically scoped to the correct agency and styled with its brand colours.
How It Works
When a user visits a company URL (e.g., /acme-lettings), the following happens in sequence:
User visits /:company
│
▼
Middleware checks auth
└─ Not signed in → redirect to /sign-in?callbackUrl=/:company
│
▼
Server layout validates slug format
└─ Empty or >200 chars → 404
│
▼
AgentOS API availability check
└─ Not configured → render fallback shell with defaults
│
▼
verifyCompany(slug)
└─ Not found or inactive → 404 (Company Not Found page)
│
▼
Parallel fetch: branding + branches + currency
└─ Each call has independent error fallback
│
▼
Client layout renders:
CompanyProvider → CompanyThemeProvider → CompanyShell → {children}
Authentication
Authentication is checked at two layers:
- Middleware — intercepts the request before any rendering; redirects unauthenticated users to
/sign-inwith acallbackUrlpointing back to the original company URL. - Server layout — re-checks the session server-side as a belt-and-suspenders guard before any API calls are made.
Company Verification
The verifyCompany(slug) function calls the AgentOS API to confirm:
- The company short name exists
- The company is currently active
If either check fails, Next.js's notFound() is called, rendering the Company Not Found page.
Company Context
All data fetched during layout initialisation is made available to child components via the useCompany() hook:
import { useCompany } from '@/contexts/company-context';
export function MyComponent() {
const { company, branding, branches, currency, shortName } = useCompany();
return <p>Welcome to {company.companyName}</p>;
}
Context Shape
interface CompanyContextValue {
shortName: string; // URL slug, e.g. "acme-lettings"
company: CompanyInfo; // Name, address, phone, email, website, isActive
branding: CompanyBranding; // Hex colours, logo/icon/banner URLs, custom CSS
branches: CompanyBranch[]; // List of agency branches
currency: CurrencyInfo; // { isoCode, symbol, name }
}
useCompany() must only be called inside a component that is a descendant of a /:company route. Calling it outside that context throws.
Runtime Theming
The CompanyThemeProvider converts AgentOS hex brand colours into OKLCH CSS custom properties at runtime, overriding the default Tailwind/shadcn theme:
Colour Mapping
| AgentOS field | Overridden CSS variables |
|---|---|
primaryColour | --primary, --ring, --sidebar-primary, --chart-1 |
secondaryColour | --secondary, --accent |
backgroundColour | --background, --card, --popover |
textColour | --foreground, --card-foreground |
In addition, the provider auto-generates:
- Foreground contrast colours for primary/secondary surfaces
- Muted variants of background and accent colours
- Sidebar dark variant derived from
primaryColour
Conversion Pipeline
Colours are converted through a three-step pipeline:
Hex string (e.g. #2563EB)
└─ Parse to RGB (0–255 per channel)
└─ Normalise to linear light
└─ Convert to OKLCH (L, C, H)
└─ Write as CSS custom property
If a colour field is null or unparseable, that CSS variable is left at its default value — the rest of the theme continues to apply normally.
Navigation Shell
CompanyShell wraps all company page content with a responsive navigation frame:
Desktop
- Fixed sidebar, always visible
- Company logo and name at the top
- Navigation links
- Company contact info (phone, email) at the bottom
- User profile dropdown with sign-out
Mobile
- Hamburger button in the top bar
- Slide-out drawer containing the same navigation links
- User profile dropdown with sign-out
Route Files Reference
| Path | Type | Purpose |
|---|---|---|
src/app/[company]/layout.tsx | Server Component | Auth, verification, data fetch, metadata |
src/app/[company]/company-layout-client.tsx | Client Component | Wires context + theme + shell |
src/app/[company]/page.tsx | Server Component | Company home with quick-access cards |
src/app/[company]/not-found.tsx | Client Component | Invalid/inactive company slug |
src/app/[company]/loading.tsx | Server Component | Skeleton while data fetches |
src/app/[company]/error.tsx | Client Component | Runtime error boundary with retry |
src/contexts/company-context.tsx | Context | CompanyProvider + useCompany() hook |
src/components/company-theme-provider.tsx | Client Component | Hex → OKLCH CSS variable injection |
src/components/company-shell.tsx | Client Component | Responsive nav sidebar/drawer |
Company Not Found Page
The not-found page is shown when:
- The URL slug doesn't match any AgentOS agency
- The company has been deactivated
- The slug is malformed (empty or over 200 characters)
It provides users with actionable guidance: verify the URL, contact their agent, or check their invitation email.
Error Boundary
Runtime errors within the company shell are caught by error.tsx. The boundary:
- Logs the error with
captureError(), including the error digest for tracing - Displays a recovery UI with a Try Again button (calls
reset()) and a Back to Home link - Does not expose raw error messages to the user
Graceful Degradation
The layout is designed to never crash due to AgentOS API unavailability:
- If the API is not configured → renders a fallback shell with default branding
- If
getCompanyBrandingfails → falls back tonullcolours (default theme preserved) - If
getCompanyBranchesfails → falls back to an empty array - If
getCurrencyInfofails → falls back to GBP (£)
Each fallback is independent; a failure in one API call does not affect the others.
Route Priority
Next.js resolves static route segments before dynamic ones. The [company] segment will never capture requests to existing static routes such as /dashboard, /sign-in, /privacy, or /api/*. No existing routes are affected by this addition.
Health Check Endpoint
The middleware now fast-paths requests to /api/health, bypassing all auth and redirect logic and returning 200 immediately. This ensures uptime monitors and load balancers always receive a reliable response regardless of session state.