docs
/
Getting started

Core concepts

Organizations, datatypes, BaseModel, and the difference between a user and a customer.

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.

orgid is not optional

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.

You can supply it four ways — header, query string, body field, or a cookie:

GET/repository/get/customer/1234

2. 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:

PrefixFamilyExamples
(none)Core content and identitypage, site, collection, user, customer, file, task, setting
sf_Storefront / commercesf_product, sf_order, sf_cart, sf_invoice, sf_discount, sf_rental
bm_HR, payroll and booksbm_employee, bm_payroll_run, bm_leave_request, bm_bill, bm_vendor
community_Social layercommunity_post, community_follow, community_story, community_reaction
event_Events and ticketingevent_booking, event_ticket, event_session, event_checkin
bank_, ledger_, loanBanking and financebank_account, bank_transfer, ledger_entry, journal_entry, loan_payment
stowbo_Storage marketplacestowbo_listing, stowbo_unit, stowbo_booking

Because the datatype is a URL segment, one generic controller serves all of them:

GET/repository/get/sf_product/{id}
POST/repository/search/sf_product
PUT/repository/create
DELETE/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:

  • sk is the id. Not _id, not data.id. Endpoints that take :id take the sk.
  • 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.

UserCustomer
WhoOperates the tenant — staff, admins, buildersAn end user of the tenant's site or app
Datatypeusercustomer
Signs in at/profile/* operator routes/profile/customer/signin, /profile/customer/signup
Token markerdatatype absent or userdatatype: "customer" in the JWT
SeesThe org's whole workspace, subject to rolesTheir 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: acme

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