VSA Monolith Framework

XAuth client API

Everything an app asks the XAuth console after sign-in: its people, branches, roles, subscription and account actions. Each call is one method in vsd/xauth-client (PHP) and one in @vsd/xauth-client-next (TypeScript), over the same endpoint.

You call methods, not URLs. Each package keeps a table of endpoints from a short key to a path: branches_lookup → /api/xauth/branches/lookup. The key is the path after /api/xauth/ with / and - turned into _, so it is a valid PHP array key and a plain TypeScript property. You only see a key when you override a path or use the raw call(); you only see a path in network traffic or when calling from another stack.

How every call works

PartWhat it is
RequestPOST {XAUTH_BASE_URL}/api/xauth/… with a JSON body
The app's credentialHeaders on every call: X-Project-Id + X-Project-Secret (+ X-App-Id), or X-App-Id + X-App-Secret. The package adds them from .env
The answerHTTP 200 with {status, status_code, error_message, data}. Success is status_code: 200 in the body — HTTP 200 alone means nothing
Engine unreachableTypeScript: a failure with status_code: 503. PHP: a transport error result. Lists answer [], single rows null

Two kinds of authority

Authorised byWhat the engine decidesExamples
App credential Reads are limited to the subscriptions your project is entitled to. Another tenant's row answers exactly like a missing one — an empty list or null — so the answer never confirms it exists branchLookup, roleByName, usersByClass, userIdentity
Actor token (the signed-in user's bearer) The engine judges the person: their subscription, their rank, the action's permission. Subscription comes from the token, never from the body roster, userBranches, userSetStatus, userAddRole, userSetPassword

In PHP, AuthGate takes the actor token from the session for you, so those methods have no token argument. In TypeScript you pass it: await xauth.token() in Next.js, @XauthToken() in NestJS.

Getting a client

PHP — Laravel (local or remote driver, same calls)
use Vsd\XauthClient\Auth\AuthGate;

$ss     = AuthGate::user();     // the signed-in snapshot
$subsId = $ss->subs_id;         // hex string
Next.js — server code only (route handler, server action, server component)
import { createXauth } from '@vsd/xauth-client-next/core'
import { xauth } from '@/lib/xauth'          // createNextXauth()

const engine = createXauth().engine
const user   = await xauth.user()
const token  = await xauth.token()           // the actor token
const subsId = user?.subs_id ?? ''
NestJS — a controller
import { Controller, Get, Inject, Query } from '@nestjs/common'
import { XAUTH_GATEWAY, XauthToken, XauthUser } from '@vsd/xauth-client-next/nest'
import type { XauthGateway } from '@vsd/xauth-client-next/core'

@Controller('staff')
export class StaffController {
    constructor(@Inject(XAUTH_GATEWAY) private readonly xauth: XauthGateway) {}

    @Get()
    list(@XauthToken() token: string, @Query('search') search?: string) {
        return this.xauth.engine.roster(token, { search, page: 1, per_page: 50 })
    }
}

Never call the console from the browser. The credential is a server secret. A browser screen asks your own back end, and your back end makes these calls.

People: the roster

users/roster is the list behind a staff or member screen: one page of the people the signed-in user administers, each with their primary role, their branches, and whether the user may act on them. The engine applies three rules nobody can switch off: only the actor's own subscription; never platform identities; the subscription root and commercial owner only for someone with commercial authority.

PHP
$page = AuthGate::userRoster([
    'search'     => 'dara',                 // login name, full name or phone; case-insensitive
    'user_class' => ['member', 'admin'],    // one class, a comma list, or an array
    'branch_ids' => [3, 5],                 // optional; can only NARROW
    'page'       => 1,
    'per_page'   => 50,                     // 1..200, default 20
]);

foreach ($page['data'] ?? [] as $row) {
    echo $row['local_login_name'], ' ', $row['role_name'], $row['can_manage'] ? ' (can manage)' : '', PHP_EOL;
}
TypeScript
const page = await engine.roster(token, {
    search: 'dara',
    user_class: ['member', 'admin'],
    branch_ids: [3, 5],
    page: 1,
    per_page: 50,
})

if (page.status_code !== 200) {
    // unreachable or refused - show an error, NOT "no users"
    throw new Error(page.error_message)
}

page.data.forEach((row) => console.log(row.local_login_name, row.role_name, row.can_manage))
the answer (data)
{
  "data": [
    {
      "id": 42, "login_name": "wis-dara", "local_login_name": "dara",
      "full_name": "Dara Sok", "phone_number": "012345678", "email": null,
      "user_class": "admin", "status": 1, "is_locked": 0, "identity_type": null,
      "official_id": null, "official_code": null,
      "role_id": 7, "role_name": "Branch Manager", "authority_rank": 40,
      "branches": [{ "id": 3, "name": "Toul Kork" }],
      "can_manage": true
    }
  ],
  "total": 1, "per_page": 50, "current_page": 1, "last_page": 1
}
  • Show local_login_name, not login_name — the stored name may carry the subscription code.
  • can_manage is for the screen only. Offer edit actions when it is true; the engine judges every change again anyway.
  • Narrow with branch_ids, never by filtering the page afterwards. The engine narrows before paging, so pages stay full and the total stays right.
  • TypeScript returns a failure, not an empty page, when it cannot answer. Check status_code first.

Branches

In the tables below, PHP calls are on AuthGate:: and TS calls on engine.

You wantCallReturns
A picker, or labels for idsPHP branchLookup($subsId, $ids, true)
TS branchLookup(subsId, ids, true)
[{id, name, shortcut}]
Just the idsPHP branchIdsBySubs($subsId)
TS branchIdsBySubs(subsId)
[1, 2, 3]
A letterhead or report headerPHP branchDetails($id)
TS branchDetails(id)
name, name_kh, address, phones, email, website, contact person, logo
Which branches a person holds (actor token)PHP userBranches($userId)
TS — (use call('users_branches', …))
[{id, name}]
Grant or withdraw one branch (actor token)PHP userSetBranch($userId, $branchId, true)
TS — (use call('users_set_branch', …))
result
PHP
// every branch of the subscription, by name
$branches = AuthGate::branchLookup($subsId, null, true);

// only some branches - e.g. to label rows that carry branch_id
$labels = collect(AuthGate::branchLookup($subsId, [3, 5]))
    ->mapWithKeys(fn ($b) => [(int) data_get($b, 'id') => data_get($b, 'name')]);

$ids    = AuthGate::branchIdsBySubs($subsId);   // [3, 5, 9]
$header = AuthGate::branchDetails(3);           // object or null
TypeScript
const branches = await engine.branchLookup(subsId, null, true)   // [] on failure
const labels   = new Map(branches.map((b) => [Number(b.id), String(b.name)]))

const ids    = await engine.branchIdsBySubs(subsId)
const header = await engine.branchDetails(3)                       // null when absent or out of scope

Branch numbers. On a project that uses branch_key = 'no', a branch's id in every answer is its number within its subscription, and each row also carries subs_id. Numbers repeat across subscriptions, so store the pair. Store whatever you are given as your branch key; never translate it.

Roles

You wantCallReturns
A role picker: only roles this admin may assignPHP roleOptions($subsId, null, null, 500, true)
TS roleOptions(subsId, { token })
Role rows, in console order
Every role of the subscriptionPHP roleOptions($subsId)
TS roleOptions(subsId)
Role rows
One role by namePHP roleByName('Teacher', $subsId, true)
TS roleByName('Teacher', subsId, true)
{id, name} or null
Who has a role (rows)PHP roleMembers($roleId, true)
TS roleMembers(roleId, true)
[{id, official_id, full_name, user_class}]
Who has a role (ids, by name)PHP roleUserIdsByName('Teacher', $subsId)
TS roleUserIdsByName('Teacher', subsId)
[12, 15]

A role row is {id, name, user_class, code, authority_rank}.

PHP
// the options of an "assign role" select: grantable only (the last argument)
$roles = AuthGate::roleOptions($subsId, 'admin', null, 500, true);

$teacher  = AuthGate::roleByName('Teacher', $subsId, true);       // case-insensitive
$members  = $teacher ? AuthGate::roleMembers((int) $teacher->id) : [];
TypeScript
const roles = await engine.roleOptions(subsId, { token, user_class: 'admin', search: 'man', limit: 100 })

const teacher = await engine.roleByName('Teacher', subsId, true)
const members = teacher ? await engine.roleMembers(teacher.id) : []

Pass the actor token (TypeScript) or true as the last argument (PHP) whenever the list feeds an assign-role picker. Without it you get every role, and the engine will refuse the ones the admin cannot grant — after the user picked one.

Other user reads

PHP
// every user of a class - for backend jobs; no paging, no role, no branches
$drivers = AuthGate::usersByClass('driver', $subsId, 'id,full_name,phone_number');

// one user by a few allowed columns
$u = AuthGate::userIdentity(['login_name' => 'dara'], 'id,full_name,user_class');

// one value
$phone = AuthGate::userValueBy(['id' => 42], 'phone_number');

// does a login name exist (sign-up forms)
$taken = AuthGate::loginNameExists('dara');
TypeScript
const drivers = await engine.usersByClass('driver', subsId, 'id,full_name,phone_number')
const u       = await engine.userIdentity({ login_name: 'dara' }, 'id,full_name,user_class')
const phone   = await engine.userValueBy<string>({ id: 42 }, 'phone_number')
const taken   = await engine.loginNameExists('dara')
  • A screen uses the roster; a job uses usersByClass. The roster pages and judges; usersByClass returns bare rows.
  • Columns are limited. usersByClass returns only the um_users columns the console lets your project read, and userIdentity / userValueBy accept a small allow-list. A password hash or OTP never comes back.
  • For joins against your own tables, use the mirror, not a call per row.

Endpoint reference

Every endpoint both packages know. Credential = the app credential alone; Actor = the signed-in user's token as well; Pre-auth = nobody is signed in (the credential still applies). Token = the call is about the token it carries: checking, refreshing or revoking it. In Method, PHP means AuthGate:: and TS means engine.; one name marked PHP & TS is the same in both. A dash means that package has no method yet — use the raw call shown under Raw calls.

Session

EndpointAuthMethodPurpose
login
/api/xauth/login
Pre-authPHP verifyUser
TS login
Web sign-in; returns tokens and the snapshot
mobile_login
/api/xauth/mobile/login
Pre-authmobileLogin PHP & TSMobile sign-in; the user carries firebase_topics
introspect
/api/xauth/introspect
TokenPHP authenticateToken
TS introspect
Turn a bearer into a user. 401 = refused, 503 = not judged
refresh
/api/xauth/refresh
TokenPHP automatic
TS refreshToken
Swap a refresh token for a new pair; both rotate
logout
/api/xauth/logout
TokenrevokeToken PHP & TSRevoke; an unknown token is not an error
sso_exchange
/api/xauth/sso/exchange
CredentialPHP redeemSsoCode
TS ssoExchange
Redeem the one-time code from a cross-domain sign-in
social_providers
/api/xauth/social/providers
Pre-authPHP XAuthClient::socialProviders
TS socialProviders
Which external sign-in buttons to draw
reports, reports_by_category
/api/xauth/reports[/by-category]
ActorreportCatalog PHP & TSThe reports the user may open, flat or grouped

Users

EndpointAuthMethodPurpose
users_roster
/api/xauth/users/roster
ActorPHP userRoster
TS roster
One page of the people the actor administers, with role, branches, can_manage
users_by_class
/api/xauth/users/by-class
CredentialusersByClass PHP & TSAll users of a class; for jobs
users_identity
/api/xauth/users/identity
CredentialuserIdentity PHP & TSOne user by allowed columns
users_value_by
/api/xauth/users/value-by
CredentialuserValueBy PHP & TSOne column of one user
users_special_details
/api/xauth/users/special-details
CredentialuserSpecialDetails PHP & TSAccount details by login name
users_enrich
/api/xauth/users/enrich
CredentialuserEnrich PHP & TSProfile fields for one user and app
users_mobile_data
/api/xauth/users/mobile-data
CredentialPHP userMobileData
TS —
What a mobile app needs about a user
users_profile_photo
/api/xauth/users/profile-photo
CredentialuserProfilePhoto PHP & TSThe photo URL
users_login_name_exists
/api/xauth/users/login-name-exists
Pre-authloginNameExists PHP & TSIs a login name taken
users_register
/api/xauth/users/register
Pre-authuserRegister PHP & TSSelf-registration, where the class allows it
users_create_identity
/api/xauth/users/create-identity
ActorPHP userCreateIdentity
TS —
An admin creates someone; rank and permission are checked
users_update
/api/xauth/users/update
CredentialuserUpdate PHP & TSUpdate profile fields
users_set_status
/api/xauth/users/set-status
ActoruserSetStatus PHP & TSActivate, suspend
users_add_role
/api/xauth/users/add-role
ActoruserAddRole PHP & TSGive a user a role
users_branches
/api/xauth/users/branches
ActorPHP userBranches
TS —
Branches a user holds
users_set_branch
/api/xauth/users/set-branch
ActorPHP userSetBranch
TS —
Grant or withdraw one branch
users_set_password
/api/xauth/users/set-password
ActoruserSetPassword PHP & TSAn admin sets someone's password
users_change_password
/api/xauth/users/change-password
ActoruserChangePassword PHP & TSChange your own password
users_deactivate_self
/api/xauth/users/deactivate-self
ActoruserDeactivateSelf PHP & TSClose your own account
users_delete_identity
/api/xauth/users/delete-identity
ActoruserDeleteIdentity PHP & TSDelete an identity
users_generate_otp
/api/xauth/users/generate-otp
Pre-authuserGenerateOtp PHP & TSCreate and send an OTP — do not send your own SMS too
users_match_otp
/api/xauth/users/match-otp
Pre-authuserMatchOtp PHP & TSCheck an OTP
users_activate_by_otp
/api/xauth/users/activate-by-otp
Pre-authuserActivateByOtp PHP & TSActivate a new account with its OTP
users_reset_password_by_otp
/api/xauth/users/reset-password-by-otp
Pre-authuserResetPasswordByOtp PHP & TSForgot-password; the OTP authorises it
users_enter_linked_identity
/api/xauth/users/enter-linked-identity
ActorPHP enterLinkedIdentity
TS —
Switch to a linked identity in another app

Branches, roles, subscription, apps

EndpointAuthMethodPurpose
branches_lookup
/api/xauth/branches/lookup
CredentialbranchLookup PHP & TSBranches of a subscription, optionally by ids, by name
branches_ids_by_subs
/api/xauth/branches/ids-by-subs
CredentialbranchIdsBySubs PHP & TSThe branch ids of a subscription
branches_details
/api/xauth/branches/details
CredentialbranchDetails PHP & TSOne branch with its contact block
roles_options
/api/xauth/roles/options
Credential
or Actor
roleOptions PHP & TSRoles for a picker; grantable only with the actor
roles_by_name
/api/xauth/roles/by-name
CredentialroleByName PHP & TSOne role by name
roles_members
/api/xauth/roles/members
CredentialroleMembers PHP & TSThe people holding a role
roles_user_ids_by_name
/api/xauth/roles/user-ids-by-name
CredentialroleUserIdsByName PHP & TSUser ids holding a role
subscriptions_default
/api/xauth/subscriptions/default
CredentialsubscriptionDefault PHP & TSThis app's default subscription
subscriptions_by_app
/api/xauth/subscriptions/by-app
CredentialPHP subscriptionsByApp
TS —
Subscriptions entitled to an app — for a pre-login screen
subscriptions_customer_id
/api/xauth/subscriptions/customer-id
CredentialsubscriptionCustomerId PHP & TSThe subscriber behind a subscription
subscriptions_signup_plans, subscriptions_signup
/api/xauth/subscriptions/signup[-plans]
Pre-authPHP —
TS signupPlans, signup
Self-serve signup of a whole new tenant
subscribers_details
/api/xauth/subscribers/details
CredentialcustomerDetails PHP & TSThe subscriber's details
subscribers_upsert
/api/xauth/subscribers/upsert
ActorcustomerUpsert PHP & TSEdit the subscriber's details
subscribers_logo_*
/api/xauth/subscribers/logo/{save,url,delete}
ActorPHP customerLogoSave, …Url, …Delete
TS customerLogoSave, …Url, …Delete
The subscriber's logo
entitlements_offers, entitlements_activate
/api/xauth/entitlements/{offers,activate}
ActorPHP activateFreeTrial
TS trialOffers, activateTrial
Free-trial offers and activation, by feature code or module ref_code
apps_list
/api/xauth/apps/list
CredentialappList PHP & TSApplications
apps_by_route
/api/xauth/apps/by-route
CredentialPHP — (getApplicationByRoute answers from appList)
TS appByRoute
The app that owns a route

The capability endpoints (jwks, client/user-snapshot, client/authority-events, subscriber/token) are used by /verify, and the mirror/* endpoints by the mirror. You do not call them yourself.

Raw calls

For an endpoint a package has no method for yet, call it by key. The credential, retries and the response unwrapping still apply:

TypeScript
const res = await engine.call<Array<{ id: number; name: string }>>('users_branches', { user_id: 42, token })
// null = engine unreachable; otherwise { status_code, error_message, data }
const branches = res?.status_code === 200 ? res.data ?? [] : []

From any other stack — Python, Go, a quick test — it is a plain JSON POST:

curl
curl -s -X POST "$XAUTH_BASE_URL/api/xauth/branches/lookup" \
  -H "Content-Type: application/json" -H "Accept: application/json" \
  -H "X-App-Id: $XAUTH_APP_ID" -H "X-App-Secret: $XAUTH_APP_SECRET" \
  -d '{"subs_id": "'"$SUBS_ID"'", "order_by_name": true}'
curl — an actor call (the user's bearer goes in the body as token)
curl -s -X POST "$XAUTH_BASE_URL/api/xauth/users/roster" \
  -H "Content-Type: application/json" -H "Accept: application/json" \
  -H "X-App-Id: $XAUTH_APP_ID" -H "X-App-Secret: $XAUTH_APP_SECRET" \
  -d '{"token": "'"$USER_TOKEN"'", "search": "dara", "page": 1, "per_page": 20}'

With a project credential, send X-Project-Id and X-Project-Secret (and X-App-Id for the entry app) instead of the app pair.

Overriding a path

Only needed if a console serves an endpoint somewhere else. Override by key; every method keeps working.

PHP — config/xauth_client.php (publish it first)
'endpoints' => [
    // ...the published defaults, then:
    'branches_lookup' => '/api/xauth/v2/branches/lookup',
],

Laravel merges config shallowly, so a published endpoints array replaces the default one wholesale. Keep every key you did not change, or those calls fail with "no path configured for endpoint".

TypeScript
import { createXauth } from '@vsd/xauth-client-next/core'

const xauth = createXauth({ endpoints: { branches_lookup: '/api/xauth/v2/branches/lookup' } })