Platform Access — Repository & Folder Conventions
Domain-first at the top, subdomain-first within. The app (access vs plt) is the top-level boundary. Inside each app, slice by subdomain (a vertical business capability), not by type.
1. Organizing principle
- The app (
accessvsplt) is the top-level boundary — the DDD bounded-context / code-organization line. It's a hard, permanent split in the codebase (different auth posture: pre-auth vs auth-required). It is not a URL or domain split — see §1.1. - Inside each app, slice by subdomain (a vertical business capability), not by type. A subdomain colocates its own pages, components, composables, stores, and routes. Do not create app-wide
components/,stores/,composables/buckets that mix unrelated subdomains. academic-enrollmentanddashboardare both subdomains — they simply live inside different apps.
This is the opposite of the type-first layout in the monolith and in the migration design docs (see §6).
1.1 Domain, URLs, and build topology
One domain, one URL structure — same as platform-frontend today. Both apps are served under plt.solidprofessor.com with the existing path layout: plt.solidprofessor.com/login, plt.solidprofessor.com/dashboard, etc. The access vs plt split is invisible in the URL.
Note: the access.solidprofessor.com / plt.solidprofessor.com "Domain" lines in the access.md / plt.md design docs are dev/staging labels, not the production topology.
Build topology: single Vite SPA with one router (one build, one deploy). This is simplest and closest to how platform-frontend behaves now. Keep the pre-auth/auth route groups lazy-loaded so an unauthenticated user at /login doesn't download the dashboard.
2. Repository tree
platform-access/ (one repo → eventually owned by Pod 4; npm workspaces)
├─ apps/
│ ├─ access/ (pre-auth bounded context)
│ │ └─ src/
│ │ ├─ subdomains/
│ │ │ ├─ auth/ (login, SAML, password recovery, activation)
│ │ │ ├─ signup/ (commercial signup)
│ │ │ ├─ academic-enrollment/
│ │ │ ├─ onboarding/
│ │ │ └─ policy-consent/
│ │ ├─ pages/ (app infrastructure: NotFound, ServerError, Maintenance)
│ │ ├─ layouts/ (shared layouts: AuthLayout, CardLayout, EnrollmentLayout)
│ │ ├─ router/ (composes each subdomain's routes.ts + guards)
│ │ ├─ guards/ (enrollmentGate, resetPassword, authAlt, maintenance)
│ │ ├─ stores/ (app-wide ONLY: e.g. account subset for post-login redirect)
│ │ ├─ App.vue
│ │ └─ main.ts
│ │
│ └─ plt/ (post-auth bounded context)
│ └─ src/
│ ├─ subdomains/
│ │ ├─ dashboard/
│ │ ├─ my-learning/
│ │ ├─ profile/
│ │ ├─ achievements/
│ │ ├─ certificates/
│ │ ├─ subscription/
│ │ ├─ account-switcher/
│ │ └─ feedback/
│ ├─ pages/ (app infrastructure: NotFound, ServerError, Maintenance)
│ ├─ layouts/ (shared layouts: DashboardLayout, DefaultLayout, BlankLayout)
│ ├─ router/
│ ├─ guards/ (auth, accountLocked, maintenance, myLearning)
│ ├─ stores/ (app-wide ONLY: e.g. user, account)
│ ├─ App.vue
│ └─ main.ts
│
└─ packages/ (shared across BOTH apps)
├─ shared-components/ (local layer over @solidprofessorhub/shared-components)
├─ cross-app/ (crossAppRedirect map — same-origin)
├─ stripe/ (useStripe(): access checkout + plt management)
└─ types/
3. Anatomy of a subdomain folder
A subdomain is self-contained: everything for that vertical slice lives together, and nothing from another subdomain bleeds in.
subdomains/academic-enrollment/
├─ pages/ EnrollmentIndex.vue, SchoolCode.vue, EnrollmentAccount.vue,
│ EnrollmentPayment.vue, EnrollmentComplete.vue
├─ components/ ACACard.vue, ACALogin.vue, BaseNavigation.vue, BaseNavigationStep.vue
├─ composables/ useAcademicEnrollment.ts
├─ stores/ academicEnrollment.ts
└─ routes.ts (this subdomain's route definitions, imported by src/router)
subdomains/dashboard/
├─ pages/ DashboardPage.vue (shell — layout + composition only)
├─ components/ DashboardWelcome.vue, ContinueLearning.vue, ThingsToDo.vue,
│ LiveTraining.vue, NewsAndResourcesBlock.vue, CareerDashboardBlock.vue,
│ AchievementsSummary.vue, QuickLinks.vue
├─ composables/ (subdomain-local composables)
├─ stores/ (subdomain-local stores)
└─ routes.ts
Guidelines:
- Each subdomain exports its own
routes.ts;src/routerimports and composes them (see §4). - Page components are thin: layout + composition. Push logic into composables and child components.
3.1 App-level concerns (pages, layouts, router)
Some things live outside subdomains because they span multiple subdomains or aren't business capabilities at all:
| Concern | Location | Why |
|---|---|---|
| Infrastructure pages (404, 500, Maintenance) | src/pages/ |
Not a business capability — app infrastructure |
| Shared layouts (DashboardLayout, AuthLayout) | src/layouts/ |
Used by multiple subdomains (Tier 2) |
| Single-use layout | subdomains/<name>/layouts/ |
Used by only one subdomain (Tier 1) |
| Router | src/router/ |
Composes all subdomain routes + global guards |
| Subdomain routes | subdomains/<name>/routes.ts |
Each subdomain owns its route definitions |
Layouts follow the 3-tier rule:
- If a layout is used by one subdomain → it can live in that subdomain's
layouts/folder - If a layout is used by multiple subdomains → it lives at
src/layouts/(Tier 2) - If a layout is used by both apps → it lives in
packages/(Tier 3)
Router is inherently app-level because it:
- Imports and composes routes from all subdomains
- Applies global navigation guards that span all subdomains
- Is the single orchestration point for the app's navigation
4. Pages & routing (post-Nuxt)
We are not using Nuxt. In a plain Vue 3 + Vite SPA there is no file-based routing, no auto-layouts, and no page middleware — a .vue file under pages/ is just a component until a router maps a path to it.
Mental model: a "page" is a component that a subdomain's routes.ts maps a path to, tagged with a layout and guard via route meta.
4.1 Routing — explicit, per subdomain
// subdomains/academic-enrollment/routes.ts
import type { RouteRecordRaw } from 'vue-router'
export const academicEnrollmentRoutes: RouteRecordRaw[] = [
{
path: '/academic-enrollment',
component: () => import('./pages/EnrollmentIndex.vue'),
meta: { layout: 'enrollment', guard: 'enrollmentGate' },
},
{
path: '/academic-enrollment/code',
component: () => import('./pages/SchoolCode.vue'),
meta: { layout: 'enrollment', guard: 'enrollmentGate' },
},
]
// apps/access/src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { authRoutes } from '@/subdomains/auth/routes'
import { academicEnrollmentRoutes } from '@/subdomains/academic-enrollment/routes'
import { onboardingRoutes } from '@/subdomains/onboarding/routes'
export const router = createRouter({
history: createWebHistory(),
routes: [
...authRoutes,
...academicEnrollmentRoutes,
...onboardingRoutes,
{ path: '/:pathMatch(.*)*', component: () => import('@/pages/NotFound.vue') },
],
})
4.2 Layouts — meta-driven
<!-- App.vue -->
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import DashboardLayout from '@/layouts/DashboardLayout.vue'
import BlankLayout from '@/layouts/BlankLayout.vue'
const route = useRoute()
const layouts: Record<string, unknown> = { dashboard: DashboardLayout, blank: BlankLayout }
const layout = computed(() => layouts[route.meta.layout as string] ?? DashboardLayout)
</script>
<template>
<component :is="layout"><RouterView /></component>
</template>
4.3 Middleware → navigation guards
// apps/plt/src/router/guards.ts
router.beforeEach((to) => {
if (to.meta.requiresAuth && !auth.isAuthenticated) {
return crossAppRedirect('/login') // → access app
}
if (to.meta.guard === 'enrollmentGate' && !enrollmentStore.canAccess(to)) {
return { path: '/academic-enrollment' }
}
})
4.5 Nuxt-isms that are gone — do not port
- No
useAsyncData/useFetchSSR hydration. Fetch client-side in a Pinia store action or inonMounted. - No auto-imports. Import Vue APIs, components, and composables explicitly.
- No
definePageMeta. Route meta lives inroutes.ts. process.client/process.server— delete these, don't port; it's all client.
5. The 3-tier "where does this live?" rule
Decide by who uses it. Start at the narrowest tier; promote only when a real second consumer appears — do not pre-share.
| Tier | Used by… | Lives in | Examples |
|---|---|---|---|
| 1 — Subdomain | one subdomain | apps/<app>/src/subdomains/<subdomain>/… |
ACACard.vue, useAcademicEnrollment |
| 2 — App | multiple subdomains in the same app | apps/<app>/src/{layouts,stores,guards} |
AuthLayout, account/user stores |
| 3 — Shared | both apps | packages/… |
GlobalHeader, useStripe(), crossAppRedirect |
Promotion rule (gray-area components)
A component that is currently single-use but plausibly reusable starts in the app and is promoted to packages/ when the second consumer actually materializes — not before. Over-sharing balloons Foundation and blocks the app teams; under-sharing is cheap to fix later.
6. Translation note — design-doc paths are TYPE-FIRST; convert them
access.md and plt.md list Vue 3 target paths in a type-first layout inherited from the monolith. Do not take those paths literally — translate them to this repo's subdomain-first convention during migration.
| Design-doc target path (type-first) | Use instead (subdomain-first) |
|---|---|
components/auth/LoginForm.vue |
subdomains/auth/components/LoginForm.vue |
components/enrollment/ACACard.vue |
subdomains/academic-enrollment/components/ACACard.vue |
stores/academic-enrollment.ts |
subdomains/academic-enrollment/stores/academicEnrollment.ts |
components/dashboard/LiveTraining.vue |
subdomains/dashboard/components/LiveTraining.vue |
composables/useAcademicEnrollment.ts |
subdomains/academic-enrollment/composables/useAcademicEnrollment.ts |
9. Quick checklist for contributors
- New code belongs to a subdomain unless it's a layout, guard, app-wide store, or shared package.
- Colocate pages/components/composables/stores/routes inside the subdomain folder.
- Subdomain exports its own
routes.ts;src/routercomposes them. - Pages are wired via
routes.tswithmeta.layout+meta.guard; layout resolved inApp.vue; guards inbeforeEach. No Nuxt auto-magic. - Fetch data client-side (store action /
onMounted); nouseAsyncData; import everything explicitly. - Before creating a shared component, confirm a second real consumer exists — otherwise keep it app-local and promote later.
- Migrating from a design doc? Translate type-first paths → subdomain-first (see §6).
- Shared component conversion → Foundation. App-specific conversion → the app story.