Breaking changes and upgrade notes for downstream projects.
Breaking change (URL-level). The admin section now exposes its four
built-in sections (Users, Organizations, Readiness, Activity) as routed
siblings of the downstream config.admin.tabs extras. There is no more
nested "General" tab with an internal v-window — every tab is a real
URL and renders through the same <router-view>.
- REMOVED:
src/modules/admin/views/admin.content.vue— the inner nested tab bar is gone. Its four window items are now dedicated views. - NEW:
src/modules/admin/views/admin.users.view.vuesrc/modules/admin/views/admin.organizations.view.vuesrc/modules/admin/views/admin.readiness.view.vuesrc/modules/admin/views/admin.activity.view.vue
- CHANGED:
src/modules/admin/router/admin.router.js— the parent/adminroute now has dedicated children:''→ redirect to{ name: 'Admin Users' }users→Admin Usersusers/:id→Admin Userorganizations→Admin Organizationsorganizations/:organizationId→Admin Organizationreadiness→Admin Readinessactivity→Admin Activity
- CHANGED:
src/modules/admin/views/admin.layout.vue— the tab bar now renders built-in tabs +config.admin.tabsextras in one flat row. The global error banner and the mailer warning were moved from the oldadmin.content.vueinto the layout so they stay visible across every admin tab (including downstream extras). - Readiness and Activity now fetch their data on
mounted()(previously via awatch: { tab }inside the old nested window).
No config change is required. The mechanism for contributing extra
admin tabs via config.admin.tabs + injectAdminChildren is unchanged,
and extras continue to render inline inside the admin layout.
- Run
/update-stackto pull the changes. - Verify hard-coded links in your project. The old URL
/adminused to land on the nested "General" tab; it now redirects to/admin/users(client-side via vue-router), so existing links keep working. If you want to point somewhere else, use one of the new stable URLs:/admin/users/admin/organizations/admin/readiness/admin/activityOptional check:grep -r "/admin" src/.
- Downstream projects that override
admin.content.vuemust delete the override — the file no longer exists. Replace any such override with a dedicated override ofadmin.users.view.vue/admin.organizations.view.vue/admin.readiness.view.vue/admin.activity.view.vue, or attach a custom tab viaconfig.admin.tabs+injectAdminChildren. - No route-name breakage for downstream extras —
injectAdminChildrenstill mounts your routes as children of the same/adminparent.
Two stacked tab bars on */admin (top-level General + extras, nested
Users / Organizations / Readiness / Activity) was visually noisy and
duplicated navigation. The original intent of config.admin.tabs was
for downstream extras to live alongside the built-ins — not above
them. Flattening gives every admin section a real URL, preserves
deep-linking and browser back/forward, and keeps downstream extras
config-driven with zero migration work.
Breaking change. Downstream projects that added admin tabs via
config.admin.tabs + sibling routes under /admin/* must migrate to the
new nested-route layout. Extra tabs now render inline inside the admin
layout (no full-page navigation, no PageHeader duplication).
- NEW:
src/modules/admin/views/admin.layout.vue— parent layout that rendersPageHeader+ the tab bar (General + config-driven extra tabs)<router-view>.
- NEW:
src/modules/admin/views/admin.content.vue— renamed fromadmin.view.vue. Contains only the built-in tabs (Users, Organizations, Readiness, Activity).PageHeaderand extra-tabs logic moved to the layout. - CHANGED:
src/modules/admin/router/admin.router.js— now exports a single parent route/adminwithchildren(''→AdminContent,users/:id,organizations/:organizationId). - NEW:
src/lib/helpers/router.js— exportsinjectAdminChildren. - UPDATED:
src/modules/app/app.router.js— callsinjectAdminChildrenbefore mounting the router, so downstream "admin tab" modules attach as children of the admin parent route.
-
Router file — change
pathfrom'/admin/xxx'to'xxx'(relative), and remove the leading slash:// Before export default [ { path: '/admin/knowledge', name: 'Admin Knowledge', component: adminKnowledge, meta: { display: false, action: 'manage', subject: 'UserAdmin' }, }, ]; // After export default [ { path: 'knowledge', name: 'Admin Knowledge', component: adminKnowledge, meta: { display: false, action: 'manage', subject: 'UserAdmin' }, }, ];
-
app.router.js— register the module viainjectAdminChildreninstead of adding it tooptionalModules:import { injectAdminChildren } from '@/lib/helpers/router'; import { isModuleActive } from '@/lib/helpers/modules'; import admin from '../admin/router/admin.router'; import knowledge from '../knowledge/router/knowledge.router'; import costs from '../costs/router/costs.router'; const adminChildModules = [ { name: 'knowledge', routes: knowledge }, { name: 'costs', routes: costs }, ]; injectAdminChildren(admin, adminChildModules, isModuleActive);
-
Config — update
config.admin.tabs[].routeto a relative path:// Before admin: { tabs: [ { value: 'knowledge', label: 'Knowledge', icon: 'fa-solid fa-book', route: '/admin/knowledge' }, ], } // After admin: { tabs: [ { value: 'knowledge', label: 'Knowledge', icon: 'fa-solid fa-book', route: 'knowledge' }, ], }
Legacy absolute routes nested under
/admin/(e.g.'/admin/knowledge') still work during the transition and log a dev-mode warning to nudge you toward relative paths. Absolute routes outside/admin/are filtered out (also with a dev-mode warning). Migrate to relative paths when you can. -
View component — remove
PageHeaderfrom the tab's view component: the admin layout now provides it. Keep thev-container+ inner content, drop<PageHeader icon="..." title="..." />. -
CASL guards — keep
meta.action/meta.subjecton each injected child route. The router guard walksto.matched, so parent + children each enforce their own CASL meta.
Previously, extra tabs used :to="route" to sibling /admin/xxx pages,
unmounting the admin view on click and showing a separate page with its
own header. Nesting them under the admin parent route means:
- The admin layout (tab bar + header) stays mounted across tab switches
- URL updates are deep-linkable (
/admin/knowledgerenders in-layout) - Browser back/forward works between tabs
- Downstream modules register WITHOUT modifying the admin module itself
src/modules/home/components/home.about.component.vue now renders item images through the shared homeImgComponent instead of raw <v-img>. This aligns the About section with home.features.component.vue and enables inline SVG rendering (theme-aware via CSS custom properties).
- About section images (both
imgBackground-wrapped and bare) now render through<homeImgComponent :img="item.img" :img-mode="item.imgMode || 'contain'" /> - SVGs are inlined via
v-htmlinstead of loaded as<img>tags — they can now access Vuetify CSS custom properties and respond to theme toggling imgModefrom config is respected (containfor background-wrapped images, defaults tocontainfor bare images too)- No config schema change — existing
item.imgand optionalitem.imgModekeep working as before
- Run
/update-stackto pull the changes - If you use custom SVGs in
aboutoraboutFeaturessections, verify they render correctly with theme switching (they now follow Vuetify theme, not OSprefers-color-scheme) - Downstream projects that override
home.about.component.vueshould review their override to keep image rendering consistent — either re-pull the file via/update-stackor manually swapv-imgforhomeImgComponent(using:img="item.img"and:img-mode="item.imgMode") in the override - No config changes needed — existing
home.aboutconfig is fully compatible
New reusable OrgAvatarComponent for displaying organization avatars with colored initials and a tooltip. Mirrors the pattern of UserAvatarComponent.
- New component:
src/modules/core/components/org.avatar.component.vue - Props:
org(Object, required) +size(Number, default 32) - Renders a colored
v-avatarwith the first letter of the organization name and a tooltip showing the full name - Color is deterministic, derived from the organization name via
orgColorhelper
-
Run
/update-stackto pull the changes -
Replace any custom org avatar rendering with the new component:
<orgAvatarComponent :org="currentOrganization" :size="40" />
-
No breaking change — this is a new component, existing code is unaffected
Replaced Gravatar-based avatar rendering with a reusable UserAvatarComponent that shows an uploaded image or colored initials fallback. The Gravatar plugin has been removed entirely.
UserAvatarComponentprops reduced touser(Object, optional) +size(Number, default 32)- Removed props:
width,height,radius,border,color,disabled - Removed:
src/lib/plugins/gravatar.jsand its unit tests - Removed:
app.use(plugins.gravatar)frommain.js
-
Run
/update-stackto pull the changes -
Replace old avatar usage:
<!-- Before --> <userAvatarComponent :user="user" :width="'60px'" :height="'60px'" :radius="'50%'" :size="128" /> <!-- After --> <userAvatarComponent :user="user" :size="60" />
-
Remove any
disabledprop usage — the component no longer wraps in an<a>tag -
If you imported or referenced the gravatar plugin directly, remove those imports
Standardized the {module}.{project}.config.js pattern for per-module project overrides in downstream projects. The config loader already supported this — this note documents the standard and adds the template.
src/config/defaults/myproject.config.js— template for global project overrides (already existed, now documented)- Per-module override files (
src/modules/{name}/config/{module}.{project}.config.js) are discovered automatically bygenerateConfig.jswhenNODE_ENVis set to the project name
- Module defaults:
src/modules/*/config/*.development.config.js - Global development defaults:
src/config/defaults/development.config.js - Per-module project overrides:
src/modules/*/config/*.{project}.config.js - Global project overrides:
src/config/defaults/{project}.config.js - Build-time env vars:
DEVKIT_VUE_*
No breaking change — this is a documentation and template addition only.
To adopt the pattern:
- Name your per-module override files
{module}.{project}.config.jsinsidesrc/modules/{module}/config/ - Set
NODE_ENV={project}when runningnpm run devor building - Use per-module files for module-specific overrides (theme, sign, home sections); use the global file for app/api/cookie/header/footer
billing.config.js has been renamed to billing.development.config.js to align with the {module}.development.config.js stack convention.
If your downstream project imports or overrides billing.config.js, rename it:
src/modules/billing/config/billing.config.js→src/modules/billing/config/billing.development.config.js- Update any
importstatements from'../config/billing.config'to'../config/billing.development.config'
Per-module activated: true/false config flag. When activated: false, the module's routes are not mounted and it's invisible to the app.
- New
isModuleActive(moduleName)helper insrc/lib/helpers/modules.js src/modules/app/app.router.jsnow conditionally includes routes based on activation status- Core modules (
home,auth,users,app,core) are always active regardless of flag - New
modulesconfig block indevelopment.config.jswithactivated: truedefaults for:tasks,billing,organizations,analytics,admin
- Run
/update-stackto pull the change - No breaking change — all modules default to
activated: true(backward compatible) - To deactivate a module, override in config:
// src/config/defaults/development.config.js modules: { tasks: { activated: false } }
- Sidenav items from deactivated modules are automatically hidden (routes not registered = no nav entries)
- If you have custom modules, add them to the
modulesconfig block withactivated: true
Vite 8 replaces Rollup with Rolldown. manualChunks as an object is no longer supported — must be a function.
Node.js requirement: Rolldown requires Node.js ^20.19.0 or >=22.12.0. Upgrading to Vite 8 will fail on older Node versions (including CI/build images) until they are updated.
Action for downstream: If your vite.config.js overrides manualChunks, convert the object to a function:
// Before (Vite 7)
manualChunks: { 'my-chunk': ['pkg-a', 'pkg-b'] }
// After (Vite 8)
manualChunks(id) {
if (['pkg-a', 'pkg-b'].some((p) => id.includes(`/${p}/`))) return 'my-chunk';
}Client-side analytics, user/org identification, page view tracking, and feature flags via PostHog.
In your global config (e.g. src/config/defaults/development.config.js), uncomment:
analytics: {
posthog: {
host: 'https://app.posthog.com',
key: 'ph_your_project_api_key',
},
}Or use env vars: DEVKIT_VUE_analytics_posthog_host, DEVKIT_VUE_analytics_posthog_key.
All features are no-op when key is empty — safe to deploy without PostHog.
| Feature | File | Notes |
|---|---|---|
| PostHog plugin | src/lib/plugins/posthog.js |
Auto-init, conditional on config |
| Analytics helpers | src/lib/helpers/analytics.js |
capture(), capturePageview(), isPosthogReady() |
usePostHog composable |
src/lib/composables/analytics.usePostHog.js |
Access PostHog instance |
useFeatureFlag composable |
src/lib/composables/analytics.useFeatureFlag.js |
Reactive feature flag ref |
| Page view tracking | src/modules/app/app.router.js |
capturePageview() in router.afterEach |
| User identify | src/modules/auth/stores/auth.store.js |
posthog.identify() on signin, posthog.reset() on signout |
| Org group | src/modules/organizations/stores/organizations.store.js |
posthog.group('company', ...) on org switch |
- Run
/update-stackto pull the changes - Set config:
analytics.posthog.host+analytics.posthog.key - No additional setup needed — identify/group/pageview are automatic
New config options in vuetify.theme.navigation:
vuetify: {
theme: {
navigation: {
glass: true, // liquid glass effect (false = opaque, backward compatible)
inset: true, // Apple-style floating with margin
}
}
}When glass: true, the sidenav uses liquidGlassStyle() instead of solid background. Content on / (home) becomes fullwidth; other routes keep the rail padding.
| Param | Old default | New default |
|---|---|---|
intensity |
0.8 |
1 |
opacity |
undefined |
0.5 |
variant |
'card' |
'pill' |
Action for downstream: Remove redundant params from existing calls. Add variant: 'card' explicitly for non-pill elements (e.g. presentation cards).
The glowBorder parameter and all related CSS/JS have been removed. Remove any glowBorder: true or glowBorder: 'animated' from existing calls.
Dark mode now includes a subtle outer shadow matching the existing light mode behavior.
Content v-row after PageHeader should use class="pa-2 mt-0" to reduce gap between header and cards.
Margins changed from mt-4 mb-2 to my-1.
app.vue: nav a color changed from rgba(var(--v-theme-onPrimary)) to inherit to support glass mode.
This guide walks downstream Vue projects through migrating from the legacy role-based routing (meta.roles / localStorage.UserRoles) to the new CASL ability system and the optional organizations module.
| Area | What changed |
|---|---|
| Route meta | meta.roles removed from all routes, replaced by meta.action + meta.subject |
| Dependencies | New: @casl/ability (v6), @casl/vue (v2) |
| Router guard | beforeEach in app.router.js rewritten to use ability.can() |
| Navigation | refreshNav() in core.store.js now filters by CASL abilities instead of roles |
| localStorage | UserRoles is still written (backward compat) but no longer used for routing |
| Auth store | Imports updateAbilities from src/lib/helpers/ability.js; calls it on signin, token refresh, and signout |
| New module | src/modules/organizations/ (store, router, views, components) |
| Signup flow | Optional organization step after registration when serverConfig.organizations.enabled === true |
- Update the backend first. The Node stack must return
abilitiesin its signin / token responses and expose the/organizationsAPI. See the Node migration guide. - Install the new dependencies:
npm install @casl/ability @casl/vuenpm install @casl/ability @casl/vueVerify package.json contains:
"@casl/ability": "^6.8.0",
"@casl/vue": "^2.2.6"Create src/lib/helpers/ability.js:
import { createMongoAbility } from '@casl/ability';
import { reactive } from 'vue';
export const ability = reactive(createMongoAbility([]));
export const updateAbilities = (rules) => {
ability.update(rules);
};
export default { ability, updateAbilities };Key points:
createMongoAbility([])starts with an empty rule set (deny-all by default).- Wrapping with
reactive()keeps Vue templates reactive when rules change. updateAbilities()replaces all rules atomically.
Add the following to src/main.js:
import { abilitiesPlugin } from '@casl/vue';
import { ability } from './lib/helpers/ability';
// In the app.use() chain:
app.use(abilitiesPlugin, ability);This makes can() and cannot() available in all component templates via @casl/vue.
In src/modules/auth/stores/auth.store.js:
- Import the helper:
import { updateAbilities } from '../../../lib/helpers/ability';- In the
signinaction, after setting user data:
if (res.data.abilities) updateAbilities(res.data.abilities);- In the
tokenaction, same pattern:
if (res.data.abilities) updateAbilities(res.data.abilities);- In the
signoutaction, clear abilities:
updateAbilities([]);The backend sends abilities as an array of CASL rule objects, e.g.:
[
{ "action": "read", "subject": "Task" },
{ "action": "manage", "subject": "User" }
]For every module router file, replace meta.roles with meta.action + meta.subject.
| Module | Route | Old meta |
New meta |
|---|---|---|---|
| home | / |
(none) | (none) -- public, no change |
| home | /changelogs, /team, /pages/:name |
(none) | (none) -- public, no change |
| auth | /signin, /signup, /forgot, /reset, /token |
display: false |
display: false -- public, no change |
| tasks | /tasks (list) |
roles: ['user'] |
(no action/subject) -- public list page |
| tasks | /task (create) |
roles: ['user'] |
action: 'read', subject: 'Task' |
| tasks | /tasks/:id (detail) |
roles: ['user'] |
action: 'read', subject: 'Task' |
| users | /users (list) |
roles: ['admin'] |
action: 'manage', subject: 'User' |
| users | /users/:id (detail) |
roles: ['admin'] |
action: 'manage', subject: 'User' |
| secure | /secure |
roles: ['user'] |
action: 'read', subject: 'Secure' |
| organizations | /organizations |
(new) | action: 'read', subject: 'Organization' |
| organizations | /organizations/create |
(new) | action: 'create', subject: 'Organization' |
| organizations | /organizations/:organizationId |
(new) | action: 'read', subject: 'Organization' |
Example -- tasks.router.js:
// Before
{
path: '/task',
name: 'task create',
component: task,
meta: {
display: false,
roles: ['user'],
},
},
// After
{
path: '/task',
name: 'task create',
component: task,
meta: {
display: false,
action: 'read', subject: 'Task',
},
},Replace the beforeEach guard in src/modules/app/app.router.js:
router.beforeEach(async (to) => {
// Page title
const pageTitle = to.meta.title || to.name;
document.title = pageTitle ? `${pageTitle} - ${config.app.title}` : config.app.title;
// Ability-based guard
if (to.matched.some((record) => record.meta.action)) {
const authStore = useAuthStore();
if (authStore.isLoggedIn) {
const { ability } = await import('../../lib/helpers/ability.js');
if (ability && ability.rules && ability.rules.length > 0) {
if (ability.can(to.meta.action, to.meta.subject)) return true;
return '/'; // forbidden
}
// Fallback: no abilities loaded yet (backend not updated) -- allow if logged in
return true;
}
return '/signin';
}
});The fallback to isLoggedIn provides cross-stack safety: if the backend has not been updated yet and does not send abilities, authenticated users can still navigate.
Replace refreshNav() in src/modules/core/stores/core.store.js:
import { ability } from '../../../lib/helpers/ability';
// Inside the store actions:
refreshNav(isLoggedIn) {
const hasAbilities = ability && ability.rules && ability.rules.length > 0;
const nav = orderBy(
pickBy(this.routes, (i) => {
if (i.meta.display !== false) {
if (!('action' in i.meta)) return i; // no guard, always show
if (isLoggedIn) {
if (hasAbilities) {
if (ability.can(i.meta.action, i.meta.subject)) return i;
} else {
return i; // fallback: show if logged in
}
}
}
return null;
}),
['meta.action'],
['desc'],
);
this.nav = nav;
},Update templates that previously relied on isLoggedIn or role checks for conditional rendering.
<!-- Before -->
<v-btn v-if="isLoggedIn">Delete</v-btn>
<!-- After (using @casl/vue helpers in <script setup> or Options API) -->
<v-btn v-if="can('delete', subject('Task', task))">Delete</v-btn>In Options API components, can() is available automatically through the @casl/vue plugin registered in main.js. You can use it directly in templates:
<template>
<v-btn v-if="$can('delete', 'Task')">Delete Task</v-btn>
<v-btn v-if="$can('manage', 'User')">Manage Users</v-btn>
</template>For instance-level checks (e.g. "can this user update this specific task?"), use subject():
<script>
import { subject } from '@casl/ability';
export default {
methods: {
canUpdate(task) {
return this.$can('update', subject('Task', task));
},
},
};
</script>Copy the entire src/modules/organizations/ directory from the stack. The module includes:
src/modules/organizations/
router/organizations.router.js # Routes: /organizations, /organizations/create, /organizations/:id
stores/organizations.store.js # Pinia store: CRUD + switchOrganization + members
views/organizations.view.vue # List view
views/organizations.create.view.vue # Create view
views/organization.view.vue # Detail/edit view (with delete confirmation)
components/organizations.switcher.component.vue # Org switcher dropdown (for nav bar)
components/organizations.members.component.vue # Members list + invite
components/organizations.invite.component.vue # Invite form
tests/organizations.store.spec.js # Store unit tests
Register the module router in app.router.js:
import organizations from '../organizations/router/organizations.router';
const routes = [].concat(home, auth, users, secure, tasks, organizations);Add the organization switcher to your navigation/header component:
<script>
import OrganizationsSwitcherComponent from '../../organizations/components/organizations.switcher.component.vue';
</script>
<template>
<OrganizationsSwitcherComponent />
</template>The switcher auto-hides when organizations are disabled or the user belongs to only one organization. When the user switches organizations, the store calls POST /organizations/:id/switch, receives a new JWT with updated abilities, and calls updateAbilities().
The signup view (src/modules/auth/local/views/signup.view.vue) now supports a multi-step flow:
- Form step -- standard email/password registration.
- Organization welcome -- shown when the backend auto-created or auto-joined an organization (the response contains
result.organization). - Organization setup -- shown when
result.organizationSetupRequired === true; renders theAuthOrganizationSetupComponentwhich lets the user name their new org.
Add the AuthOrganizationSetupComponent:
src/modules/auth/local/components/organizationSetup.component.vue
In the signup validate() method, handle the organization flow:
const result = await authStore.signup({ email, password, firstName, lastName });
if (this.serverConfig?.organizations?.enabled) {
if (result.organization) {
// auto-created or auto-joined
this.signupStep = 'organizationWelcome';
} else if (result.organizationSetupRequired) {
// manual creation needed
this.signupStep = 'organizationSetup';
} else {
this.$router.push(this.config.sign.route);
}
} else {
this.$router.push(this.config.sign.route);
}If your backend returns 403 when abilities are stale, you can add an interceptor in src/lib/services/axios.js to re-fetch abilities:
let isRefreshingAbilities = false;
instance.interceptors.response.use(
(response) => response,
async (err) => {
if (err?.response?.status === 403 && !isRefreshingAbilities) {
isRefreshingAbilities = true;
try {
const authStore = useAuthStore();
await authStore.token(); // re-fetches user + abilities
} finally {
isRefreshingAbilities = false;
}
}
throw err;
},
);The isRefreshingAbilities flag prevents infinite loops when the token endpoint itself returns 403.
When serverConfig.organizations.enabled === false (or the backend does not expose organizations at all):
| Behavior | Effect |
|---|---|
| Organization routes | Still registered but guarded by action: 'read', subject: 'Organization' -- the backend will not grant this ability, so routes are inaccessible |
| Org switcher | Auto-hidden (isVisible returns false when no orgs or feature disabled) |
| Signup | No organization step -- proceeds directly to the app |
| Abilities | Still function normally -- the backend sends ability rules scoped to the user without an org context |
| Navigation | Org link will not appear because ability.can('read', 'Organization') returns false |
No code changes are needed to disable organizations. It is entirely controlled by the backend configuration.
- All routes guarded by
ability.can()-- verify inapp.router.jsbeforeEach - Fallback works when backend does not send abilities (logged-in users can still navigate)
- Public routes (
/,/signin,/signup,/forgot,/changelogs, etc.) accessible without auth - Navigation updates on login/logout (
refreshNavcalled with correctisLoggedIn) - 403 triggers ability refresh (if interceptor added) without infinite loop
- Org switcher visible only when enabled and user has multiple orgs
- Org switcher calls
switchOrganizationand updates abilities - Signup org step shown only when
serverConfig.organizations.enabled === true -
AuthOrganizationSetupComponentemitscreatedand transitions to welcome step -
updateAbilities([])clears all permissions on signout - Admin-only routes (
/users) requiremanage+Userability
New header scroll mode: the navbar shrinks to a floating pill on scroll.
To activate, set in src/modules/app/config/app.development.config.js → vuetify.theme.header:
scrollBehavior: 'float',
colorMode: null,The development defaults now ship with 'float' enabled. To restore the previous behavior, set:
scrollBehavior: 'hide',
colorMode: 'light',
opacity: undefined,All config files now follow the module.env.kind.js naming convention consistently.
- Global defaults renamed:
config.{env}.js→{env}.config.js(e.g.development.config.js) - Module defaults renamed:
config.{module}.js→{module}.development.config.js(e.g.auth.development.config.js) - Generator updated:
scripts/generateConfig.jsmatches*.development.config.jsfor module defaults - Template renamed:
src/config/defaults/myproject.config.js
| File type | Pattern | Example |
|---|---|---|
| Global default | {env}.config.js |
development.config.js |
| Global override | {env}.config.js |
production.config.js |
| Module default | {module}.development.config.js |
app.development.config.js |
| Module env override | {module}.{env}.config.js |
app.test.config.js |
| Downstream project | {project}.config.js |
myproject.config.js |
Config belongs to the module that semantically owns the data, even if other modules read it. Global keeps only pure infrastructure (app metadata, api, port, cookie, analytics). This enables autonomous, pluggable modules.
| Key | Owner | Why |
|---|---|---|
vuetify, header, footer, pages |
app |
App module owns the UI framework and layout |
sign, oAuth |
auth |
Auth defines how users authenticate |
whitelists |
users |
Users defines its own field visibility |
organizations |
organizations |
Orgs defines its own settings |
| home sections | home |
Home defines its own page content |
app, port, api, cookie, analytics |
global | Pure infrastructure, no module owns them |
src/config/defaults/
development.config.js ← infra only (app, port, api, cookie, analytics)
production.config.js ← production overrides (standalone)
test.config.js ← test overrides (standalone)
myproject.config.js ← template for downstream projects
src/modules/app/config/
app.development.config.js ← vuetify, header, footer, pages
src/modules/auth/config/
auth.development.config.js ← sign, oAuth
src/modules/users/config/
users.development.config.js ← whitelists
src/modules/home/config/
home.development.config.js ← home sections (hero, features, gallery, etc.)
src/modules/organizations/config/
organizations.development.config.js ← organizations settings
- Module defaults —
src/modules/*/config/*.development.config.js - Global defaults —
src/config/defaults/development.config.js - Global env overrides —
src/config/defaults/${NODE_ENV}.config.js(if NODE_ENV ≠ development) DEVKIT_VUE_*environment variables
Create NODE_ENV=staging by adding:
src/config/defaults/staging.config.js(global overrides)
Files must be named {projectname}.config.js. A template is provided at src/config/defaults/myproject.config.js.
- Rename global config files:
config.{env}.js→{env}.config.js - Rename module config files:
config.{module}.js→{module}.development.config.js - Rename project config files:
config.{project}.js→{project}.config.js - Run
npm run devto verify the generatedsrc/config/index.jsis correct.
The monolithic src/config/defaults/development.js has been split into per-module config files.
See "Config file naming convention (2026-03-13)" above for the current naming standard.
Additional changes from this migration:
- Typo fixes:
sucessColor→successColor(app config),Ressources→Resources(home config) — a backward-compatible fallback is provided inaxios.js - Env var prefix:
WAOS_VUE_*→DEVKIT_VUE_*
- Run
npm run lint && npm run test:unit && npm run buildto confirm everything works.