@vch-lt/path
v0.3.3
Published
TypeScript SDK for integrating custom course websites with Path patient tracking software (VCH)
Maintainers
Readme
@vch-lt/path
TypeScript SDK for integrating custom course websites with the Path LMS (VCH).
Handles authentication, enrollment data, and progress tracking. Works client-side (any framework) and server-side (Next.js App Router, Nuxt, Express, etc.).
How it works
Authentication uses a bearer token stored in a cookie on your course domain:
- Your course page calls
path.login()→ user is redirected to the Path login page - After login, Path redirects back to your course page with
?path-token=...in the URL - The SDK automatically reads the token, stores it in a cookie, and cleans the URL
- All subsequent API calls include the token as an
Authorization: Bearerheader - Your server-side code reads the same cookie to make authenticated requests
Installation
npm install @vch-lt/pathClient-side usage
Works in any browser environment — vanilla JS, Vue, React, Svelte, etc.
import pathSDK from "@vch-lt/path";
const path = pathSDK({
courseId: "your-course-id",
courseUrl: "https://your-course-site.com",
// pathUrl: 'https://path.vchlearn.ca', // optional, this is the default
});Initialize on page load
Create the SDK instance as early as possible (e.g., in your app entry point). On first instantiation it automatically reads ?path-token= from the URL, stores it in a cookie, and removes it from the URL bar.
import pathSDK from "@vch-lt/path";
const path = pathSDK({ courseId: "...", courseUrl: "..." });Check authentication
const auth = await path.checkAuth();
if (!auth.isAuthenticated) {
// Pass the current URL so the user returns to the right page after login
path.login(window.location.href);
return;
}
console.log(auth.enrollment?.user.email);
console.log(auth.enrollment?.progress);Submit progress
await path.submitProgress("module-1-complete", true);
await path.submitProgress("quiz-score", 85);
await path.submitProgress("chapter", "introduction");Get enrollment and progress
const enrollment = await path.getEnrollmentAndProgress();
console.log(enrollment.progress);Logout
await path.logout();Next.js App Router
Everything imports from @vch-lt/path/next — server SDK, middleware, PathProvider, and usePath() all come from the same import path. The package uses a barrel that re-exports from separate server and client modules, so the Next.js bundler automatically handles the "use client" boundary.
Setup
Set your course ID in the environment so you never have to pass it explicitly. Use NEXT_PUBLIC_PATH_COURSE_ID so it's available on both the server and in Client Components:
# .env.local
NEXT_PUBLIC_PATH_COURSE_ID=your-course-id
NEXT_PUBLIC_PATH_COURSE_URL=https://your-course-site.comMiddleware (required for SSR auth on first login)
Without middleware, the bearer token arrives as ?path-token= in the URL on redirect from Path login. The cookie isn't set until client-side JS runs, so the first SSR render would see an unauthenticated request.
The pre-built middleware fixes this by reading ?path-token=, setting the cookie on a redirect response, and cleaning the token from the URL bar. The browser follows the redirect to the clean URL with the cookie already set, so the SSR render sees an authenticated request.
// middleware.ts
export { pathMiddleware as default, pathMiddlewareConfig as config } from "@vch-lt/path/next";If you already have middleware, compose them:
// middleware.ts
import { createPathMiddleware } from "@vch-lt/path/next";
import type { NextRequest } from "next/server";
const handlePath = createPathMiddleware();
export function middleware(request: NextRequest) {
const pathResponse = handlePath(request);
if (pathResponse) return pathResponse;
// ... your own middleware logic
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};PathProvider (recommended: place in your layout)
Add PathProvider to your root layout. It automatically fetches auth on the server and hydrates the client with reactive state. Then usePath() works in any nested client component.
Required: Layouts/pages using
PathProvidermust useexport const dynamic = 'force-dynamic'. Next.js intentionally disablescookies()in statically rendered routes — the cookie store will always appear empty andcheckAuth()will always returnisAuthenticated: falsewithout this.
// app/layout.tsx
import { PathProvider } from "@vch-lt/path/next";
export const dynamic = "force-dynamic";
export default async function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<PathProvider>{children}</PathProvider>
</body>
</html>
);
}Now any client component can use usePath():
// app/components/progress-button.tsx
"use client";
import { usePath } from "@vch-lt/path/next";
export function ProgressButton() {
const { auth, submitProgress } = usePath();
const progress = auth.enrollment?.progress ?? [];
return (
<div>
<p>{progress.length} milestones completed</p>
<button onClick={() => submitProgress("module-1", true)}>
Complete Module 1
</button>
</div>
);
}When submitProgress() succeeds, the progress array updates reactively — every component using usePath() re-renders with the new data.
Props:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| pathUrl | string | https://path.vchlearn.ca | Path backend URL |
| courseId | string | NEXT_PUBLIC_PATH_COURSE_ID env var | Your course ID |
| courseUrl | string | NEXT_PUBLIC_PATH_COURSE_URL env var | Your course URL |
| initialAuth | boolean | true | Set to false to skip server-side fetch — the client fetches on mount instead |
usePath() return value
| Field | Type | Description |
| --- | --- | --- |
| auth | AuthStatus | Reactive auth state (updates after submitProgress / logout / refreshAuth) |
| isLoading | boolean | true while the initial fetch is in-flight (only when no initialAuth) |
| getProgress(slug) | Progress \| undefined | Look up a progress record by milestone slug |
| submitProgress(slug, value) | Promise<SubmitProgressResponse> | Submit progress and update local state on success |
| login(callbackUrl?) | void | Redirect to Path login |
| logout() | Promise<void> | Log out and clear state |
| refreshAuth() | Promise<void> | Re-fetch auth from the server |
getProgress cross-references the milestones array (slug → id) with the progress array, so you can look up progress by slug:
const { getProgress, submitProgress } = usePath();
// Read progress
const module1 = getProgress("module-1-completion");
console.log(module1?.value); // "true"
// After submitting, getProgress returns the updated value on the next render
await submitProgress("module-1-completion", true);Route Handler
// app/api/progress/route.ts
import { pathServerSDK } from "@vch-lt/path/next";
import { NextResponse } from "next/server";
export async function GET() {
const path = await pathServerSDK();
const enrollment = await path.getEnrollmentAndProgress();
return NextResponse.json(enrollment);
}Server Action
// app/actions.ts
"use server";
import { pathServerSDK } from "@vch-lt/path/next";
export async function completeModule(milestoneSlug: string) {
const path = await pathServerSDK();
return path.submitProgress(milestoneSlug, true);
}
---
## Nuxt Module (recommended for Nuxt apps)
Use `@vch-lt/path/nuxt` for zero-config Nuxt integration. The module:
- Auto-registers the path-token middleware (SSR auth on first login, no extra redirect)
- Auto-imports `usePath()` — a universal composable that works on server and client transparently
- Auto-imports `pathNuxtSDK` for lower-level use in server routes
- Adds a client plugin that initializes the token from the URL on login redirect
### Setup
```ts
// nuxt.config.ts
export default defineNuxtConfig({
modules: ["@vch-lt/path/nuxt"],
path: {
courseId: "your-course-id",
// pathUrl: 'https://path.vchlearn.ca', // optional, this is the default
},
});Pages and components
usePath() is auto-imported. Data-fetching methods use useAsyncData internally so they fetch on the server during SSR and reuse that result on the client — no double-fetch, no extra server route needed:
<!-- pages/course.vue -->
<script setup lang="ts">
const path = usePath();
// checkAuth() and getEnrollmentAndProgress() work on server and client
const { data: auth } = await path.checkAuth();
if (!auth.value?.isAuthenticated) {
path.login(window.location.href);
}
</script><!-- components/ProgressButton.vue -->
<script setup lang="ts">
const path = usePath();
const complete = async () => {
await path.submitProgress("module-1", true);
};
</script>usePath() methods
| Method | Returns | Description |
| ----------------------------- | ----------------------- | ----------------------------------------------- |
| checkAuth() | AsyncData<AuthStatus> | Auth status + enrollment. SSR-safe. |
| getEnrollmentAndProgress() | AsyncData<Enrollment> | Full enrollment data. SSR-safe. |
| submitProgress(slug, value) | Promise | Record a milestone. Works on server and client. |
| login(callbackUrl?) | void | Redirect to Path login. Client-only. |
| logout() | Promise | Clear session and log out. Client-only. |
Server routes (advanced)
pathNuxtSDK is also auto-imported for cases where you need a server route:
// server/api/something.get.ts
export default defineEventHandler(async (event) => {
const path = await pathNuxtSDK(event);
return path.getEnrollmentAndProgress();
});Low-level Nuxt/H3 usage (without the module)
If you need more control, use @vch-lt/path/nuxt directly:
import { pathNuxtSDK, createPathH3Middleware } from "@vch-lt/path/nuxt";Generic server-side usage
Use @vch-lt/path/server for Express or any other server framework. You read the token from your framework's cookie store and pass it in.
import { pathServerSDK, TOKEN_STORAGE_KEY } from "@vch-lt/path/server";Express
import { pathServerSDK, TOKEN_STORAGE_KEY } from "@vch-lt/path/server";
import cookieParser from "cookie-parser";
app.use(cookieParser());
app.get("/progress", async (req, res) => {
const token = req.cookies[TOKEN_STORAGE_KEY] ?? null;
const path = pathServerSDK({ courseId: "your-course-id", token });
res.json(await path.getEnrollmentAndProgress());
});API Reference
pathSDK(config) — client-side
| Option | Type | Required | Description |
| ----------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------ |
| courseId | string | No | Your course ID from Path (falls back to VITE_PATH_COURSE_ID / NEXT_PUBLIC_PATH_COURSE_ID / PATH_COURSE_ID env var) |
| courseUrl | string | Yes | Your course website URL |
| pathUrl | string | No | Path backend URL (default: https://path.vchlearn.ca) |
Returns an SDK instance with:
| Method | Description |
| -------------------------------------- | ------------------------------------------------------------------------------ |
| checkAuth() | Returns { isAuthenticated, enrollment? } |
| login(callbackUrl?) | Redirects to Path login. Pass window.location.href to return to current page |
| logout() | Clears token and calls logout API |
| getEnrollmentAndProgress() | Returns full enrollment + progress data |
| submitProgress(milestoneSlug, value) | Records progress for a milestone |
getProgress(enrollment, slug) — cross-platform utility
Import from @vch-lt/path (or @vch-lt/path/next). Looks up a progress record by milestone slug by cross-referencing the milestones and progress arrays on the enrollment object. Returns Progress | undefined.
import { getProgress } from "@vch-lt/path";
const enrollment = await path.getEnrollmentAndProgress();
const module1 = getProgress(enrollment, "module-1-completion");pathServerSDK(config?) — Next.js server-side
Import from @vch-lt/path/next. All config fields are optional and fall back to environment variables:
| Option | Env var fallbacks | Description |
| ----------- | -------------------------------------------------------------- | -------------------------------------- |
| courseId | NEXT_PUBLIC_PATH_COURSE_ID, PATH_COURSE_ID | Your course ID from Path |
| courseUrl | NEXT_PUBLIC_PATH_COURSE_URL, COURSE_URL | Your course URL (used for login redirect) |
| pathUrl | PATH_URL | Path backend URL (default: https://path.vchlearn.ca) |
Returns a Promise of a server SDK instance with checkAuth(), getEnrollmentAndProgress(), submitProgress(), getLoginUrl(), and login().
PathProvider — Next.js async Server Component
Import from @vch-lt/path/next. Place in your root layout — automatically fetches auth on the server and provides reactive state to all client components via usePath(). Accepts optional pathUrl, courseId, courseUrl, and initialAuth (boolean, default true) props. See the Next.js section for full usage.
usePath() — Next.js reactive client hook
Import from @vch-lt/path/next in Client Components. Returns reactive auth, isLoading, submitProgress(), login(), logout(), and refreshAuth(). Must be used within a <PathProvider> tree.
pathClientSDK(config) — Next.js client-side (low-level)
Import from @vch-lt/path/next. Re-export of pathSDK for direct use in Client Components without the reactive provider.
pathNuxtSDK(event, config) — Nuxt/H3 server-side
Import from @vch-lt/path/nuxt. Takes an H3 event and optional config (courseId, pathUrl). Returns a Promise of a server SDK instance. When using the Nuxt module, pathNuxtSDK(event) is auto-imported with courseId pre-configured.
usePath() — Nuxt universal composable (Nuxt module only)
Auto-imported. Works in pages, components, and layouts on both server and client. Data-fetching methods (checkAuth, getEnrollmentAndProgress) return AsyncData (Nuxt's useAsyncData result) — SSR-safe with no double-fetch. Action methods (login, logout, submitProgress) return Promises.
pathServerSDK(options) — generic server-side
Import from @vch-lt/path/server. Takes courseId, pathUrl, and token: string | null. Synchronous — returns the SDK instance directly.
Cookie details
The bearer token is stored in a cookie named path_bearer_token on your course domain with SameSite=Lax; Secure. This means:
- It is readable by your course server (for SSR)
- It is not sent cross-origin (Path's server never receives it as a cookie — only as a
Bearerheader) - It survives page reloads and navigation within your course site
- It is cleared when the user logs out
