SP

Platform Access

Repository & Folder Conventions
Epic: SPPLT-18663 Status: Authoritative Audience: access + plt teams, Pod 4
Foundation Epic

Platform Access — Repository & Folder Conventions

Attach to: Foundation epic (SPPLT-18663) | When a design doc's target path conflicts with this document, this document wins (see §6).

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

1. Organizing principle

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

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 Subdomain Folder Anatomy

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:

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:

Router is inherently app-level because it:

  1. Imports and composes routes from all subdomains
  2. Applies global navigation guards that span all subdomains
  3. Is the single orchestration point for the app's navigation
§4 Pages & Routing

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

§5 The 3-Tier Rule

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

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

9. Quick checklist for contributors