Appearance
Collage Admin (Nuxt 4) ​
Nuxt 4 SSR rewrite of the Admin-Frontend. Same domain features as the Nuxt 2 app — DAM, workspaces, portals, search, analytics — rebuilt with the Composition API, Pinia, TanStack Query, and strict TypeScript.
Dev server runs on port 5000 (nuxt.config.ts devServer.host: '0.0.0.0').
Tech Stack ​
| Layer | Choice |
|---|---|
| Framework | Nuxt 4 (compatibilityDate: '2025-06-30') + Vue 3 |
| UI components | Vuetify 3 (auto-import via vite-plugin-vuetify) |
| State | Pinia 3 (Options Store syntax) |
| Data-fetching | TanStack Vue Query 5 (useQuery / useMutation) |
| HTTP | $fetch → $api (configured in plugins/fetch.ts) |
| Search | Typesense 3 (server-proxy Nitro routes hide the API key) |
| Realtime | Laravel Echo 2 + Pusher 8 (echo.client.ts) |
| Analytics | Amplitude browser 2 + Session Replay 1 |
| Auth | Custom JWT — cookie (SSR) + localStorage + useState |
| Passkeys | @simplewebauthn/browser |
| Storage | AWS S3 multipart (server-proxy Nitro routes) |
| Validation | Zod 4 |
| Image editing | Cropper.js + Jimp + html-to-image |
| Charts | Chart.js 4 + vue-chartjs |
| Payments | Stripe (keys in runtime config, never in client bundle) |
| Styling | SCSS (sass-embedded), MDI fonts, custom collage icon collection |
| Language | TypeScript 5 strict mode |
| Package manager | pnpm 9 only |
| Node | ^22.12.0 / ^24.11.0 / ≥26.0.0 |
Project Structure ​
collage-admin/
├── app/ # All application source (Nuxt 4 convention)
│ ├── pages/ # File-based routing
│ ├── components/ # Vue SFCs (PascalCase, pathPrefix: false)
│ ├── composables/ # Auto-imported composables (api/, auth/, core/, queries/)
│ ├── stores/ # Pinia stores (Options API syntax)
│ ├── layouts/ # Layout wrappers
│ ├── middleware/ # Route guards
│ ├── plugins/ # Nuxt plugins
│ ├── utils/ # Auto-imported utilities
│ ├── constants/ # Auto-imported constants
│ ├── types/ # TypeScript types (NOT auto-imported)
│ └── assets/scss/ # Global SCSS
├── server/ # Nitro server routes and API handlers
│ ├── api/ # Proxied API routes (hides secrets)
│ ├── routes/ # Non-API server routes (manifest, health)
│ └── utils/ # Server utilities (CSRF, rate-limit, S3 helpers)
├── public/ # Static files
├── scripts/ # Build/maintenance scripts
├── docs/ # Internal design docs and tickets
└── nuxt.config.ts # Framework configurationAuto-Import Configuration ​
Nuxt auto-imports from these directories — you do not need explicit import statements:
composables/**— all composablesutils/**— all utilitiesstores/**— all Pinia storesconstants/**— all constants
Not auto-imported: app/types/ — always import type { X } from '~/types/x' explicitly.
Data Fetching Pattern ​
All server communication uses a strict two-layer architecture built on TanStack Vue Query 5.
Layer 1 — API Composables (composables/api/) ​
Raw fetch functions that wrap $api (the configured $fetch instance). They handle request/response shape and nothing else — no caching, no reactivity.
ts
// composables/api/useAssetsApi.ts
export function useAssetsApi() {
const { $api } = useNuxtApp()
return {
listAssets: (params: AssetListParams) =>
$api<AssetListResponse>('digital-assets/list', { params }),
deleteAsset: (id: number) =>
$api(`digital-assets/${id}`, { method: 'DELETE' }),
}
}Layer 2 — Query Composables (composables/queries/) ​
TanStack Query wrappers that consume Layer 1 functions. They own caching, background refetch, invalidation, and optimistic updates.
ts
// composables/queries/useAssetsQueries.ts
export function useAssetsQueries(params: MaybeRef<AssetListParams>) {
const { listAssets } = useAssetsApi()
return useQuery({
queryKey: ['assets', params],
queryFn: () => listAssets(toValue(params)),
staleTime: useRuntimeConfig().public.queryStaleTime,
})
}Rules ​
- Components only call Layer 2 (query composables) — never
$apidirectly. - Mutations call
queryClient.invalidateQueriesafter success to keep caches in sync. queryStaleTimeis controlled by theNUXT_PUBLIC_QUERY_STALE_TIMEenv var (default300000ms / 5 min).
Key Commands ​
bash
pnpm dev # Dev server (port 5000)
pnpm build # Production build
pnpm preview # Preview production build
pnpm lint # ESLint
pnpm typecheck # Nuxt typecheckCoding Conventions ​
<script setup lang="ts">exclusively — no Options API, nodefineComponentdefineProps<T>()anddefineEmits<T>()— always typed generics- No
anywithout aneslint-disablecomment and justification interfacefor object shapes;typefor unions/aliases- Composable naming:
useFeatureName.ts; API composables:useFeatureNameApi.ts - Composables always return named properties, never positional arrays
- Pinia: Options Store syntax (
defineStore('name', { state, getters, actions })) - Actions are the only state mutators — never
store.prop =from components pnpmonly — do not usenpmoryarn- No
console.log(esbuild strips them in production anyway) - No code comments — self-documenting names and small functions; explanations go in commit messages and docs
- No AI co-author attribution in commits