Vertical Slices
The frontend uses a vertical slice architecture. Each feature is a self-contained module with its own types, schemas, API client, composables, components, and stores. Features are organized into three layers with strict dependency rules.
Layer Hierarchy
frontend/
├── features/
│ ├── foundation/ # Layer 0 — Auth, tenancy, entitlements, settings, profile
│ ├── core/ # Layer 1 — Business domains: billing, catalog, team, notifications
│ └── product/ # Layer 2 — Product features: onboarding, analytics, platform
└── common/ # Cross-cutting utilities (auto-imported)
Dependency Rules
Dependencies flow downward only:
product/ → can import → core/, foundation/, common/
core/ → can import → foundation/, common/
foundation/ → can import → common/ only
common/ → imports NOTHING from features/
These rules are enforced by an ESLint rule. Violating the hierarchy — for example, importing from core/team inside foundation/auth — is rejected at lint time.
What Goes Where
| Layer | Contents | Examples |
|---|---|---|
foundation/ | Infrastructure that every feature depends on | Auth store, tenant resolution, entitlements, user profile, settings, currencies |
core/ | Business domains with API endpoints | Billing, team management, product catalog, notifications |
product/ | End-user product features | Onboarding flow, analytics dashboard, platform admin |
common/ | Shared utilities with no feature dependencies | API client, case transforms, money formatting, UI components |
Feature Module Structure
Every feature in core/ or product/ follows the same file structure:
features/core/<feature>/
├── index.ts # Barrel exports — public API (REQUIRED)
├── types.ts # TypeScript types (REQUIRED)
├── schemas.ts # Zod validation schemas (REQUIRED)
├── api/ # API client composable (core/product only)
│ ├── index.ts # Re-exports the API composable
│ └── <feature>.api.ts
├── composables/ # Reactive logic (useSubscription, useTeamMembers, etc.)
├── components/ # Vue components
├── stores/ # Pinia stores (if needed)
└── __tests__/ # Vitest tests
api/ folder. They access the API directly through stores (e.g., the auth store calls /auth/me on bootstrap).Real Example: The Billing Feature
Here is the actual structure of the billing feature — the most complex module in the codebase:
features/core/docs/billing/
├── index.ts # Barrel: 70+ exports
├── types.ts # 60+ types inferred from Zod
├── schemas.ts # All Zod schemas + status constants
├── api/
│ ├── index.ts # Re-exports useBillingApi
│ ├── billing.api.ts # 15+ API methods
│ └── public-catalog.api.ts # Public (unauthenticated) catalog API
├── composables/
│ ├── useSubscription.ts # Current subscription + cancel/resume
│ ├── useInvoices.ts # Invoice list + PDF download
│ ├── usePlans.ts # Available plans + eligibility
│ ├── useCheckout.ts # Checkout flow orchestration
│ ├── useSeatBilling.ts # Seat-based billing info
│ ├── useBillingPortal.ts # Stripe billing portal
│ ├── useUsage.ts # Usage metering
│ └── usePublicCatalog.ts # Public pricing page
├── components/
│ ├── BillingSubscriptionCard.vue
│ ├── BillingSubscriptionStatus.vue
│ ├── BillingInvoiceList.vue
│ ├── BillingCancelModal.vue
│ ├── BillingChangePlanModal.vue
│ ├── BillingSeatInfo.vue
│ ├── UsageMeterCard.vue
│ └── ... (15+ components total)
├── schemas/
│ └── public-catalog.schemas.ts
├── types/
│ └── public-catalog.types.ts
└── __tests__/
Barrel Exports
Every feature has an index.ts that defines its public API. Other modules import only through this barrel — never from internal files.
// ─── Types ─────────────────────────────────────────────────────────
export type {
Subscription, Invoice, BillingPlan, CheckoutSession,
PlanFeature, SeatBilling, ProrationPreview,
// ... 30+ more types
} from './types'
// ─── Status Constants ──────────────────────────────────────────────
export {
ACTIVE_STATUSES, CANCELLABLE_STATUSES, RESUMABLE_STATUSES,
SUBSCRIPTION_STATUS_COLORS, INVOICE_STATUS_COLORS,
// ... more constants
} from './schemas'
// ─── Schemas ───────────────────────────────────────────────────────
export {
subscriptionSchema, invoiceSchema, billingPlanSchema,
checkoutInputSchema, cancelSubscriptionInputSchema,
// ... more schemas
} from './schemas'
// ─── API ───────────────────────────────────────────────────────────
export { useBillingApi } from './api'
// ─── Composables ───────────────────────────────────────────────────
export { useSubscription } from './composables/useSubscription'
export { useInvoices } from './composables/useInvoices'
export { usePlans } from './composables/usePlans'
export { useCheckout } from './composables/useCheckout'
export { useSeatBilling } from './composables/useSeatBilling'
// ─── Components ────────────────────────────────────────────────────
export { default as BillingSubscriptionCard } from './components/BillingSubscriptionCard.vue'
export { default as BillingInvoiceList } from './components/BillingInvoiceList.vue'
// ... more components
Import Rules
// ✅ Import through the barrel
import { useSubscription, type Subscription } from '@core/docs/billing'
// ❌ NEVER import internals
import { helper } from '@core/docs/billing/_internal/...'
Foundation features are auto-imported by Nuxt — you use them directly:
// Foundation: no import needed
const { tenant, hasTenantPermission } = useCurrentTenant()
const authStore = useAuthStore()
The common/ Directory
common/ contains cross-cutting utilities that have no feature dependencies. It is auto-imported by Nuxt.
common/
├── api/
│ ├── client.ts # useApiClient — centralized HTTP client
│ ├── errors.ts # Typed error classes (ApiError, ValidationError, ...)
│ ├── schema.ts # Pagination + error response Zod schemas
│ ├── url.ts # URL builder utility
│ └── _lib/
│ └── billing-helpers.ts # PDF download, billing error helpers
├── composables/
│ ├── useAuthenticatedAsyncData.ts # SSR-safe data fetching wrapper
│ ├── useLocalizedCollection.ts
│ ├── useRoleLabel.ts
│ └── useZodI18nValidation.ts
├── components/
│ ├── layout/ # AppLogo, Header, Footer, LanguageSelector
│ ├── marketing/ # HeroBackground, PromotionalVideo
│ └── ui/ # ConfirmModal, ImagePlaceholder
├── types/
│ ├── content.ts # Nuxt Content collection types
│ └── money.ts # Money schema + type
└── utils/
├── index.ts # Barrel: toCamelCase, toSnakeCase, formatMoney, ...
└── _lib/
├── caseTransform.ts # Deep snake_case ↔ camelCase conversion
├── cookie.ts # XSRF token reader
├── money.ts # Locale-aware money formatting
├── pricing.ts # Static pricing helpers
├── queryBuilder.ts # Spatie QueryBuilder URL params
└── currency-decimals.ts # ISO 4217 minor units
Key utilities:
useApiClient()— Centralized HTTP client with CSRF handling, tenant headers, error interceptors, and automatic case transformationuseAuthenticatedAsyncData()— WrapsuseAsyncDatawithserver: falsefor SSR-safe authenticated data fetchingtoCamelCase()/toSnakeCase()— Deep recursive key transformation bridging Laravel's snake_case API and TypeScript's camelCaseformatMoney()— Locale-aware money formatting with ISO 4217 decimal handling
Cross-Feature Imports
Within core/, features are isolated from each other. The exception is catalog, which is designated as an "owner" feature that other core features can import:
// ✅ billing imports from catalog (catalog is an owner feature)
import { usePlans } from '@core/catalog'
// ❌ billing imports from team (not allowed)
import { useTeamMembers } from '@core/team'
If two core features need to share data, the coordination happens at the page level — the page imports both composables and passes data between them.
What's Next
- Composables & Stores — How reactive state is managed with Pinia and composable facades
- Validation — Runtime validation with Zod schemas
- API Contracts — The full contract system bridging backend and frontend