VSA Monolith Framework

Next.js & Node

VSA's rules are not tied to Laravel. A Next.js front end and a Node back end (NestJS) work with the same XAuth console as a VSA app: they sign in, check permissions, call APIs and mirror auth tables under the same rules, through TypeScript packages that mirror the PHP ones.

This page is for developers on a Next.js or Node project. The rest of this site covers the Laravel side. Everything set up on the console — the project, its applications, the credential, roles and permissions — is identical for both, and the console's own guides (Project setup, App authentication, Mirror setup) apply to you word for word. Only the app side differs, and that is what this page covers.

What stays the same on any stack

RuleWhat it means for you
The console is the truthIdentity, roles and permissions live on the engine. The app holds no auth table of its own
Permissions come from the engine's snapshotEvery permission check reads the snapshot the engine returns at login and introspect, never a local table, and never the mirror
503 is not 401If the engine is unreachable, a cached session stays alive for a limited time instead of signing everyone out
One response shapeAPIs answer {status_code, data, error_message} over HTTP 200. 401/402: sign in again, once, guarded against a sign-in loop. 403: a refusal, never a redirect to sign-in. 405/422: a business message
The browser never holds the bearerTokens live in HttpOnly cookies; a Next.js app calls its back end through its own proxy
Never authorise from the mirrorA mirror is for display and joins, exactly as on the XAuth mirror page

The packages, side by side

JobPHP (VSA, Laravel)Node, Next.js
Sign-in, sessions, permissionsvsd/xauth-client@vsd/xauth-client-next: /core, /next, /react, /nest
Verify tokens locally (capability services)vsd/xauth-client@vsd/xauth-client-next/verify
Mirror auth tablesphp artisan xauth:mirror:*npx xauth-client mirror:*, @vsd/xauth-client-next/mirror (0.5.0+)
Calling the app's APIsvsapi.js from the VSEL CDN@vsd/vsapi-next, built on @vsd/vsapi-core — the shared core vsapi.js 2.0 is also being built from, so the two cannot drift apart

Install

The packages are private, and only their built files are installed — the same rule Composer applies to vsd/xauth-client. Add them as a Git dependency or from the GitHub Packages registry, as the console's App authentication guide describes, with a read-only token that never goes into a committed package.json. Then paste in the .env block the console gives you:

.env
XAUTH_BASE_URL=https://devauth.vectorasoft.com
XAUTH_APP_ID=...
XAUTH_APP_SECRET=...
XAUTH_HTTP_TIMEOUT=20
XAUTH_SUBS_CODE=acme          # on a host that serves ONE subscription
COOKIE_NAME=acme_at

XAUTH_SUBS_CODE matters: a login name is unique only within a subscription, so without it sign-in works only until another subscriber picks the same name. Check the credential before writing any code:

prove the credential
npx xauth-client check

A Next.js front end

Three files connect the app: the client, the proxy, and the route handlers that own sign-in and refresh.

lib/xauth.ts · proxy.ts · app/api/xauth/[...xauth]/route.ts
// lib/xauth.ts
import { createNextXauth } from '@vsd/xauth-client-next/next'
export const xauth = createNextXauth()

// proxy.ts  (Next 16; middleware.ts on 15)
import { xauth } from '@/lib/xauth'
export const proxy = xauth.proxy({ publicPaths: ['/', '/about'] })

// app/api/xauth/[...xauth]/route.ts
import { xauth } from '@/lib/xauth'
export const { GET, POST } = xauth.handlers()

Permissions are checked the same way on the server and in the browser:

server and client
// a server component or server action
const user = await xauth.user()
if (!xauth.allowed(user, 312)) notFound()

// app/layout.tsx
<XauthProvider user={await xauth.browserUser()}>{children}</XauthProvider>

// a client component
const { user, can, logout } = useXauth()
<Can prn={312}><DeleteButton /></Can>

The proxy must leave /api/xauth/* alone. The engine replaces both tokens on refresh and invalidates the old ones, so a proxy that handles POST /api/xauth/refresh spends the refresh cookie before the handler can read it, and every refresh lands on the sign-in page. The package's proxy already leaves that path alone; if you write your own proxy, exclude it too.

API calls go through @vsd/vsapi-next. You declare each endpoint once in a shape file, and every call gets the VSA behaviour for free: response parsing, typed errors, timeouts, retries, one shared 401 refresh and the sign-in loop guard.

lib/api.shape.ts
import { endpoint } from '@vsd/vsapi-next/define'

export const shape = {
    class: {
        list:   endpoint<Paginated<Class>, ListPayload>({ path: 'class/list-class' }),
        update: endpoint<Class, ClassInput, string>({ method: 'PATCH', path: (id) => `class/update-class/${id}`, invalidates: ['class'] }),
    },
}

A Node back end (NestJS)

One guard protects every route. A rotated access token goes back on X-Access-Token, exactly as the Laravel APIAuthenticate middleware does it.

app.module.ts and a controller
providers: [
    { provide: XAUTH_GATEWAY, useValue: createGateway() },
    { provide: APP_GUARD, useClass: XauthGuard },
]

@Get('me') me(@XauthUser() user: AuthSnapshot) {}
@Public() @Get('health') health() {}
@RequirePermission(312) @Post('save') save() {}

Admin screens list a subscription's people with engine.roster(), its branches with branchLookup(), and its roles with roleOptions(). roster() returns a failure, not an empty page, when the engine cannot answer, so a screen never shows "no users" when the real answer is "could not ask". The XAuth client API page lists every call with examples. A capability service (chat, signal, storage) verifies tokens locally with /verify and SubscriberGuard; the console's Capability services guide is the reference.

The mirror on Node

The same contract as the XAuth mirror: exact rows, exact values, exact columns, a digest per run, a differential that escalates to a full refresh when the digest still differs, reported drift, and the same xauth_mirror_state table. It uses the same allow-list, credentials and flags as the PHP sync.

The mirror needs a database, so it runs where the database is — in a Next.js + NestJS product that is the NestJS API, not the Next.js app. It writes to PostgreSQL through pg, an optional peer dependency (pnpm add pg).

xauth-mirror.config.mjs
import { defineMirror } from '@vsd/xauth-client-next/mirror'

export default defineMirror({
    // omitted = XAUTH_MIRROR_DATABASE_URL, then DATABASE_URL, then the PG* variables
    connection: process.env.DATABASE_URL,
    tables: {
        um_users: { columns: ['id', 'login_name', 'full_name', 'user_class', 'status', 'subs_id', 'updated_at'] },
    },
})
in order
npx xauth-client mirror:tables             # what the console lets this app mirror
npx xauth-client mirror:sync --dry-run     # what it would create and pull; writes nothing
npx xauth-client mirror:sync               # first run: full; afterwards: differential
npx xauth-client mirror:verify             # exit 1 on drift - for cron and CI

The manifest lists columns only, because the types come from the console. The --table, --full, --schema-only, --data-only, --replace-tables, --since and --chunk flags mean what they mean in PHP, and the xauth:mirror:sync spelling works too.

There is no Laravel scheduler, so schedule the sync yourself. As in PHP, the scheduled sync is what keeps the mirror correct:

crontab on the server
*/15 * * * * cd /srv/acme-api && npx xauth-client mirror:sync >> /var/log/xauth-mirror.log 2>&1
0 3 * * *    cd /srv/acme-api && npx xauth-client mirror:verify >> /var/log/xauth-mirror.log 2>&1

The same sync runs from code, in a scheduled job or a route that refreshes particular ids: new MirrorSync(db, new MirrorClient(undefined, manifest), manifest).run(), or .refreshIds('um_users', [12, 15]). A PostgreSQL advisory lock stops two syncs of the same table from overlapping.

PHP hasOn Node
Profiles, one per databaseOne config file is one database; --config=<path> for a second
Generated Eloquent modelsNone — read the mirror through your own query builder
The console's change callback, the queue hand-offNot ported. The scheduled sync is the guarantee; the callback only made it faster
status, doctor, watchNot ported. npx xauth-client check proves the credential; mirror:verify proves the data
Tables the console keys by something other than id (um_branches on a project that uses branch numbers)Refused by name, rather than synced against the wrong key

What has no twin yet

The pieces below are Laravel-only today. A Next.js project uses its own tooling for them:

  • the vsa command line — vsa new, the generators, vsa check, the builds;
  • app files, VSPageRouter and the VSEL components (a ListView port is planned);
  • the Report Center;
  • the translation commands.

vsa xauth:register and vsa xauth:sync read Laravel app files, so a Node project's catalog is set up on the console, or imported there from a project manifest with php artisan xauth:provision.

The other direction: a VSA front end on another back end

The VSA framework is defined by its browser side — VSApp, the router, the page contract — and that side does not care which server it talks to. Laravel is one back end that provides it; Node, Python, Go or C# can too, if they serve the same five things:

WhatShape
the shell documentone HTML page per app, with the meta tags VSApp reads, the layout chrome, and every page root it carries hidden
#vs-app-filea JSON block: {app, home, main_route, alternative_route, pages, lazy}
one page's markupGET /<main_route>/_page/<Name> for a declared lazy page only, under the same session
the page's filesscript and stylesheet URLs: hashed when built, a cache-busting query otherwise
the APIs the pages call{status_code, data, error_message} over HTTP 200

A Node back end that does this gets its auth and mirror from the packages above. The project template's docs/framework.md has the full contract under "What the front end asks of a back end".