Documentation

Principals and tenancy

Users vs Customers, the orgid header, and how AppEngine isolates tenant data.

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.

PrincipalSign-in endpointRefresh endpointWhere they appear
UserPOST /profile/signin (alias /profile/user/signin)POST /profile/user/refreshStudio Manager, admin tooling, server-to-server
CustomerPOST /profile/customer/signinPOST /profile/customer/refreshStorefronts, member portals, mobile apps
POST/profile/signinNo auth
POST/profile/customer/signinNo auth

Both 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.

orgid is not optional

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

PatternHeadersWhen to use
Publicorgid onlyStorefront product browse, public contact forms, magic-link emails
User JWTorgid + Authorization: Bearer <jwt>Admin dashboards, Studio Manager, staff sessions
Customer JWTorgid + Authorization: Bearer <jwt>Storefronts, member portals — same header, different token
API keyorgid + x-api-key: <key>Server-to-server, build pipelines, scripts
Mixedorgid + bothFine — 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.