Frontend

Overview

Architecture and stack for all Booki frontend applications.

All Booki frontend applications are Single-Page Applications (SPAs) built with Nuxt, deployed to Cloudflare Pages.

Apps at a Glance

AppNuxt presetAuthTenancy
customer-booki-web-appcloudflare-pagesCookie JWTSubdomain slug
owner-booki-web-appcloudflare-pagesCookie JWTOrg from user profile
admin-booki-web-appcloudflare-pagesCookie JWT + admin roleNone
cms-booki-web-appcloudflare-pagesNone (public)None

Key Settings

All apps share these nuxt.config.ts defaults:

export default defineNuxtConfig({
  ssr: false,                         // Full SPA — no server-side rendering
  compatibilityDate: '2025-05-15',

  nitro: {
    preset: 'cloudflare-pages',       // Deploys to Cloudflare Pages
    devProxy: { host: '0.0.0.0' },
  },
})

API Proxying

All apps proxy API calls through Nuxt route rules — the browser never knows the real API URL:

// nuxt.config.ts
const apiCore = process.env.API_CORE   // e.g. https://api.booki.app

export default defineNuxtConfig({
  routeRules: {
    '/api/auth/**':          { proxy: `${apiCore}/api/auth/**` },
    '/api/bookings/**':      { proxy: `${apiCore}/api/bookings/**` },
    '/api/organizations/**': { proxy: `${apiCore}/api/organizations/**` },
    '/api/services/**':      { proxy: `${apiCore}/api/services/**` },
    '/api/users/**':         { proxy: `${apiCore}/api/users/**` },
  },
})

This means all $api('/api/bookings', ...) calls go through the Nitro edge server, which forwards them to the real API with no CORS issues.

CSS

All apps use Tailwind CSS v4 via Vite plugin + DaisyUI for component classes:

// nuxt.config.ts
import tailwindcss from '@tailwindcss/vite'

export default defineNuxtConfig({
  vite: { plugins: [tailwindcss()] },
})
/* app/assets/css/main.css */
@import 'tailwindcss';
@plugin 'daisyui';

Fonts

Fonts are loaded via @nuxtjs/google-fonts:

modules: [
  ['@nuxtjs/google-fonts', {
    families: { Geist: true, Inter: [400, 700] }
  }]
]

app.vue Structure

All apps follow the same root structure. Everything is wrapped in <ClientOnly> because SSR is disabled:

<!-- app/app.vue -->
<template>
  <div>
    <ClientOnly>
      <NuxtLoadingIndicator />
      <NuxtLayout>
        <NuxtPage />
      </NuxtLayout>
    </ClientOnly>
  </div>
</template>

The customer-booki-web-app adds a tenant-not-found guard at root level:

<template>
  <div>
    <ClientOnly>
      <NuxtLoadingIndicator />
      <WebsiteNotFound
        v-if="tenantNotFound"
        message="This organization's website could not be found."
      />
      <NuxtLayout v-else>
        <NuxtPage />
      </NuxtLayout>
    </ClientOnly>
  </div>
</template>

<script setup lang="ts">
const tenantNotFound = useState<boolean>('tenantNotFound', () => false)
</script>