SDK Reference

Auth & Session

Covers user.ts (User), cookie_manage.ts (cookie primitives), simple.ts (session helpers), and sandbox.ts (disposable test companies). See the Introduction for DataStructure<T>, SimpleResult<T>, EventData<T>, and QueryRequest<T>, which User builds on.

User (user.ts)

The account-level login — email/password credentials, a global profile, and cross-company query methods. This is distinct from Member (company/member/member.ts, see Members): a User is one row in the top-level users table; a Memberis that account's (or an unlinked invitee's) profile row within a single company. A User may have zero, one, or many Memberrows (one per company it's joined); Member.userID is the optional link back. Member has no roleID — company staff/role assignment is a Staff concern (see Company), not a User or Member field.

Data fields

User extends DataStructure<UserData>, cached in a static Map keyed by id like every other domain class.

FieldTypeNotes
idstring
emailstring
firstName?string
lastName?string
isAdmin?booleanpresent on LazyUserDataalready — no separate "full data" gate for it
stripeCustomerID?string
mobilePhone? / homePhone? / workPhone?string
addressLine1? / addressLine2? / city? / state? / country? / postalCode?string
birthday?stringdate-only; save() normalizes it to YYYY-MM-DD via toISOString().split("T")[0] before sending
gender?MemberGendershared enum from types.ts ("male"|"female"|"non_binary"|"other"|"prefer_not_to_say") — same type Member uses, use MemberGenderOptions for display labels
emergencyName? / emergencyRelationship? / emergencyPhone? / emergencyEmail?string
photo?stringstorage key, not a URL — resolve via GetPhoto()
createdAt / updatedAtDatefull-data only

LazyUserData is just { id, email, firstName?, lastName?, isAdmin? } — the subset returned by lazy fetches.

Constructing

ts
new User(id: string);                              // lazy — triggers preloadData(true)
new User(data: LazyUserData | UserData, lazy?);     // pre-seeded, e.g. from a query result

Static methods

ts
User.create(data: CreateUserRequest): Promise<SimpleResult<EventData>>

CreateUserRequest = { firstName?, lastName?, email, password }. Hits POST /auth/register with needUser: false (no session required — this is how a session begins). Does not log the caller in — follow with User.Login().

ts
User.Login(data: LoginUserRequest): Promise<boolean>

LoginUserRequest = { email, password }. POST /auth/login, needUser: false. On status === 200 with a returned expireTime, stores it via SetCookie("ss-token-expire", ...); with a returned id, also stores it via SetCookie("ss-user-id", ...) (see the cookie-origin note below). Returns a plain boolean, not a SimpleResult — check the return value, not a status code.

Instance methods

ts
user.save(): Promise<SimpleResult<EventData>>                       // PATCH /users
user.delete(): Promise<SimpleResult<EventData>>                     // DELETE /users?ids={id}
user.SetPassword(newPassword: string): Promise<SimpleResult<EventData>>  // PATCH /users/password, no current password required
ts
user.UploadPhoto(file: File | Blob, contentType = "image/jpeg"): Promise<SimpleResult<EventData>>
user.GetPhoto(): Promise<string | undefined>

Requests a presigned URL from POST /users/photo ({ type: "user-photo", objectID, contentType }), PUTs the file, then saves the returned key onto photo via save(). GetPhoto() re-posts to the same /users/photo endpoint with { key } and returns data.publicUrl. This is an account-level, bespoke endpoint — distinct from the generic MediaResourceType system (helper.ts's UploadMedia/DownloadMedia) used for companies, members, products, etc. (see the Introduction's Media Uploads section).

ts
user.GetListOfCompanies(): Promise<SimpleResult<Company[]>>

GET /users/companies?userID={id} — every company this account has a Member row in, i.e. what the account /dashboard lists to switch into.

ts
user.idLink(): Promise<string | undefined>

POST /users/idLink— issues a short-lived token for the account's digital-ID/QR check-in flow.

Cross-company query methods

Seven methods query this account's records across every company it belongs to, via account-level /users/{resource}/queryendpoints, then wrap each raw row in its domain class scoped to the row's own companyID:

ts
user.notifications(opts: { limit?, offset?, lazy?, isRead? }): Promise<{ status, data: Notification[], total?, unreadCount? }>
user.visits(opts: QueryRequest): Promise<{ status, data: Visit[], total? }>
user.invoices(opts: QueryRequest): Promise<{ status, data: Invoice[], total? }>
user.subscriptions(opts: QueryRequest): Promise<{ status, data: Subscription[], total? }>
user.checkouts(opts: QueryRequest): Promise<{ status, data: Checkout[], total? }>
user.payments(opts: QueryRequest): Promise<{ status, data: Payment[], total? }>
user.bookings(opts: QueryRequest): Promise<{ status, data: Booking[], total? }>
user.appointments(opts: QueryRequest): Promise<{ status, data: Appointment[], total? }>

Each POSTs to its own /users/{resource}/query endpoint, then does result.data?.filter((r) => r.companyID).map((r) => new X(r, new Company(r.companyID))) — rows with no companyIDare silently dropped since they can't be constructed into their domain class. notifications is the odd one out: GET-style options object instead of a QueryRequest, and it also returns unreadCount.

Cookie session management (cookie_manage.ts)

Three primitives, all guarded for SSR:

ts
SetCookie(name: string, val: string): void      // 7-day expiry, path=/
GetCookie(name: string): string | undefined
DeleteCookie(name: string): void                 // expires immediately (sets expiry to yesterday)

Default implementation reads/writes document.cookie directly, and every function checks typeof document === "undefined" first — SetCookie/DeleteCookie silently no-op on the server, GetCookie returns undefined. This is what makes the same session code safe to import in both Next.js server and client contexts without crashing at import/SSR time.

Non-browser environments

ts
interface CookieAdapter {
  get(name: string): string | undefined;
  set(name: string, val: string): void;
  remove(name: string): void;
}
SetCookieAdapter(adapter: CookieAdapter): void

For environments with no document at all (e.g. React Native/Expo), install a CookieAdapter once, before any other zensile-sdk/ code touches cookies — once set, all three functions delegate to the adapter instead of document.cookie, bypassing the SSR guard entirely.

Cookie names used across the SDK

CookieSet byRead byPurpose
ss-user-idUser.Login()GetActiveUser(), APIRequest's needUser gateIdentifies the signed-in account
ss-company-idcompany.SetActiveCompany()GetActiveCompany()Identifies the currently-selected company
ss-token-expireUser.Login()EnsureToken() (helper.ts)Drives proactive token refresh

Session helpers (simple.ts)

ts
GetActiveUser(): Promise<User | undefined>

Reads ss-user-id; if present, returns a memoized User instance (module-level currentUservariable, recreated only if the cookie's id changes from the last call). Returns undefined if unset, or on any thrown error (caught and logged).

ts
LogoutActiveUser(): boolean

DeleteCookie("ss-user-id") then also calls LogoutOfActiveCompany() — clears the account session and any active company session together. Returns false (instead of throwing) on error.

ts
GetActiveCompany(): Promise<Company | undefined>

Same memoization pattern as GetActiveUser(), keyed off ss-company-id (module-level activeCompany variable).

ts
LogoutOfActiveCompany(): boolean

DeleteCookie("ss-company-id") only — leaves the account-level login intact. This is the one to call when a user backs out of a company back to the account dashboard without logging out entirely.

ts
interface MeData {
  memberID: string;
  roleID: string | null;
  firstName: string;
  permissions: Permission[];
}
getMe(companyID: string): Promise<MeData>

GET /company/{companyID}/members/me— resolves the signed-in user's Member/Staff identity within one company: their memberID, roleID (null if they're a plain member with no staff/role grant), display firstName, and resolved Permission[] (see Management's role.ts and nav.tsx's loadPermissions() in the app for how this feeds nav gating). Requires authentication (ss-user-id cookie) — unlike GetActiveUser()/GetActiveCompany(), it throws (Error(result.message ?? "getMe failed.")) on a non-200/no-data response rather than returning undefined, so callers should wrap it in try/catch.

Why ss-user-id is set from the response body, not read from a cookie:The backend's own Set-Cookie response on /auth/login (ss-access-token, ss-refresh-token, etc.) lands in the browser under the API's own origin. When the web app is hosted on a different origin than the API (e.g. the Next.js app on Vercel calling a separately hosted backend), document.cookieon the app's own pages can never see those — cookie visibility is scoped to the responding origin, not attached across origins, regardless of SameSite/Secure. That's fine for the HttpOnly auth cookies — the browser attaches those automatically to future requests to the API domain — but ss-user-id is a client-side bookkeeping cookie this SDK reads directly via document.cookie (GetActiveUser(), APIRequest's needUser gate), so User.Login() writes it into the app's owncookie jar explicitly, from the response body's idfield, instead of assuming the backend's cross-origin cookie is visible. If a login appears to succeed but the app immediately bounces back to the login page, check whether ss-user-id actually landed in document.cookie (Application/Storage tab) versus only showing up in the /auth/login response's Set-Cookieheaders (Network tab) — the latter alone won't establish a session on web.

Typical login flow

Based only on what the code above actually supports — no invented convenience wrapper exists that chains these steps for you:

ts
import { User } from "zensile-sdk/user";
import { GetActiveUser, GetActiveCompany } from "zensile-sdk/simple";

// 1. Register (optional — skip straight to Login if the account exists)
const created = await User.create({ email, password, firstName, lastName });
if (created.status !== 200) throw new Error(created.message);

// 2. Authenticate — sets ss-token-expire and ss-user-id (see cookie-origin note above)
const ok = await User.Login({ email, password });
if (!ok) throw new Error("Login failed");

// 3. Resolve the now-active User
const user = await GetActiveUser(); // memoized, undefined if the cookie didn't take

// 4. List companies this account belongs to and select one
const { data: companies } = await user!.GetListOfCompanies();
await companies?.[0]?.SetActiveCompany(); // sets ss-company-id

// 5. Resolve the now-active Company, and this account's role/permissions within it
const company = await GetActiveCompany();
const me = await getMe(company!.id); // throws on failure — wrap in try/catch

Sandbox (sandbox.ts)

Disposable demo/test companies — seed data plus an admin login — for exploring the app without touching real data. Not a DataStructure subclass; all standalone functions.

ts
interface SandboxMember { name: string; email: string; }

interface SandboxData {
  companyID: string;
  adminEmail: string;
  adminPassword: string;
  expiresAt: string;
  members: SandboxMember[];
}
ts
CreateSandbox(): Promise<SimpleResult<SandboxData>>

POST /v1/sandbox (called via a raw fetch, not APIRequest — bypasses the needUser/retry/timeout machinery entirely, though it still sends credentials: "include"). Provisions a new sandbox company with seed data and an admin login for the current session.

ts
GetSandbox(): Promise<SimpleResult<SandboxData>>

GET /sandbox (via APIRequest, unlike the other four functions in this file) — fetches the current session's active sandbox, if any.

ts
DeleteUserSandbox(companyID: string): Promise<SimpleResult<void>>

DELETE /sandbox/{companyID} (via APIRequest) — deletes the calling user's own sandbox company.

Admin-only functions

ts
DeleteSandbox(secret: string, companyID: string): Promise<SimpleResult<void>>
CleanupSandboxes(secret: string): Promise<SimpleResult<void>>

Both bypass APIRequest and use raw fetch with an x-sandbox-secret: {secret}header instead of the normal cookie-session auth — this is the SDK's one deliberate escape hatch from the standard user-session model, for ops/admin tooling that manages sandboxes across all users. DeleteSandbox removes any sandbox company by id; CleanupSandboxes bulk-removes every expired sandbox in one call (POST /v1/sandbox/cleanup).

  • See Members for Member, distinct from the account-level User covered here.
  • See Management for Role/Permission and the module list that getMe()'s permissions resolves against.