Zensile Documentation

Introduction

Zensile is a gym management platform: member check-ins, company/staff management, role-based access control, scheduling, POS/checkout, payroll, and third-party integrations (Stripe, Mindbody, KISI). It ships as a Next.js 15 app (this codebase, zensile-app) backed by a MySQL/REST API, plus an Expo mobile/ app — both consuming a shared, UI-free TypeScript domain layer, zensile-sdk.

What's in this section

This is reference documentation for the software itself — architecture, UI conventions, and the full domain-layer API — written for anyone extending or integrating with Zensile. It's split into two parts:

PartPagesCovers
Application3 pagesHow the Next.js app is put together: routing, the component library, and the display-panel/CRUD conventions used throughout.
SDK Reference11 pagesEvery domain class in zensile-sdk — fields, methods, endpoints — organized by business area (members, scheduling, payroll, …).

Use the sidebar to jump to a topic, or the prev/next links at the bottom of each page to read in order.

Architecture

                    ┌─────────────────────────┐
                    │   zensile-app (Next.js)  │
                    │   mobile/ (Expo)          │
                    │   — UI only, no business  │
                    │     logic                 │
                    └───────────┬───────────────┘
                                │ imports zensile-sdk/*
                    ┌───────────▼───────────────┐
                    │   zensile-sdk              │
                    │                             │
                    │  Domain classes             │
                    │  (Member, Checkout, Class…) │──── extend DataStructure<T>
                    │       │                      │
                    │       ▼                      │
                    │  APIRequest<T>() (helper.ts) │──── auth, retry, timeout,
                    │       │                      │      MySQL DATETIME rewrite
                    │       ▼                      │
                    │  fetch() → REST API          │
                    └───────────┬───────────────────┘
                                │ HTTP (cookie session)
                    ┌───────────▼───────────────┐
                    │  MySQL/REST backend (AWS)   │
                    └─────────────────────────────┘

The UI layer (zensile-app, mobile/) holds no business logic — it renders components and calls methods on domain classes. All of that logic lives in zensile-sdk, checked into this repo at zensile-sdk/ and aliased as the zensile-sdk/* import path. See the SDK reference pages for the domain layer, or the Using Zensile guides for how the app looks and works day to day.

Core SDK concepts

DataStructure<T>

The abstract base class every domain entity (Member, Checkout, Class, …) extends. It provides:

  • A per-class static cache: Map<string, T> keyed by id
  • Lazy vs. full data loading — data(getFullIfLazy?) returns cached lazy data unless a full fetch is explicitly requested
  • LazyData — synchronous getter for whatever's currently cached (may be undefined, may be partial)
  • reload() — discards cache and refetches full data; invalidate() — clears cache without refetching
  • A subscriber system: subscribe(fn) fires on every setData() call, and auto-triggers the initial fetch — this is what the app's useData/useMany React hooks build on via useSyncExternalStore

Subclasses must implement cache, fetch(lazy?), save(), and delete(). create() and query() are conventional statics, not enforced by the base class.

Never mutate _data in place:Always go through setData({ ...this._data, field: newValue }) so subscribers see a changed reference:
ts
// Wrong — no notification, useSyncExternalStore sees the same reference
this._data.quantity += amount;

// Correct — new reference, subscribers notified
this.setData({ ...this._data, quantity: newQty });

APIRequest<T>()

The single choke point for all HTTP calls, in helper.ts:

ts
APIRequest<T>(
  endpoint: string,
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
  body?: any[] | any,
  needUser = true,
): Promise<SimpleResult<T> | EventData<T>>
  • Reads the ss-user-id cookie; if needUser and it's missing, short-circuits with 401 before any network call
  • Refreshes the auth token first if it's expired, and retries once on a 401 mid-flight
  • Every request times out after 20 seconds, returning a normal 408 instead of hanging
  • Rewrites ISO datetime strings/Date values in the request body into MySQL's DATETIME format before sending

SimpleResult<T> / EventData<T>

The standard response envelope every call resolves to — always check status before trusting data:

ts
interface SimpleResult<T> {
  message?: string;
  status: number;   // HTTP status code
  data?: T;
  total?: number;   // present on list/query responses
}

EventData<T> extends this with type, a required message, and timestamp — the audit-trail fields the backend attaches to every write.

QueryRequest<T>

The shape accepted by every class's static query():

ts
interface QueryRequest<T extends string = string> {
  filter?: Array<{ field: T; condition: FilterCondition; value: string | number | string[] | Date }>;
  logic?: "and" | "or";
  sort?: { field: T; order: "asc" | "desc" };
  limit?: number;
  offset?: number;
  lazy?: boolean;
  includeDeleted?: boolean;
}

FilterCondition = "=" | "!=" | "<" | ">" | ">=" | "<=" | "like" | "not like" | "in". Use "in" with a string[] to match a set of values.

ts
const result = await Member.query(company, {
  filter: [{ field: "userEmail", condition: "like", value: "%@gym.com" }],
  sort: { field: "lastName", order: "asc" },
  limit: 25,
});
if (result.status === 200) {
  console.log(result.data, result.total);
}

CheckInResponse

Shared by all three check-in endpoints (CreateVisits, Booking.CheckIn, Appointment.CheckIn):

ts
interface CheckInResponse {
  message: string;
  statusCode?: 200 | 202;
  alert?: string;
  alerts?: string[];
}
Note:status === 202 means the check-in succeeded but the member has an alert (outstanding balance, expired waiver, …) that must be surfaced to the operator — never treat 202 as failure.

Auth & permissions

Auth is purely cookie-based (ss-user-id, ss-company-id, ss-token-expire) — there's no separate auth provider. See Auth & Session for the full session lifecycle.

Permissions are bitwise flags (READ=1, EDIT=2, CREATE=4, REMOVE=8, ALL=15) scoped across 16 modules (Members, Visits, Scheduling, Checkouts, …). See Management for Role/Permission and the module list.

Conventions

  • Always call instance/static methods on domain classes (company.GetListOfUsers(), member.reload()) rather than importing standalone functions directly.
  • Never mutate _data in place — go through setData({ ...this._data, ... }).
  • Related-entity getters (e.g. line.product) must cache the constructed instance in a private field so the same object reference is returned every call — required for subscriptions to work.
  • Parent → children relationships are a thin instance method on the parent wrapping the child's query() filtered by the parent's id (e.g. coupon.promotionalCodes(opts), staff.rates(opts)).
  • Check-in endpoints return 202 for a successful check-in with an alert — never treat it as an error.

Where to go next