Tenancy Domain
Manages platform-level configuration and the multi-tenant hierarchy for the PlayTelly platform: general holds ticketing tenant settings — site branding, legal content pages (privacy policy, T&Cs, FAQ, contact us, refund policy), third-party API credentials (Zoo API for QR scanning, JohorPay for payments), and email server configuration — while Organizations and Workspaces provide the platform-wide tenant, membership, and app-access model used by CoreAPI itself. As of 2026-09-14, general is no longer a single global row — it's one row per workspace, lazily created on first access. Admin writes require the real per-request permission workspace:general-settings:manage on that specific workspace (not a flat SYSADMIN role); the content-page GETs stay public and unauthenticated for the customer app, and still resolve via the single pinned SPATIO_TICKETING_WORKSPACE_ID since ticketcms has no per-request workspace concept.
Handles:
- General settings — contact email, timezone, environment config
- Site logo — public image retrieval
- Legal content pages — privacy policy, T&C, FAQ, contact us, refund policy (multilingual: BM/EN/CN)
- Integration settings — Zoo API and JohorPay credentials, email server config
- Organizations — tenant creation, self-signup with approval, membership, roles, app access
- Workspaces — per-org sub-units for membership, app access, spaces, and channels
- Teams — org-scoped user groups that can be granted a role on a workspace as a unit
- Webhook configuration (planned)
Two tenancy models
This domain covers two independent config surfaces that happen to share a package: the legacy ticketing general record documented below (single-row, ticketing-specific), and the platform-wide organization/workspace hierarchy documented on their own pages. They don't reference each other.
Architecture
graph LR
CustomerApp["Customer App (public reads)"]
AdminPortal["Admin Portal (workspace:general-settings:manage writes)"]
CoreAPI["CoreAPI (Tenancy handlers)"]
DB["Ticketing DB\n(general — one row per workspace)"]
CustomerApp -->|GET /api/settings/privacyPolicy\n/termsOfService\n/faq etc.| CoreAPI
AdminPortal -->|GET /PUT /api/settings/:workspaceId/general| CoreAPI
AdminPortal -->|PUT /api/settings/:workspaceId/integration| CoreAPI
CoreAPI --> DB
The general table used to be a genuine single-row config store; since 2026-09-14 it's one row per workspace, created lazily with zero-value defaults the first time a workspace's settings are read or written.
Data Model
erDiagram
General {
uint generalId
string organizationId
string workspaceId
string contactEmail
string timeZone
string envKey
string envValue
string privacyPolicyContentEn
string privacyPolicyContentBm
string privacyPolicyContentCn
string termsOfPurchaseContentEn
string termsOfServiceContentEn
string faqContentEn
string contactUsContentEn
string refundPolicyContentEn
string zooApiBaseUrl
string jpGatewayUrl
string emailHost
int emailPort
}
Endpoints
Public (no auth required)
GET /api/settings/general/siteLogo
Returns the tenant's site logo image file.
Response 200 — image file with appropriate Content-Type.
GET /api/settings/privacyPolicy
Response 200
{
"success": true,
"data": {
"contentEn": "<html>...",
"contentBm": "<html>...",
"contentCn": "<html>...",
"lastUpdatedDate": "01-06-2026"
}
}
GET /api/settings/termsOfPurchase
Same shape as privacy policy — terms of purchase multilingual content.
GET /api/settings/termsOfService
Terms of service multilingual content.
GET /api/settings/faq
FAQ multilingual content.
GET /api/settings/contactUs
Contact us multilingual content.
GET /api/settings/refundPolicy
Refund policy multilingual content.
Admin — requires SYSADMIN role
GET /api/settings/general
Get the full general settings record including all API credentials and email config.
Sensitive fields
This response includes jpApiKey, jpAgToken, emailPassword, emailClientSecret, and emailRefreshToken. Never expose this endpoint to non-admin clients.
Response 200 — full General object.
PUT /api/settings/general
Update general settings (non-integration fields: contact email, timezone, logo image upload).
Request — multipart/form-data with optional logo file + JSON fields:
{
"contactEmail": "support@example.com",
"timeZone": "Asia/Kuala_Lumpur"
}
PUT /api/settings/privacyPolicy
Update privacy policy content in all three languages.
Request
{
"contentEn": "<html>Updated privacy policy...</html>",
"contentBm": "<html>Dasar privasi dikemaskini...</html>",
"contentCn": "<html>更新的隐私政策...</html>",
"lastUpdatedDate": "23-06-2026"
}
Same shape applies for PUT /termsOfPurchase, PUT /termsOfService, PUT /faq, PUT /contactUs, PUT /refundPolicy.
PUT /api/settings/integration
Update third-party API credentials and email server config.
Request
{
"zooApiBaseUrl": "https://zoo-api.example.com",
"zooQrEndpoint": "/qr/scan",
"zooTokenEndpoint": "/auth/token",
"zooTicketEndpoint": "/tickets/validate",
"zooApiUsername": "zoo_user",
"zooApiPassword": "zoo_pass",
"jpGatewayUrl": "https://jp.gateway.com",
"jpPaymentEndpoint": "/payment",
"jpRedflowEndpoint": "/redflow",
"jpBankListEndpoint": "/banks",
"jpApiKey": "<key>",
"jpAgToken": "<token>",
"emailHost": "smtp.gmail.com",
"emailPort": 587,
"emailUsername": "noreply@example.com",
"emailPassword": "<password>",
"emailFrom": "noreply@example.com",
"emailUseSsl": true,
"emailClientId": "<oauth-client-id>",
"emailClientSecret": "<oauth-client-secret>",
"emailRefreshToken": "<oauth-refresh-token>"
}
Content Pages
All legal content pages follow the same pattern:
| Endpoint | GET |
PUT (SYSADMIN) |
|---|---|---|
/api/settings/privacyPolicy |
Public read | Update content |
/api/settings/termsOfPurchase |
Public read | Update content |
/api/settings/termsOfService |
Public read | Update content |
/api/settings/faq |
Public read | Update content |
/api/settings/contactUs |
Public read | Update content |
/api/settings/refundPolicy |
Public read | Update content |
All content is stored as raw HTML strings and served verbatim. The customer app renders them in a WebView.