Frappe as a headless backend
The starting insight is that Frappe is already a capable API server. Every doctype is exposed at /api/resource, and any whitelisted Python function is exposed at /api/method, all behind Frappe's authentication and permission engine. That means you can treat Frappe as a headless backend and build any frontend you like against it.
This matters because the built-in desk UI, excellent as it is for internal administration, is unmistakably an ERP interface. When a client needs a branded portal, a streamlined data-capture tool, or a customer-facing app, the desk is the wrong skin. A Vue 3 single-page application lets you own the entire user experience while Frappe keeps doing what it does best: data modelling, permissions, workflows, and business logic.
The division of labour is clean. Vue owns presentation and interaction; Frappe owns data and rules. You get a modern frontend without giving up the mature backend.
The REST API you already have
Frappe's REST API needs no extra code to start using. To read a list of records you call /api/resource/DoctypeName with filters, fields, and pagination as query parameters. To fetch one record you append its name to the path. Creating, updating, and deleting map to the standard POST, PUT, and DELETE verbs on those same paths.
Beyond raw CRUD, /api/method lets you call any whitelisted function, which is how you reach custom business logic that does not correspond to a single doctype. Every one of these calls is checked against the same role and row-level permissions that govern the desk, so your SPA cannot accidentally become a way around the security model. A user who cannot see a record in the desk cannot fetch it through the API either.
This is the quiet advantage of building on Frappe: the API is not a thing you build and secure, it is a thing you consume and it is already secured.
frappe-ui and the resource layer
You can talk to the API with plain fetch, but the Frappe team maintains frappe-ui, a Vue 3 library that makes it far more pleasant. It is two things at once: a Tailwind-based component set, and a reactive data layer purpose-built for Frappe.
The data layer is the part that saves real time. Its resource primitives wrap common operations, a document resource for a single record, a list resource for filtered collections, and a call resource for whitelisted methods, and expose them as reactive objects with loading and error state built in. Instead of writing fetch calls, wiring loading flags, and handling errors by hand in every component, you declare a resource and bind to it.
The result is Vue components that read like they are working with local reactive data while the library handles the HTTP, the credentials, and the state underneath. For a team building on Frappe, adopting frappe-ui removes most of the boilerplate that a hand-rolled API client would accumulate.
- Document resources bind a single doctype record to reactive component state.
- List resources handle filtered, paginated collections with loading and error flags.
- Call resources invoke whitelisted methods for custom logic.
- Tailwind-based components give a consistent, modern look without a separate design system.
Authentication without token gymnastics
Authentication is where many API-plus-SPA projects overcomplicate things, but Frappe makes it easy when the app is served same-origin. After a user logs in through Frappe, the browser holds a session cookie, and any API request from your SPA that includes credentials is authenticated automatically. There is no token to store, refresh, or leak, because you are reusing the framework's own session.
This is a strong reason to serve the SPA from the Frappe site itself rather than a separate domain. Same origin means the cookie flows naturally and you sidestep an entire category of CORS and token-management work.
When you genuinely need cross-origin or non-browser access, such as a mobile client, Frappe supports API key and secret pairs sent in an Authorization header. That path is available when required, but for a web SPA that lives alongside its backend, session-cookie auth is simpler, safer, and less code.
Routing, deep links, and the refresh trap
A single-page app uses client-side routing, and Vue Router in history mode gives clean URLs without a hash. The trap is what happens on a hard refresh or a shared deep link: the browser requests that path from the server, and Frappe, which does not know your client-side routes, returns a 404.
The fix is to configure a catch-all so that any path under your app serves the SPA's index HTML, after which Vue Router takes over on the client and renders the right view. In Frappe this is done through a website route or redirect rule that maps your app's sub-paths back to the bundle's entry point.
Set this up before you start testing shareable links, because it is invisible during normal in-app navigation and only surfaces when someone refreshes or pastes a URL. It is a five-minute configuration that saves a confusing afternoon of phantom 404s.
Building and deploying as a custom app
The cleanest way to ship a Vue 3 SPA on Frappe is to bundle it inside a Frappe custom app so the backend and frontend deploy as a single unit. Your custom app holds both the Python, the doctypes, whitelisted methods, and hooks, and the frontend source, with a build step that compiles the Vue app and places the output where Frappe can serve it.
This keeps everything version-controlled together and deployed together. A single bench update pulls new backend and frontend code at once, so you never have a mismatched pair where the UI expects an endpoint the server does not yet have. The frappe-ui project ships a starter setup that wires this build and serve path, which is a sensible place to begin rather than assembling the tooling from nothing.
One deployable unit is the whole point. The SPA is not a separate service to host and coordinate; it is part of the Frappe app, sharing its origin, its auth, and its release cycle.
When this architecture is the right choice
A Vue 3 SPA on Frappe is the right call when you need a bespoke or customer-facing interface but want to keep a mature backend for data, permissions, and logic. Branded portals, focused operational tools, public-facing apps, and anything where the generic desk UI would undersell the product all benefit from this split.
It is not always necessary. For internal admin, back-office data entry, and configuration, the Frappe desk is faster to deliver and perfectly professional, and building a custom SPA there is effort spent re-creating what you already have. Reach for Vue when the interface itself is part of the value, not when you simply want the app to feel modern for its own sake.
At Upeosoft we combine both deliberately: the desk for internal operations, and Vue 3 SPAs for the experiences that clients and their customers actually touch. Using Frappe as the shared backend for both means one data model, one permission system, and one deployment, with a modern frontend exactly where it earns its keep.
