Upgrades

/app/* gated subtree routing

Move all gated routes under /app/* and add a public homepage at /. Replaces the per-route allowlist with a URL-prefix gate.

What changed

A minimal public homepage now ships at / (brand + Sign in / Create account buttons; "Go to app" CTA when authed).

All gated routes moved under /app/*:

Old URLNew URL
/dashboard/app
/settings/*/app/settings/*
/chat/*/app/chat/*
/activity/app/activity

The global middleware (app/middleware/auth.global.ts) now gates the entire /app/* subtree by URL prefix, instead of an allowlist of public paths.

Why

Policy lives in the URL and filesystem instead of a middleware allowlist. Adding a public page = drop a file at the top level. Adding a gated feature = drop a file under app/pages/app/. Structure tells the truth — there's no allowlist to forget when adding a public page, and no way to accidentally ship an authed feature without gating.

Matches the dominant pattern in modern app routing: Next.js (app) / (marketing) route groups, Stripe's /dashboard/*, Linear's /[workspace]/*.

Do I need to run this?

The simplest test:

Open app/pages/ in your project. If you see a dashboard/ folder, you're on the old structure and this upgrade applies to you. If you see an app/ folder instead (containing index.vue, chat/, settings/, etc.), you're already on the new structure — skip this upgrade.

You can also check with git log -1 --format="%ad" — if your last template sync is before 2026-04-25 (or v0.1.0), you need to upgrade.

Upgrade prompt

Paste the entire block below into Claude Code, running from your project's root directory:

Migrate this Vue Starter fork to the new `/app/*` gated subtree routing convention.

**Goal:** public surface (homepage, marketing, auth, invite) sits at the top level. All gated routes (auth + team required) live under `/app/*`. The global middleware gates the entire `/app/*` subtree by URL prefix instead of an allowlist. Adding a public page = drop a file at the top level. Adding a gated feature = drop a file under `app/pages/app/`.

Use `git mv` for every file move to preserve history. Execute steps in order without asking me to confirm — work autonomously and report what you did at the end.

### 0. Bail-out check

If `app/pages/app/` already exists with files in it, the upgrade has already been run — stop and tell me.

### 1. Discover structure

- List `app/pages/` and classify each entry:
  - **Template features** (always present in vanilla template): `dashboard/`, `chat.vue` + `chat/`, `settings.vue` + `settings/`, `activity/`
  - **Public / forced flow** (stay at top level): `auth/`, `invite/`, `onboarding.vue`, `no-team.vue`, `index.vue`
  - **Custom features** (everything else): record this list — you will move these in step 2 and rewrite their URL refs in step 5
- Run a discovery grep for every old path root used in the codebase:
  ```bash
  grep -rEn '/(dashboard|settings|chat|activity)(/|"|'\''|`)' app/ server/ shared/ layers/ 2>/dev/null --include='*.ts' --include='*.vue' --include='*.md'
  ```
  Plus the same grep for each custom feature you found.

### 2. Move pages under `app/pages/app/`

Template pages first:

```bash
mkdir -p app/pages/app/settings app/pages/app/chat app/pages/app/activity
git mv app/pages/dashboard/index.vue app/pages/app/index.vue
git mv app/pages/chat.vue app/pages/app/chat.vue
git mv app/pages/chat/index.vue app/pages/app/chat/index.vue
git mv 'app/pages/chat/[id].vue' 'app/pages/app/chat/[id].vue'
git mv app/pages/settings.vue app/pages/app/settings.vue
git mv app/pages/settings/*.vue app/pages/app/settings/
git mv app/pages/activity/index.vue app/pages/app/activity/index.vue
rmdir app/pages/dashboard app/pages/chat app/pages/settings app/pages/activity
```

Skip any source path that doesn't exist (your fork may have dropped or renamed something).

Then for each custom feature from step 1, mirror the move under `app/pages/app/`. Examples:
- `app/pages/projects/` → `app/pages/app/projects/`
- `app/pages/projects.vue` (parent route) → `app/pages/app/projects.vue`
- `app/pages/reports/[id].vue` → `app/pages/app/reports/[id].vue`

### 3. Replace `app/middleware/auth.global.ts` entirely with:

```ts
export default defineNuxtRouteMiddleware((to) => {
  const user = useSupabaseUser();

  const isAppRoute = to.path === "/app" || to.path.startsWith("/app/");
  const isAuthRoute = to.path.startsWith("/auth/");
  const isResetPassword = to.path === "/auth/reset-password";
  const isInviteRoute = to.path.startsWith("/invite/");
  const isOnboarding = to.path === "/onboarding";
  const isNoTeam = to.path === "/no-team";

  if (!user.value) {
    if (isAppRoute || isOnboarding || isNoTeam) {
      return navigateTo("/auth/login");
    }
    return;
  }

  const metadata = user.value.user_metadata ?? {};
  const onboarded = metadata.onboarded === true;
  const hasTeam = metadata.has_team === true;
  const noTeamConfirmed = metadata.has_team === false;

  if (!onboarded) {
    if (isOnboarding || isInviteRoute) return;
    return navigateTo("/onboarding");
  }

  if (noTeamConfirmed) {
    if (isNoTeam || isInviteRoute) return;
    return navigateTo("/no-team");
  }

  if ((isAuthRoute && !isResetPassword) || isOnboarding || isNoTeam) {
    return navigateTo(hasTeam ? "/app" : "/no-team");
  }
});
```

### 4. Replace `app/pages/index.vue` with a public homepage:

```vue
<template>
  <div class="min-h-screen flex flex-col items-center justify-center px-4 text-center">
    <h1 class="text-4xl sm:text-5xl font-bold tracking-tight">VueStarter</h1>
    <p class="mt-4 text-lg text-muted max-w-xl">
      A Vue + Nuxt + Supabase starter kit for teams.
    </p>
    <div class="mt-8 flex flex-wrap justify-center gap-3">
      <UButton v-if="user" to="/app" size="lg" color="primary">Go to app</UButton>
      <template v-else>
        <UButton to="/auth/login" size="lg" color="primary">Sign in</UButton>
        <UButton to="/auth/signup" size="lg" color="neutral" variant="outline">Create account</UButton>
      </template>
    </div>
  </div>
</template>

<script setup lang="ts">
definePageMeta({ layout: "plain" });
const user = useSupabaseUser();
useHead({ titleTemplate: "%s", title: "VueStarter — your tagline here" });
</script>
```

(Edit the brand name and tagline to match your app.)

### 5. Update path references everywhere

Grep for every old path root and rewrite to the `/app` prefix. Replacements:

- `/dashboard` → `/app`
- `/settings` and `/settings/*` → `/app/settings` and `/app/settings/*`
- `/chat` and `/chat/*` → `/app/chat` and `/app/chat/*`
- `/activity` → `/app/activity`
- For each custom feature you moved in step 2: `/<feature>` and `/<feature>/*` → `/app/<feature>` and `/app/<feature>/*`

Files commonly affected (check each — skip any that don't exist in your fork):

- `app/components/layout/sidebar/Links.vue` — `to:` props for Dashboard / Chat / Settings nav
- `app/components/layout/sidebar/Footer.vue` — `to:` and `active:` for Settings / Team / Profile / Members / Activity
- `app/components/layout/sidebar/Header.vue` — Team Settings dropdown link
- `app/components/chat/Sidebar.vue` — New chat button + chat link `:to` + `navigateTo` after delete
- `app/components/auth/OnboardingForm.vue` — both `navigateTo` calls (after team create + auto-onboard branch)
- `app/utils/auth.ts` — `navigateTo` in `redirectAfterAuth` AND the JSDoc comment that names the destination
- `app/utils/requirePermission.ts` — change default `fallback = "/dashboard"` to `fallback = "/app"`
- `app/middleware/requireAiChat.ts` — `navigateTo` redirect target
- `app/error.vue` — see step 7
- `app/pages/invite/[token].vue` — "Already accepted" CTA + post-accept `navigateTo`
- `app/pages/no-team.vue` — `navigateTo` in the team-appeared watcher
- `app/pages/app/settings.vue` — every `to:` in the tabs nav
- `app/pages/app/settings/announcements.vue` — `requirePermission` fallback arg
- `app/pages/app/settings/team.vue` — `requirePermission` fallback + delete-team `navigateTo` + the surrounding code comment that names the destination
- `app/pages/app/chat.vue` — `navigateTo` on team switch
- `app/pages/app/chat/index.vue` — `navigateTo` after starting a chat
- Any custom files you added — anywhere they linked to old top-level routes
- Code comments and JSDoc that mention old URLs (e.g., "redirects to /dashboard") — update prose too

DO NOT change:

- `/api/*` paths — server routes don't move
- `/auth/*`, `/invite/*`, `/onboarding`, `/no-team` — these stay where they are
- Display labels like `label: "Dashboard"` in nav items
- Nuxt UI component names like `UDashboardPanel`, `UDashboardSidebar`, `UDashboardNavbar`
- Page titles in `useHead({ title: "Dashboard" })`
- Layouts (`app/layouts/default.vue` and `plain.vue` stay as-is — `default.vue` auto-applies to `pages/app/**` since they don't override)

### 6. Verify with a follow-up grep

```bash
grep -rEn '/(dashboard|chat|activity)(/|"|'\''|`)' app/ server/ shared/ layers/ 2>/dev/null | grep -v '/app/' | grep -v UDashboard | grep -v 'app/components/activity'
grep -rEn '\b/settings(/|"|'\''|`)' app/ server/ shared/ layers/ 2>/dev/null | grep -v '/app/settings'
```

Both should return zero rows. If they don't, fix the remaining references and re-run.

### 7. Update `app/error.vue`

Send the error-page back button to the universally-safe public homepage:

```vue
<UButton label="Back to home" block @click="handleError" />
```

```ts
function handleError() {
  clearError({ redirect: "/" });
}
```

### 8. Update `CLAUDE.md`

Find the **Routing** subsection of the Project structure section and replace it with this:

```markdown
Routing: the app is split into a public surface and a gated subtree.

- Public pages live at the top level: `/` (marketing homepage), `/auth/*`,
  `/invite/*`, plus future marketing routes (`/pricing`, `/about`,
  `/blog/*`, `/legal/*`, etc.). No auth check, no opt-in needed — anything
  not under `/app/*` is public by default.
- The gated app surface lives entirely under `/app/*`. `auth.global.ts`
  enforces auth + onboarded + team for any path matching `/app` or
  `/app/*` (plus the forced-flow pages `/onboarding` and `/no-team`).
  Adding a file under `app/pages/app/` automatically inherits gating.
- `/app` is the app's home page (`pages/app/index.vue`), not a parent for
  unrelated features. Each feature is a child of `/app` — `/app/chat`,
  `/app/settings`, `/app/activity`, etc. — not nested under another feature.
- Use a parent route file (`pages/app/feature.vue` with `<NuxtPage />`) only
  when the feature needs shared sub-navigation (like `app/settings.vue` has tabs).
```

Also update the example project tree in CLAUDE.md to show `pages/index.vue` as the public homepage and `pages/app/` as the gated subtree containing `index.vue`, `settings.vue` + `settings/`, `chat.vue` + `chat/`, `activity/`, etc. Update the "Route gating" usage note so the `requirePermission` fallback default reads `/app` instead of `/dashboard`. Update any other CLAUDE.md mention of `/activity` to `/app/activity`.

### 9. Update `README.md` (if present)

Search for hardcoded URL references and rewrite:

- `/activity` → `/app/activity`
- `/chat` → `/app/chat`
- `/dashboard` → `/app`
- `/settings` → `/app/settings`

Skip external URLs (e.g. `supabase.com/dashboard`, `dash.cloudflare.com`) and skip prose descriptions of "the dashboard" as a concept.

### 10. Lint, typecheck, and report

```bash
npm run lint && npm run typecheck
```

Fix any errors. Then summarise for me:

- The list of custom features you discovered and moved (from step 1)
- Total count of files edited
- Any references you intentionally left alone (e.g. display labels) so I can spot-check
- A reminder to run `rm -rf .nuxt && npm run dev` before browser-testing — global middleware changes don't always hot-reload cleanly
- Browser-test checklist:
  - Logged-out → `/` renders homepage with Sign in / Create account
  - Logged-out → `/app` redirects to `/auth/login`
  - Sign in → lands on `/app`
  - Sidebar links navigate to `/app/*` and highlight active state correctly
  - Settings tabs work
  - Delete-team flow ends on `/no-team`
  - 404 → "Back to home" → `/`
  - Each custom feature you moved is reachable at its new `/app/<feature>` URL

Manual verification

After Claude finishes, delete the dev cache and restart before testing — global middleware changes don't always hot-reload cleanly:

rm -rf .nuxt && npm run dev

Then walk through these in a browser:

  • Logged-out → / renders the homepage with Sign in + Create account
  • Logged-out → /app redirects to /auth/login
  • Sign in → lands on /app
  • Sidebar links navigate to /app/* and highlight active state correctly
  • Settings tabs (/app/settings/*) work
  • Delete-team flow ends on /no-team
  • 404 page → Back to home/
  • Each custom feature you moved is reachable at its new /app/<feature> URL

What if something breaks?

If the upgrade prompt failed partway:

  1. Run git status — check what was moved or modified
  2. Run git diff to see textual changes
  3. If badly broken, git reset --hard HEAD to roll back, fix the issue noted in Claude's output, and re-run the prompt (it's idempotent thanks to the bail-out check in step 0)
Found an edge case the prompt didn't handle? Open a GitHub issue with the symptom — the prompt itself can be improved.