Skip to content

App registration

How a new app gets wired into the Vendidit auth system. Every consumer — backend service, frontend SPA, mobile client, or internal tool — goes through this same flow, once.

For the broader picture (token lifecycle, multi-tenant model) see the auth-server architecture. This doc covers only the onboarding contract.


TL;DR

1. system_admin runs `ven apps create` (or POSTs to /admin/apps)
2. New app sets two env vars: VENDIDIT_APP_CODE + JWT_ACCESS_SECRET
3. New app calls /auth/login with app_code; gets back a JWT scoped to that app

Three steps total. Step 1 is one-time per app. Steps 2-3 are normal config + runtime.

Step 1 is easiest via the ven CLI — see the Apps command for the interactive form. The raw REST equivalent is documented in §2 below for reference.


1. Concepts in one paragraph each

App. A user-facing consumer of Vendidit auth — anything that initiates logins or holds access tokens. Identified by a stable code (e.g. marketplace-v2, release-manager). One row in the apps table. Owns its redirect-URL allowlist, opt-in/out of auto-grant, and the set of permission services it consumes. Required before tokens can be minted for it.

Service. A backend that owns and declares a slice of the permission catalog. Identified by a stable string (e.g. auction-api, billing). A service self-registers its permissions at boot via POST /admin/permissions/register; auth-server reconciles (upserts new, prunes removed). Most apps are 1:1 with a service of the same code. Pure-frontend apps may have no service at all and still work fine — they just don’t carry custom permissions.

user_apps membership. A row per (user, app) pair indicating the user is allowed into that app. Created either automatically on first login (auto_grant_on_signup: true) or explicitly by an admin (POST /admin/users/{userId}/apps/{appId}).


2. The registration step (one-time per app)

A system_admin creates the app row. Two paths — the CLI is the canonical one; the REST API is the underlying surface.

Path A — ven apps create (canonical)

Install once: npm install -g github:Vendidit/cli.

Terminal window
ven login # sign in as a system_admin user
ven apps create \
--code marketplace-v2 \
--name "Marketplace v2" \
--description "Public marketplace, browser SPA" \
--redirect-url https://marketplace-v2.vendidit.com/auth/callback \
--auth-method password --auth-method google \
--auto-grant

Or run ven apps and pick + Register a new app for the interactive form (validates every field, prompts for permissions, prints the new app’s id + code on success).

Path B — direct REST

  1. Get a system_admin access token. Log in via the demo at auth-demo.vendidit.com as a user with the system_admin role (today: demotest@vendidit.com). Copy the access token from the browser devtools (any authed request → Authorization header), or log in over the REST API and capture the token from the response.

  2. POST to /admin/apps with the new app’s config:

POST /admin/apps
Authorization: Bearer <system-admin-token>
Content-Type: application/json
{
"code": "marketplace-v2",
"name": "Marketplace v2",
"description": "Public marketplace, browser SPA",
"allowed_redirect_urls": ["https://marketplace-v2.vendidit.com/auth/callback"],
"service_codes": ["marketplace-v2"], // optional; defaults to [code]
"auto_grant_on_signup": true, // optional; default false
"status": "active"
}

Quick curl form:

Terminal window
curl -X POST https://new-auth.vendidit.com/api/v1/admin/apps \
-H "Authorization: Bearer $VENDIDIT_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"code": "marketplace-v2",
"name": "Marketplace v2",
"allowed_redirect_urls": ["https://marketplace-v2.vendidit.com/auth/callback"],
"auto_grant_on_signup": true
}'

Response:

{
"id": "9c1f...",
"code": "marketplace-v2",
"status": "active",
"created_at": "..."
}

That’s the whole platform-side setup.

What gets validated at registration

  • code is unique, kebab-case, ≤100 chars.
  • allowed_redirect_urls is non-empty in production mode (refused by config validation).
  • service_codes defaults to [code] if omitted; entries are stable strings, not required to point at a service that exists yet (a service can register later).

3. The consumer’s side (per environment)

The new app sets two env vars and calls the API. The shape of “calling the API” depends on the app type.

Required env

VENDIDIT_AUTH_URL=https://auth.vendidit.com
VENDIDIT_APP_CODE=marketplace-v2
JWT_ACCESS_SECRET=<shared with auth-server, ≥32 chars>

The JWT secret is the same one the auth-server signs with; sharing it lets the consumer validate access tokens locally (HMAC signature) without a network hop per request.

Backend-only API (Node)

import { AuthClientModule } from '@vendidit/auth-server-nest';
AuthClientModule.forRoot({
authUrl: process.env.VENDIDIT_AUTH_URL,
appCode: process.env.VENDIDIT_APP_CODE,
jwtSecret: process.env.JWT_ACCESS_SECRET,
});
// In a controller:
@UseGuards(AuthGuard)
@Get('/me/orders')
listOrders(@CurrentUser() user) { ... }

The SDK validates the bearer token locally and asserts claims.app_id matches this app’s code. Any mismatch → 401.

Frontend SPA (Preact / React)

The browser SDK (@vendidit/auth-client) is the canonical way to integrate. It handles app_code on every login, refresh-token rotation, BroadcastChannel cross-tab sync, and PKCE for SSO. See docs.auth.vendidit.com/auth-client/ for the full surface.

Minimal example:

import { createAuthClient } from '@vendidit/auth-client';
const auth = createAuthClient({
apiBaseUrl: 'https://new-auth.vendidit.com/api/v1',
appCode: 'marketplace-v2',
});
await auth.login({ email, password });

Mobile / server-to-server

Same HTTP contract as the SPA. Until language-specific SDKs exist, consumers do direct REST calls. The wire protocol is stable.


4. What’s in the issued JWT

When a user logs in with app_code: "marketplace-v2", the access token carries:

ClaimValue
app_idthe marketplace-v2 UUID
app_code"marketplace-v2"
uiduser UUID
email, first_name, last_nameuser identity
org_id, org_slugoptional org context
rolesrole codes (e.g. ["base_user"])
permissionsunion of permissions across services in apps.service_codes, plus all core permissions
tvper-user token version (for logout-everywhere — see AUDIT §1.10)
iss, aud, exp, nbf, iat, jtistandard JWT claims

core permissions (auth-server’s own slice — users:read_self, users:update_self, etc.) are always included regardless of which app the token is scoped to. They’re the bedrock catalog every user gets.

If the app’s service_codes list services that haven’t registered any permissions yet, the permissions array is just core + whatever role-based perms the user has. That’s fine — the app simply doesn’t carry custom permissions yet.


5. Adding custom permissions (optional, later)

When the new app wants to introduce its own permissions (e.g. listings:create, bids:place):

  1. The app’s backend calls on boot:
    POST /admin/permissions/register
    Authorization: Bearer <service-account-or-system-admin>
    {
    "service": "marketplace-v2",
    "permissions": [
    { "code": "listings:create", "name": "Create listing", "resource": "listings", "action": "create" },
    { "code": "bids:place", "name": "Place bid", "resource": "bids", "action": "place" }
    ]
    }
  2. Auth reconciles: upserts these, prunes any previously-declared marketplace-v2 permissions not in the list. Idempotent — safe to call every boot.
  3. A system_admin assigns the new permissions to roles via the existing role-permission API.
  4. Users with those roles, logging into marketplace-v2, see the permissions in their JWT.

This step is purely opt-in. Apps that don’t need custom permissions never call this.


6. Granting users access

Two modes, set on the apps row:

  • auto_grant_on_signup: true — every user who logs into the app gets a user_apps row on first attempt. Use for public consumer apps where any Vendidit user can enter.
  • auto_grant_on_signup: false (default) — users must be explicitly granted:
    POST /admin/users/{userId}/apps/{appId}
    Authorization: Bearer <system-admin>
    Use for internal tools and paid-tier apps.

A user can be revoked from one app without affecting their session in others:

DELETE /admin/users/{userId}/apps/{appId}

This sets the user_apps.status to revoked and bumps the user’s token version, so their currently-valid access token for that app stops validating on the next request.


7. What’s required vs. optional, at a glance

StepRequired?WhoWhen
Create the app rowYessystem_admin (POST /admin/apps)Once per app
Set VENDIDIT_APP_CODE + JWT_ACCESS_SECRET envYesthe new appPer environment
Pass app_code on /auth/loginYesthe new appEvery login
Register a service catalog (permissions/register)Nothe new app’s backendBoot, only if the app owns custom permissions
Grant users via user_appsNo (if auto_grant_on_signup)system_admin or autoFirst login or explicit grant
Configure SSO providersNosystem_adminOnly if the app accepts third-party login

8. Common pitfalls

  • Forgetting app_code on login. Returns either a 400 (when app_code is required globally) or a token without an app_id claim, depending on config. Downstream services that enforce claims.app_id == self.app_id will reject the token.
  • Redirect URL mismatch. https://marketplace-v2.vendidit.com/auth/callback registered, request goes to https://marketplace-v2.vendidit.com/auth/callback/ (trailing slash) — strict match, fails. Use the trailing * wildcard if you need a path prefix.
  • JWT secret drift. Auth-server rotates JWT_ACCESS_SECRET, the consumer doesn’t pick up the change. Validation fails for every request. Rotations need to be coordinated; the dual-secret rotation feature on the Phase C roadmap will fix this.
  • Stale service_codes after splitting a backend. If marketplace-v2’s billing logic gets pulled into a separate billing service, update apps.service_codes to ["marketplace-v2", "billing"]. Otherwise marketplace-v2 users lose access to billing permissions in their JWT.

9. References