Using Quasar CLI Build Targets to Ship SPA, PWA, SSR, and Mobile from One Codebase
Learn how Quasar CLI’s build targets let you maintain one Vue.js codebase for SPA, PWA, SSR, and mobile apps, with a concrete example, trade‑offs, and verification steps.
28 Jul 2026, 09:23 UTC

Problem: Maintaining separate codebases for web and mobile is costly
When a product needs to run as a traditional single‑page app, a progressive web app, a server‑side rendered site, and a native mobile wrapper, teams often end up with four repositories. Keeping business logic in sync across those repos leads to duplicated effort, divergent bugs, and slower releases.
Thesis: Quasar’s CLI‑driven build targets let you keep a single Vue.js codebase and switch the output format with a simple flag
The Quasar Framework provides a unified project structure where the same source files are compiled to different distribution folders (dist/spa, dist/pwa, dist/ssr, dist/capacitor or dist/cordova) by changing the -m mode flag on the quasar build command. Platform‑specific concerns are isolated through the Quasar Plugin system and the Quasar.platform utility object, which exposes boolean flags like Quasar.platform.is.pwa or Quasar.platform.is.capacitor at runtime.
How the build target works
After creating a project with quasar create my-app, the CLI scaffolds a src/ folder that contains:
- Vue components and composables that are framework‑agnostic.
- A
src/plugins/directory where you can register platform‑aware plugins (e.g., a plugin that only initializes a Cordova camera whenQuasar.platform.is.capacitoris true). - The
quasar.conf.jsfile, which defines build‑time options for each mode.
Running a build looks like this:
# Run from the project root; no special permissions needed.
# Ensure Node.js >=18 and the Quasar CLI are available (npm i -g @quasar/cli or npx).
quasar build -m spa # builds to dist/spa
quasar build -m pwa # adds a service‑worker and manifest, output to dist/pwa
quasar build -m ssr # creates a Node.js server entry, output to dist/ssr
quasar build -m capacitor # prepares a Capacitor project, output to dist/capacitor
The command does not rewrite your source code; it only changes the Webpack/Vite configuration and injects mode‑specific assets. This means the same src/components/ tree is reused, reducing the surface area for drift.
Worked example: Conditional rendering based on the target
Suppose you want to show a “Install app” banner only when running as a PWA, and hide it when the app is wrapped in Capacitor. You can use the Quasar.platform object inside a component’s setup function:
<template>
<div>
<h1>Welcome</h1>
<div v-if="showInstallBanner">
<p>Add to home screen for the best experience.</p>
<button @click="triggerInstall">Install</button>
</div>
</div>
</template>
<script setup>
import { Quasar } from 'quasar'
import { ref } from 'vue'
const showInstallBanner = ref(false)
// Detect PWA mode at runtime
if (Quasar.platform.is.pwa) {
showInstallBanner.value = true
}
async function triggerInstall() {
if (Quasar.platform.is.pwa) {
// The `beforeinstallprompt` event is stored by the PWA plugin
const prompt = Quasar.plugins.DeferredInstallPrompt
if (prompt.value) {
prompt.value.prompt()
}
}
}
</script>
When you run quasar build -m pwa, the banner appears; when you run quasar build -m capacitor, the condition evaluates to false and the banner is omitted—all without touching the component logic.
Trade‑offs and limitations
- SSR constraints: Server‑side rendering executes code in a Node environment where
windowanddocumentare undefined. Any direct DOM access must be guarded withif (process.client) { … }or wrapped in a client‑only plugin. Forgetting this leads to hydration mismatches. - Mobile native dependencies: Capacitor or Cordova builds require native SDKs (Android Studio, Xcode) and plugins that add steps to the CI pipeline. These dependencies are absent from a pure SPA build, so you must maintain separate configuration files (
capacitor.config.ts,config.xml) and test on emulators or devices. - Bundle bloat: If you import a large library that is only needed for mobile (e.g., a heavy map SDK) directly in a shared component, it will be bundled for web targets as well. The mitigation is to use dynamic imports or mode‑specific aliases:
// src/components/Map.vue
Checking the result is straightforward: after each build, inspect the generated dist/ folder.
- For SPA and PWA, verify that
index.htmlreferences a singleapp.jsbundle. - For PWA, ensure
service-worker.jsandmanifest.jsonare present. - For SSR, look for a
dist/ssr/folder containing apackage.jsonwith astartscript and aserver.jsentry point. - For Capacitor, confirm that
dist/capacitor/holds anandroid/andios/subfolder with the generated native projects.
If any of those artifacts are missing, the build likely failed due to a missing plugin or a mis‑configured quasar.conf.js.
Actionable closing
Adopting Quasar’s CLI‑driven build targets lets you ship multiple product variants from a single source tree, cutting down on duplicate work and simplifying version control. To make the approach sustainable:
- Encapsulate all platform‑specific APIs inside plugins or composables that check
Quasar.platform.is.*. - Use lazy or conditional imports for heavy libraries that are only needed on certain targets.
- SSR‑guard any browser‑only code with
process.clientor thessrContexthelper provided by Quasar. - Automate verification in CI: run
quasar build -m <target>for each mode and assert that the expected files appear in the correspondingdist/folder.
By following these practices, you retain the benefits of a unified codebase while respecting the constraints each target imposes.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.