AppEngine authentication is opinionated and consistent. One global guard, two principal types, JWT for sessions, API keys for server-to-server, OAuth as a sign-in option. Once you know how the pieces fit, every endpoint behaves the same way.
The global guard
JwtAuthGuard is registered as APP_GUARD in app.module.ts. Every request hits it first. The guard:
- Reads
Authorization: Bearer <jwt>(orx-api-key). - Resolves the principal — a User or a Customer record from the database.
- Attaches the principal to
request.userso controllers and decorators (@CurrentUser(),@CurrentCustomer()) can access it. - Allows the request through, or rejects with 401.
Endpoints that should skip the guard are marked with @PublicRoute():
// src/users/auth/jwt.auth.guard.ts
export const IS_PUBLIC_KEY = 'isPublic';
export const PublicRoute = () => SetMetadata(IS_PUBLIC_KEY, true);
Any controller method with @PublicRoute() is reachable without a token. These are intentionally narrow: sign-in/sign-up flows, public storefront browse, public form submissions, magic-link redemption.
Two principal types
The dual-principal model splits "people who run an app" from "people who use it":
| Principal | Stored in | Sign-in | JWT subject | Typical scope |
|---|---|---|---|---|
| User | user collection | POST /profile/signin | the User's pk | Admin, multi-org access via roles |
| Customer | customer collection | POST /profile/customer/signin | the Customer's pk | Single-org, customer-scoped data |
Both flows return the same JWT shape — the difference is what the JWT unlocks. A Customer JWT can hit /storefront/cart, /profile/customer/update, /client-data/*. A User JWT additionally unlocks admin endpoints (/crm/leads, /repository/*, /org-management/*) within the orgs the user is a member of.
See Principals and tenancy for the full split.
What's in the JWT
Both flows return the same envelope:
{
"data": {
"token": "eyJhbGc...",
"refresh_token": "eyJhbGc...",
"user": { "pk": "...", "email": "...", "firstName": "...", "roles": [...], ... }
}
}
The access token is short-lived; the refresh token has a longer expiry. When the access token expires, exchange the refresh token for a new pair.
/profile/user/refreshNo auth/profile/customer/refreshNo authOAuth sign-in
AppEngine ships passport strategies for several providers — they're all sign-in methods, not authorization-grant flows. The user goes through the provider, AppEngine catches the callback, validates the profile, and issues an AppMint JWT. The provider's own access token is not what subsequent calls use.
Supported providers, all under /profile/{provider}:
- GitHub —
/profile/github,/profile/github/redirect - Google —
/profile/google,/profile/google/redirect - Facebook —
/profile/facebook,/profile/facebook/redirect - Microsoft — strategy file present (
microsoft.strategy.ts); enable per-org - Magic link —
/profile/magic-link,/profile/magic-link/redirect(Customer);/profile/user/magic-link/*(User) - Code-based —
/profile/code/{email}for one-time-code auth
See OAuth flows for the redirect/callback pattern and per-provider setup.
API keys
For server-to-server calls — backend services, build pipelines, scripts — JWT sessions don't fit. API keys give you a long-lived credential sent as x-api-key:
GET /repository/find/contact HTTP/1.1
Host: appengine.appmint.io
orgid: my-org
x-api-key: ak_live_5f3a2b...
Keys are issued from Studio Manager (or the /api-key/create endpoint), scoped to a User principal, and carry their own permission scope and rate limits. Rotation, revocation, and usage tracking are first-class.
See API keys for issuance and lifecycle.
RBAC and permissions
Once authenticated, what a principal can do depends on:
- Role —
RootAdmin,ConfigAdmin,ContentAdmin, plus any custom roles. Checked via@Roles(...). - Permission verbs —
create,read,update,delete,review,approve. Checked via@RequirePermissions(...). - Per-record
requiredRole— collections and individual records can override the role/permission matrix.
Endpoints stack these decorators on top of the global JWT guard. A request with the wrong role gets 403, not 401 — auth succeeded; authorization didn't.
2FA and device security
Optional second factor and device tracking sit under /profile/security/*. Once enabled for a User, sign-in returns a partial token that requires a 2FA challenge before becoming a full session. Device records track every login and let admins revoke individual sessions.
SSO
A separate SSO surface (/sso/*) supports cross-org authentication and IDE-driven pre-authenticated flows. Useful when one Appmint account spans multiple tenants.
See SSO.
Quick reference — which header for which call
# Public — only orgid
curl https://appengine.appmint.io/storefront/products -H "orgid: my-org"
# Customer or User session
curl https://appengine.appmint.io/repository/find/contact \
-H "orgid: my-org" \
-H "Authorization: Bearer eyJhbGc..."
# Server-to-server with API key
curl https://appengine.appmint.io/repository/find/contact \
-H "orgid: my-org" \
-H "x-api-key: ak_live_..."
orgid is always required. JWT and API key are mutually exclusive in practice, but if you send both, JWT wins.