How to consume PSI’s shared frontend packages, and the policy for versioning
them. These are the Layer-1 building blocks of the PSI Web App Platform.
Source repo: psi-platform on GHE.
What these are
A set of small, single-purpose npm packages published to GHE Packages (the
private npm registry on our GitHub Enterprise instance) under the
@progressivesurface scope. They replace per-app duplication: instead of
every web app vendoring ps.css and re-deriving its MSAL config, apps install
the package.
Why @progressivesurface and not @psi? GHE/GitHub Packages require the
npm scope to equal the owning org login, and the org is ProgressiveSurface.
There is no psi org, so @psi/* cannot be published there. See
psi-platform/docs/decisions/0002-package-scope.md.
App Insights, PSIPortalShell, Vite plugin, scaffolding
planned
Owner: Adam Devereaux (adevereaux@progressivesurface.com). Breaking
(major) changes require owner sign-off.
Consuming the packages
These are private packages — every consumer authenticates; there is no
anonymous read. The setup is identical for every repo and every package, and you
do it once per repo (committed) and once per developer machine. A single
token covers all current and future @progressivesurface/* packages across
all repos, because auth is keyed by the registry host, not per package.
The token is supplied by the NODE_AUTH_TOKEN env var at install time — this
file is safe to commit.
2. CI — zero developer tokens
The workflow’s built-in GITHUB_TOKEN is accepted by the registry. Grant it
package read and pass it as NODE_AUTH_TOKEN:
permissions: contents: read packages: read # lets npm read @progressivesurface/* from GHE Packagesjobs: build: steps: - uses: actions/checkout@v4 - run: npm ci # or pnpm install env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
No PAT, no per-repo secret. (PSI Portal’s deploy.yml is the reference.)
⚠️ Critical caveat — the packages (and their source repo) MUST be internal.
A repo’s job GITHUB_TOKEN is scoped to that repo only. These npm packages are
repository-scoped — they inherit access from their source repo (psi-platform).
If psi-platform (and therefore the packages) is private, a different repo’s
GITHUB_TOKENcannot read them — npm ci fails with
403 ... permission_denied: read_package, even with packages: read. This silently
broke every psi-portal deploy for ~4 weeks (2026-06-01 → 06-29); the earlier “fix”
PRs were only verified locally, where a classic PAT (with repo scope) masked the gap.
The fix: set the psi-platform repo → internal, then each package →
internal (a package can’t go internal while its source repo is private). With the
packages internal, any org repo’s GITHUB_TOKEN reads them and the snippet above works
as written.
New packages: there is no API to change package visibility (REST PATCH 404s;
no GraphQL mutation) — it’s a UI-only toggle. So don’t script it; instead publish from
the now-internal psi-platform and new packages inherit internal from birth. Confirm
on first publish.
3. Local dev — once per machine
⚠️ gh auth token does NOT work here. The GitHub Packages npm registry
rejects the gh CLI’s OAuth token (403 … does not match expected scopes),
even when it has read:packages. You need a classic PAT.
Create the token in your personal settings — not the org’s Settings tab
(looking at the org is why people can’t find it). Go directly to
https://progressivesurface.ghe.com/settings/tokens (or avatar → Settings
→ Developer settings) → Tokens (classic) → Generate. Then:
Select both repo and read:packages — the packages are private and
tied to the private psi-platform repo, so read:packages alone returns 403.
Configure SSO → Authorize → ProgressiveSurface. This org enforces SAML
SSO; an un-authorized token 403s even with the right scopes. (Most-missed step.)
Store it once. The easiest way is the helper script in the psi-platform
repo, which writes both the scope→registry mapping and the auth token to
your user ~/.npmrc (idempotent, UTF-8 no BOM) and verifies read access:
Or do it by hand — this covers every @progressivesurface/* package in every
repo, forever:
npm config set @progressivesurface:registry "https://npm.progressivesurface.ghe.com" --location=usernpm config set //npm.progressivesurface.ghe.com/:_authToken "<ghp_token>" --location=user
npm install / pnpm install now resolves the packages.
See the step-by-step guide in psi-platform/docs/guides/installing-psi-packages.md.
4. Use
// app entry — load the design system once (stop vendoring ps.css)import '@progressivesurface/ui/css';import { PSIButton, PSIKpi } from '@progressivesurface/ui';import { createMsalConfig, initializeMsal, AuthGuard, useAuth } from '@progressivesurface/auth';
@progressivesurface/auth’s defaults encode the
Web App Compliance Standard: redirect flow,
localStorage token cache, silent-acquire-then-redirect recovery, and a
5-minute ID-token refresh guard. See each package’s README.md for the full API.
New apps created with psi-cli new (Phase 5) will scaffold the .npmrc and
the CI block automatically — this manual setup is only for existing repos.
Versioning policy
SemVer, managed with Changesets.
Every change adds a changeset (pnpm changeset) describing the bump.
Patch — bug fixes, no API change. Adopt freely.
Minor — additive (new component, new option). Backward compatible.
Major — breaking. Requires owner sign-off and a coordinated migration
note; consuming apps upgrade deliberately, not automatically.
Apps should pin a caret range (^0.1.0) so they get patches/minors but not
majors.
@progressivesurface/ui tracks the psi-design-system repo: when that
releases a new ps.css, the package re-vendors it and bumps accordingly
(currently mirrors design-system v1.2.1).
Publishing (maintainers)
pnpm changeset # describe the changepnpm version-packages # changeset version → bumps + CHANGELOG# merge to main → GH Actions publishes to GHE Packages and tags @progressivesurface/<pkg>@x.y.z
The publish workflow is identity-based (packages: write on the job
GITHUB_TOKEN); no personal token is used to publish.
Migration guidance
Adopt opportunistically — the next time an app touches CSS or auth, swap to the
package and delete the duplicated code. PSI Portal is the reference migration
(it replaced its vendored ps.css and ~250 lines of MSAL logic). App-specific
configuration (Graph scopes, Entra group IDs) stays in the app; only the
duplicated logic moves to the package.