Research: Onboarding Subdomain — Deeper Investigation
Scope: Issue #210 — extract
onboard.neemias.appfrom the main SPA. Date: 2026-07-13 Sources: Vite docs (vitejs.dev), pnpm docs (pnpm.io), Cloudflare Pages docs (developers.cloudflare.com), Turborepo docs, GitHub real-world examples, Stack Overflow, community discussions.
Primary Source Findings
1. Vite CLI: --config Is First-Class
Source: Vite CLI docs and Vite CLI source
Vite --config flag works for both dev server and build:
# Dev
vite --config vite.onboarding.config.ts
# Build
vite build --config vite.onboarding.config.ts
# Preview
vite preview --config vite.onboarding.config.ts"Users can explicitly specify a configuration file using the
--configCLI option, which resolves the path relative to the current working directory."
Key insight: --config is the intended mechanism for multiple builds. It's not a hack.
Vite config is a regular JS/TS module — you can import from a shared config:
// vite.shared.ts
import { defineConfig } from "vite";
export const sharedConfig = defineConfig({
plugins: [react()],
resolve: {
alias: { "@": "/src" },
},
});// vite.onboarding.config.ts
import { defineConfig } from "vite";
import { sharedConfig } from "./vite.shared";
export default defineConfig({
...sharedConfig,
build: {
rollupOptions: {
input: "onboarding-index.html",
},
outDir: "dist-onboarding",
},
});This pattern is validated by the Vite plugin config hook and used in real projects (Vite Ruby, Symfony Vite).
2. Why rollupOptions.input Must Be a Separate HTML File
Source: Vite build source — input resolution
Vite resolves the entry point from rollupOptions.input (or falls back to index.html). The entry must be an HTML file — Vite's HTML plugin reads it, discovers <script> tags, and uses those as the JS entry points.
Not a library mode case — build.lib mode produces format outputs (ESM/CJS/UMD), not a deployable SPA. The onboarding is a full SPA, so it needs its own HTML entry, not library mode.
3. emptyOutDir Gotcha
Source: Stack Overflow, Vite build docs
When chaining vite build --config A && vite build --config B, the second build clears the output directory of the first by default if they share the same output parent.
Fix: Either:
- Use different
outDir(e.g.distvsdist-onboarding) - Or set
build.emptyOutDir: falseon one config
// vite.onboarding.config.ts
export default defineConfig({
build: {
outDir: "dist-onboarding", // Separate directory — no conflict
emptyOutDir: true, // Safe: only empties dist-onboarding/
},
});4. Cloudflare Pages Monorepo — Exact Configuration
Source: Cloudflare Pages Build Configuration and Monorepos
Cloudflare Pages supports monorepos with per-project settings:
| Setting | Main SPA | Onboarding |
|---|---|---|
| Project name | neemias-app | neemias-onboarding |
| Root directory | app/ | app/ |
| Build command | pnpm build:app | pnpm build:onboarding |
| Build output | dist | dist-onboarding |
| Domain | app.neemias.app | onboard.neemias.app |
"You have the option to vary the build command and/or root directory of your project to tell Pages where you would like your build command to run."
Both projects point to the same root directory (app/), because:
- They share
package.jsonandnode_modules - The build commands differ (different Vite configs)
- The output directories differ (different
outDir)
Build watch paths (avoid unnecessary rebuilds):
# Main SPA — only rebuild when app/ files change (excluding onboarding)
include: ["app/src/app/", "app/src/modules/", "app/index.html", "app/vite.config.ts"]
# Onboarding — only rebuild when onboarding files change
include: ["app/src/modules/onboarding/", "app/onboarding-index.html", "app/vite.onboarding.config.ts"]Limit: Cloudflare Pages supports up to 5 projects per repository.
5. Real-World Projects Using Two Vite Configs
a) Symfony Vite Bundle (Pentatrion)
Source: symfony-vite.pentatrion.com
Has an official guide for "multiple configurations": vite.config1.config.js + vite.config2.config.js, building to separate outDir with separate base paths. They run dev servers concurrently via concurrently:
"scripts": {
"dev": "concurrently \"vite -c vite.config1.config.js\" \"vite -c vite.config2.config.js\"",
"build": "vite build -c vite.config1.config.js && vite build -c vite.config2.config.js"
}b) Vite Ruby (ElMassimo/vite_ruby)
Source: GitHub Discussion #496
Production app using 6 different builds for admin, main, client-portal, etc. Approach endorsed by maintainer:
"Should be easy to achieve if you use a custom binstub that can set the
--configflag for Vite accordingly."
c) Stack Overflow consensus
Source: Stack Overflow
Accepted answer:
"build": "tsc && vite build --config vite.config.lib.dev.ts && vite build --config vite.config.lib.prod.ts"Multiple configs chained with && is the standard pattern.
d) Single-SPA ecosystem
Source: single-spa.js.org
Micro-frontend orchestration uses independent Vite builds per micro-app, each with its own config. This validates the pattern of multiple Vite builds from a single repository.
6. pnpm Workspace — No Changes Needed
Source: pnpm workspace YAML
The current pnpm-workspace.yaml lists "app" explicitly. The two-config approach doesn't need to add a new workspace entry because both builds share the same package.json.
packages:
- "app"
- "packages/*"
- "workers"
- "docs"This stays exactly the same.
7. Turborepo — Canonical apps/* Structure
Source: Turborepo Handbook
Turborepo's standard: applications in apps/, libraries in packages/. If this repo used Turborepo, the canonical layout would be:
apps/
app/ # main SPA
onboarding/ # separate Vite project
packages/
schemas/
permissions/But Turborepo isn't in use, so this isn't a constraint. The two-config approach is valid as a middle ground before committing to full app separation.
8. Custom Domain Setup on Cloudflare Pages
Source: Cloudflare Pages Custom Domains and Workers Custom Domains
Steps to set up onboard.neemias.app:
- Go to Workers & Pages in Cloudflare Dashboard
- Select the
neemias-onboardingproject - Go to Settings > Custom domains > Add custom domain
- Enter
onboard.neemias.app - Cloudflare auto-creates the DNS CNAME record
No WAF/rate-limit config needed at the Pages level — the API already has rate limiting on api.neemias.app. Optional: add WAF rule for onboard.neemias.app with rate limit of 30 req/min per IP.
Concrete Implementation Plan
File changes
app/
├── index.html (unchanged)
├── onboarding-index.html NEW — entry for onboarding build
├── vite.config.ts (unchanged)
├── vite.onboarding.config.ts NEW
├── vite.shared.ts NEW — shared config (optional)
└── src/
├── main.tsx (unchanged — main SPA entry)
└── onboarding/
├── main.tsx NEW — onboarding SPA entry
├── OnboardingPage.tsx MOVE from modules/onboarding/
└── components/ MOVE from modules/onboarding/components/onboarding-index.html
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Cadastro — Neemias</title>
<link rel="icon" href="/favicon.ico" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/onboarding/main.tsx"></script>
</body>
</html>vite.onboarding.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
root: ".",
build: {
rollupOptions: {
input: "onboarding-index.html",
},
outDir: "dist-onboarding",
},
});src/onboarding/main.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter, Route, Routes } from "react-router";
import OnboardingPage from "./OnboardingPage";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<BrowserRouter>
<Routes>
<Route path="/:hash" element={<OnboardingPage />} />
</Routes>
</BrowserRouter>
</StrictMode>
);package.json scripts
{
"scripts": {
"dev": "vite",
"dev:onboarding": "vite --config vite.onboarding.config.ts",
"build": "vite build",
"build:onboarding": "vite build --config vite.onboarding.config.ts",
"preview:onboarding": "vite preview --config vite.onboarding.config.ts"
}
}Cloudflare Pages Project 2 Configuration
| Setting | Value |
|---|---|
| Project name | neemias-onboarding |
| Root directory | app/ |
| Build command | pnpm build:onboarding |
| Build output directory | dist-onboarding |
| Production branch | main |
| Custom domain | onboard.neemias.app |
| Build watch paths (include) | app/src/onboarding/**, app/onboarding-index.html, app/vite.onboarding.config.ts, packages/schemas/** |
Risk Assessment Matrix
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Shared react-router v7 upgrade breaks both SPAs | Low | Medium | Keep onboarding-index.html minimal dependencies |
| CSS/Tailwind class conflicts | Low | Low | Each build tree-shakes independently |
import.meta.env.VITE_BACKEND_URL differences | Low | Low | Both SPAs hit same API; no cross-env concerns |
| Onboarding needs its own dependencies later | Medium | Low | Migrate to apps/onboarding/ at that point |
| Cloudflare Pages max project limit (5) | Low | High | Only 2 projects now; 3 spare slots |
Conclusion
The research confirms the Two-Config approach is:
- Documented — Vite docs explicitly support
--configfor multiple builds - Validated by real projects — Symfony Vite, Vite Ruby, Single-SPA, and Stack Overflow patterns
- Fully supported by Cloudflare Pages — root directory + build command per project
- No pnpm changes needed — same
package.json, same workspace entry - Low migration cost — if needed later, move to
apps/onboarding/by addingpackage.json+ updatingpnpm-workspace.yaml
The separate-project approach (apps/onboarding/) is the correct long-term architecture and matches Turborepo/pnpm conventions. But it adds complexity (duplicate package.json, tsconfig.json, Tailwind config, CI overhead) without benefit at Neemias's current scale.
Sources
- Vite Configuring
- Vite Build Options
- Vite Build Source (input resolution)
- Vite CLI Source
- Vite Multi-Page App
- pnpm Workspaces
- pnpm pnpm-workspace.yaml
- Cloudflare Pages Build Configuration
- Cloudflare Pages Monorepos
- Cloudflare Pages Custom Domains
- Cloudflare Pages Build Watch Paths
- Turborepo Structuring a Repository
- Symfony Vite Multiple Configurations
- Vite Ruby discussion #496
- Stack Overflow: Multiple builds with Vite
- Single-SPA Vite ecosystem