Praxsuite

End-user authentication

Vincent Depassier · September 17, 2026

These routes sign in the users of your application — not the people who log into Praxsuite. They issue a JWT that your client then sends on /query and /schema, where the end user's roles decide which rows and columns come back.

Auth: an API key, pk_live_ or sk_live_. An end-user JWT is refused here — the one exception is change-password, which requires the JWT instead. Three routes need no credential at all: config, logo and jwks.json.

The response envelope

Every route on this page except config, logo and confirm-email answers in the service envelope:

{
  "isSuccess": true,
  "statusCode": 200,
  "message": null,
  "data": { },
  "errors": []
}

On failure, isSuccess is false, data is null, and the reason is in message — with per-field detail in errors when validation is what failed. The HTTP status matches statusCode.

Bootstrap

GET /{workspaceId}/auth/config

Public — no credential. This is how a browser app learns which key to use.

{
  "success": true,
  "publicKey": "pk_live_…",
  "branding": {
    "name": "Acme",
    "lightPrimary": "…", "darkPrimary": "…",
    "hasLogo": true,
    "logoUrl": "/api/v1/gateway/{workspaceId}/auth/logo"
  },
  "authPageConfig": {
    "defaultLanguage": "es",
    "enabledRegisterFields": ["firstName", "lastName"],
    "enabledSocialProviders": ["google"],
    "requireEmailConfirmation": false,
    "termsUrl": null,
    "privacyUrl": null
  },
  "providers": [ ],
  "oidcProviders": [ ]
}

publicKey is served only for a credential someone has explicitly marked publishable. If nobody has, this route answers 404 and says so — publishing a key grants its table scopes to anyone holding the workspace GUID, so it is a deliberate act, not a default. Scope the credential down first.

providers is the full list of identity providers this workspace accepts, each with its kind. enabledSocialProviders and oidcProviders are older, narrower views of the same rows, kept for clients already reading them.

GET /{workspaceId}/auth/logo

Public. Redirects to the workspace logo, cacheable for an hour. 404 when the workspace has none.

GET /{workspaceId}/auth/jwks.json

Public. The workspace's RSA public key in JWKS form, for verifying post-login redirect tokens (RS256) without a shared secret. Cacheable for an hour.

Local sign-up and sign-in

POST /{workspaceId}/auth/register
{ "email": "a@b.com", "password": "at-least-8", "firstName": "Ada", "lastName": "Lovelace", "username": "ada" }

email and password are required; password is 8–128 characters. The rest are optional, and which of them your sign-up form should show is in authPageConfig.enabledRegisterFields.

POST /{workspaceId}/auth/login
{ "email": "a@b.com", "password": "…" }

Both routes return the same data:

{
  "accessToken": "eyJ…",
  "refreshToken": "…",
  "accessTokenExpiresAt": "2026-01-14T10:22:11Z",
  "refreshTokenExpiresAt": "2026-02-13T09:22:11Z",
  "tokenType": "Bearer",
  "user": {
    "id": "…",
    "email": "a@b.com",
    "firstName": "Ada",
    "lastName": "Lovelace",
    "username": "ada",
    "roles": ["customer"]
  },
  "requiresEmailConfirmation": false,
  "emailVerified": true,
  "postLoginRedirectUrl": null
}

When the workspace requires email confirmation, a register returns requiresEmailConfirmation: true with endUserId and email and no tokens. Do not treat that as a failed call.

If the workspace has an OIDC provider configured, login validates the credentials through that provider instead of locally. No redirect, and nothing changes for your client.

POST /{workspaceId}/auth/refresh
POST /{workspaceId}/auth/logout

Both take { "refreshToken": "…" }. Refresh returns a new pair and invalidates the old refresh token — they rotate, so keep only the newest. Logout revokes one refresh token, which signs out one device.

Email confirmation

GET /{workspaceId}/auth/confirm-email?token=…
POST /{workspaceId}/auth/resend-confirmation      { "email": "a@b.com" }

confirm-email is public and returns a small branded HTML page, not JSON — it is opened from a link in an email, which cannot send headers.

Password

POST /{workspaceId}/auth/forgot-password     { "email": "a@b.com" }
POST /{workspaceId}/auth/verify-reset-code   { "email": "a@b.com", "code": "123456" }
POST /{workspaceId}/auth/reset-password      { "sessionToken": "…", "newPassword": "…", "confirmPassword": "…" }
POST /{workspaceId}/auth/change-password     { "currentPassword": "…", "newPassword": "…", "confirmPassword": "…" }

The reset flow is three calls: ask for a code, exchange the six-digit code for a short-lived sessionToken, then use that token to set the new password. New passwords are 8–128 characters.

change-password is the one route on this page that takes the end user's JWT rather than an API key — the user is changing their own password, so it is their identity that has to be proven.

Social and federated sign-in

GET  /{workspaceId}/auth/{providerSlug}/authorize
POST /{workspaceId}/auth/{providerSlug}/callback   { "code": "…", "state": "…", "redirectUri": "…" }

authorize returns the URL to send the browser to. callback exchanges the returned code for a gateway JWT, in the same data shape as login. state is one-time and CSRF-checked, and redirectUri must match the one used in the authorize step.

authorize resolves what to do from the provider's kind: for a browser provider it returns a URL; for a server-assertion provider it answers 400, because there is no page to send anyone to.

The older OIDC-only spelling still works and still means the same thing:

GET  /{workspaceId}/auth/oidc/{providerSlug}
POST /{workspaceId}/auth/oidc/callback   { "providerSlug": "…", "code": "…", "state": "…", "redirectUri": "…" }

A literal route segment wins over the parameter, so /auth/oidc/... keeps reaching the old handler.

Signing in a player from a game server

POST /{workspaceId}/auth/{providerSlug}/assert
{
  "platformPlayerId": "1234567",
  "displayName": "ada",
  "avatarUrl": "https://…",
  "metadata": { "accountAge": 812, "region": "us-east" }
}

There is no browser inside a game, so there is no redirect to perform. Your game server vouches for the player, and what is being trusted is your sk_live_ key — which must be a server key marked for that platform. Read platformPlayerId server-side, from the platform's own API; a value the client sent you is a value the player can forge.

displayName and avatarUrl are cached for leaderboards and admin views and are never trusted for anything.

Returns the same data as login.

Errors

Status

Typical cause

400

Validation failed, or the provider cannot do browser sign-in

401

No API key, or a JWT sent where an API key is required

404

No publishable credential, no such provider, or no logo

409

The email is already registered

501

The provider kind is registered but its sign-in path is not built yet

Every call here is written to the query log as an Auth event, with the action, the email, the status and the source IP.