Frontend

Plugins

The $api plugin, auth bootstrap, socket.io, and icon registration.

All shared plugins live in codi-layer/app/plugins/ and are auto-loaded by every consuming app via the layer.

$api — Universal HTTP Client

The most important plugin. Creates a typed $fetch instance pre-configured with:

  • Authorization: Bearer <token> header injection
  • x-tenant-slug header for multi-tenant requests
  • Automatic 401 → logout redirect
// codi-layer/app/plugins/api.ts
export default defineNuxtPlugin(() => {
  const api = $fetch.create({
    baseURL: '/',
    retry: 1,
    retryStatusCodes: [401],
    retryDelay: 500,

    onRequest({ options }) {
      const token  = useCookie<string | null>('accessToken').value
      options.headers = new Headers(options.headers)
      if (token) options.headers.set('Authorization', `Bearer ${token}`)

      // Multi-tenant: inject slug on every request
      const tenant = useTenant()
      if (tenant?.value?.slug) {
        options.headers.set('x-tenant-slug', tenant.value.slug)
      }
    },

    async onResponseError({ response }) {
      if (response.status === 401) {
        useCookie<string | null>('accessToken').value = null
        useState<any | null>('currentUser').value     = null
        await navigateTo('/login')
      }
    },
  })

  return { provide: { api } }   // → useNuxtApp().$api
})

Usage in a composable:

function getBookings() {
  return useNuxtApp().$api<{ bookings: IBooking[]; pages: number }>('/api/bookings', {
    method: 'GET',
    query: { page: 1, limit: 10 },
  })
}

Never call $fetch directly in app code — always use $api so headers are applied consistently.

auth.client.ts — Bootstrap User on Mount

Runs once when the app loads. If an access token cookie exists, it fetches the current user silently. Clears the token if the request fails (expired/invalid).

// codi-layer/app/plugins/auth.client.ts
export default defineNuxtPlugin(async () => {
  const { getCurrentUser } = useAuth()
  const token = useCookie<string | null>('accessToken')

  if (token.value) {
    try {
      await getCurrentUser()
    } catch {
      token.value = null    // clear invalid token silently
    }
  }
})

The .client.ts suffix ensures this plugin only runs in the browser, never during SSR.

socket.client.ts — Socket.IO

Provides a $socket object with typed room helpers.

// codi-layer/app/plugins/socket.client.ts
export default defineNuxtPlugin(() => {
  if (import.meta.server) return { provide: { socket: null } }

  const config = useRuntimeConfig()
  const { token } = useAuth()
  let socket: Socket | null = null

  const connect = () => {
    socket = io(config.public.socketUrl, {
      auth:          { token: token.value || undefined },
      transports:    ['polling', 'websocket'],
      autoConnect:   false,
      reconnection:  true,
    })
    return socket
  }

  return {
    provide: {
      socket: {
        connect,
        joinBookingRoom: (bookingId: string) => socket?.emit('join:booking', bookingId),
        joinAdminRoom:   ()                  => socket?.emit('join:admin'),
        joinUserRoom:    (userId: string)    => socket?.emit('join:user', userId),
        on:  (...args: Parameters<Socket['on']>)  => socket?.on(...args),
        off: (...args: Parameters<Socket['off']>) => socket?.off(...args),
      },
    },
  }
})

Usage in a composable or component:

const { $socket } = useNuxtApp()

onMounted(() => {
  $socket?.on('booking:updated', (data) => {
    // handle real-time update
  })
})

onUnmounted(() => {
  $socket?.off('booking:updated')
})

phosphor-icons.ts — Global Icons

Registers Phosphor icon components globally so they don't need to be imported per-component:

// codi-layer/app/plugins/phosphor-icons.ts
import {
  PhEye, PhCaretLeft, PhMagnifyingGlass, PhList,
  PhPencilSimple, PhTrash, PhPlus, PhX, PhCheck,
} from '@phosphor-icons/vue'

export default defineNuxtPlugin((nuxtApp) => {
  const icons: Record<string, Component> = {
    PhEye, PhCaretLeft, PhMagnifyingGlass, PhList,
    PhPencilSimple, PhTrash, PhPlus, PhX, PhCheck,
  }
  Object.entries(icons).forEach(([name, component]) => {
    nuxtApp.vueApp.component(name, component)
  })
})

Only icons that are registered in this plugin can be used without an import statement. Add new icons here when needed — don't import them locally in individual components.