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
| App | Nuxt preset | Auth | Tenancy |
|---|---|---|---|
customer-booki-web-app | cloudflare-pages | Cookie JWT | Subdomain slug |
owner-booki-web-app | cloudflare-pages | Cookie JWT | Org from user profile |
admin-booki-web-app | cloudflare-pages | Cookie JWT + admin role | None |
cms-booki-web-app | cloudflare-pages | None (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>