Five ideas explain most of AppEngine. Learn these and the 2,400-odd endpoints stop looking arbitrary.
1. The organization is the tenant
Every request carries an orgid. AppEngine resolves it to a dedicated MongoDB database — the org record holds a data.database field, and getDatabase(orgId) looks it up and caches the connection (repository.provider.mongodb.ts).
Tenants are therefore isolated at the database level, not by a tenantId column. Two orgs can hold a customer collection with colliding keys and never see each other.
A request with no resolvable orgid is rejected before it reaches a controller, with 400 and code: "missing_orgid". The only exceptions are /health, /readiness, /favicon.ico, OAuth callbacks under /connect/oauth2callback/, /connect/webhook/*, and two pre-signup /org-management paths.
/repository/get/customer/12342. Everything is a datatype
There are no bespoke tables. Every record in the system is one of ~261 datatypes, enumerated in DataType (@jaclight/dbsdk). A datatype is literally the MongoDB collection name.
They cluster into recognizable families:
| Prefix | Family | Examples |
|---|---|---|
| (none) | Core content and identity | page, site, collection, user, customer, file, task, setting |
sf_ | Storefront / commerce | sf_product, sf_order, sf_cart, sf_invoice, sf_discount, sf_rental |
bm_ | HR, payroll and books | bm_employee, bm_payroll_run, bm_leave_request, bm_bill, bm_vendor |
community_ | Social layer | community_post, community_follow, community_story, community_reaction |
event_ | Events and ticketing | event_booking, event_ticket, event_session, event_checkin |
bank_, ledger_, loan | Banking and finance | bank_account, bank_transfer, ledger_entry, journal_entry, loan_payment |
stowbo_ | Storage marketplace | stowbo_listing, stowbo_unit, stowbo_booking |
Because the datatype is a URL segment, one generic controller serves all of them:
/repository/get/sf_product/{id}/repository/search/sf_product/repository/create/repository/delete/sf_product/{id}The full list is in the datatype reference.
3. Every record is a BaseModel<T>
Records share one envelope. Your domain fields live under data; everything else is platform machinery.
interface BaseModel<T> {
pk: string; // partition key
sk: string; // sort key — this is the id you pass to /get/:datatype/:id
name: string;
data?: T; // your fields
datatype?: DataType | string;
subschema?: string; // a variant within a datatype
version: number;
state?: ModelState; // draft | pending | approved | published | archived | deleted | …
createdate?: Date;
modifydate?: Date;
publishedDate?: Date;
author?: string;
requiredRole?: RequiredRoleModel; // per-record read/create/update/delete/review/approve roles
workflow?: TaskModel;
stats?: BaseModelStats; // likes, views, shares, ratings, reactions
share?: BaseModelShare; // tokenized public share link
owner?: { datatype?, id?, name?, email? };
notes?: { author, comment, date }[];
created_by?: string;
modified_by?: string;
}Two consequences worth internalizing:
skis the id. Not_id, notdata.id. Endpoints that take:idtake thesk.- Publishing, versioning, approval, sharing and social stats are free. They are on every datatype because they are on
BaseModel, not bolted onto individual features.
ModelState runs draft → new → pending → inprogress → reviewed → approved → published → completed, with hold, rejected, cancelled, archived and deleted as terminal states.
4. Users and customers are different identities
This is the distinction that trips people up most.
| User | Customer | |
|---|---|---|
| Who | Operates the tenant — staff, admins, builders | An end user of the tenant's site or app |
| Datatype | user | customer |
| Signs in at | /profile/* operator routes | /profile/customer/signin, /profile/customer/signup |
| Token marker | datatype absent or user | datatype: "customer" in the JWT |
| Sees | The org's whole workspace, subject to roles | Their own records |
CurrentUserMiddleware decodes the bearer token, branches on tokenInfo.datatype, and populates request.currentUser — plus request.currentCustomer when the identity is a customer.
There is a third mode. A System user (an integration or service account) can act on behalf of a customer by sending both tokens:
Authorization: Bearer <system-user-token>
x-client-authorization: Bearer <customer-token>
orgid: acmeFor repository writes, JwtAuthGuard will then evaluate permissions against the customer, and stamp the customer as author on created records. Omitting x-client-authorization on such a write returns 401 — No customer found.
5. Roles gate content and components
RoleType has fifteen values: Guest, User, Customer, Owner, Publisher, Reviewer, PowerUser, ContentAdmin, ConfigAdmin, System, AI (an AI employee's user), and the Root* variants (RootUser, RootPowerUser, RootAdmin, RootSystem).
Each role expands to two permission sets — content (what records you may touch) and component (what UI you may load) — plus menu include/exclude lists that the Studio, Business Made and the mobile app use to build navigation. The sign-in profile carries each role's menu separately under data.permissions.menu — see Menu access.
Authorization is checked in layers, in this order (JwtAuthGuard):
1. Public route
@PublicRoute() short-circuits everything.
2. Own profile
Reading or writing your own user record is always allowed.
3. Record ownership
On repository/create|update|delete, if data.author matches the caller, allow.
4. Per-record roles
If the record carries requiredRole.update / .delete, the caller needs one of those content permissions.
5. Decorator requirements
@Permissions(...) and @Roles(...) on the handler. RootSystem bypasses the role check.
6. Token validity Finally the Passport JWT strategy runs.
Next
Your first request puts all five together against a live org.