AppEngine has two kinds of authenticated principal and one tenant boundary. Get these straight and most "why am I getting 401?" debugging disappears.
User vs Customer
A User is staff — someone who runs an app. Admins, operators, content editors, sales reps. Users have roles (RootAdmin, ConfigAdmin, ContentAdmin, custom) and can manage other users, configure the org, and see everything in their tenant.
A Customer is an end-buyer or end-user — someone who uses an app you've built. Customers sign in with their own credentials, see only their own orders/profile/messages, and never have admin power.
The two are stored in different collections (user vs customer), authenticated through different endpoints, and carry different JWTs. Mixing them is the most common integration mistake — a customer JWT will not unlock /crm/leads, and a user JWT calling /storefront/cart looks like an operator placing an order.
| Principal | Sign-in endpoint | Refresh endpoint | Where they appear |
|---|---|---|---|
| User | POST /profile/signin (alias /profile/user/signin) | POST /profile/user/refresh | Studio Manager, admin tooling, server-to-server |
| Customer | POST /profile/customer/signin | POST /profile/customer/refresh | Storefronts, member portals, mobile apps |
/profile/signinNo auth/profile/customer/signinNo authBoth return the same envelope: a JWT, a refresh token, and the principal's record.
The orgid header
Every authenticated request must include the orgid HTTP header — it identifies the tenant the request is acting against. Without it, the API returns 400. With the wrong one, you'll either get 403 or empty results, depending on the route.
POST /repository/create HTTP/1.1
Host: appengine.appmint.io
orgid: acme
Authorization: Bearer eyJhbGc...
Content-Type: application/json
The header is required because the same staff User can be granted access to multiple orgs (think agency consultants). The JWT identifies who you are; orgid tells the server which tenant you're operating on this call.
Even if your JWT was issued for one specific org, AppEngine still expects the header. Adding it is cheap; debugging missing-header 400s is not.
How isolation is enforced
Tenant isolation lives in the repository layer. Every record's sk (sort key) starts with the orgid, and every read/write in RepositoryService scopes its query by the orgid argument. There's no way to ask the database "give me all contacts" — only "give me all contacts for org X".
The JwtAuthGuard resolves the principal and attaches them to the request; the controller then passes the orgid from the header into the repository call. If a User tries to operate on an org they don't belong to, the repository's permission check rejects the call.
RootAdmin is the only role that can cross org boundaries — used for platform support and the AppMint internal tooling.
Auth header combinations
| Pattern | Headers | When to use |
|---|---|---|
| Public | orgid only | Storefront product browse, public contact forms, magic-link emails |
| User JWT | orgid + Authorization: Bearer <jwt> | Admin dashboards, Studio Manager, staff sessions |
| Customer JWT | orgid + Authorization: Bearer <jwt> | Storefronts, member portals — same header, different token |
| API key | orgid + x-api-key: <key> | Server-to-server, build pipelines, scripts |
| Mixed | orgid + both | Fine — JWT takes precedence, API key is the fallback |
API keys are scoped to a User principal under the hood — the apikey service maps the key to a User record and a permission scope. See API keys.
Worked example
A SaaS dashboard signs in a staff member, then calls a customer-scoped endpoint to inspect a buyer's cart.
// 1. Staff sign-in (User flow)
const staff = await fetch('https://appengine.appmint.io/profile/signin', {
method: 'POST',
headers: { orgid: 'acme', 'Content-Type': 'application/json' },
body: JSON.stringify({ email: '[email protected]', password: '...' }),
}).then(r => r.json());
const userJwt = staff.data.token;
// 2. Inspect a customer's cart — note same orgid
const cart = await fetch(
'https://appengine.appmint.io/storefront/cart/get/customer-id-123/cart-id-456',
{ headers: { orgid: 'acme', Authorization: `Bearer ${userJwt}` } }
);
Same orgid for both calls. Different principals would be a different sign-in flow.
Multi-org users
A User record can list multiple orgids in its allowed-orgs array. When that User signs in, the JWT itself is org-agnostic; the orgid header on each subsequent call decides which tenant they're acting on. AppEngine enforces that the org in the header is one the User is permitted on — anything else 403s.
For Customers, the binding is one-to-one: a Customer record exists in exactly one org. They can't operate cross-tenant, by design.