Documentation

Client apps

Multi-tenant client apps — provisioning, configuration, and lifecycle for SaaS extensions.

A client app is a separate tenant inside an org's namespace — its own configuration, its own users, its own URL. Use client apps to run multiple distinct SaaS products, embedded extensions, or per-customer white-labels off a single AppEngine deployment without spinning up new orgs. The Client Apps module at /client-app/* exposes CRUD over these tenant records.

What a client app is

A client app sits one level above a site and one level below an org:

  • Org — your top-level tenant (your platform). One org per business.
  • Client App — a configured product/tenant inside the org. Multiple per org.
  • Site — a deployable web property serving a client app. Multiple per app.

For a typical SaaS where every customer is a User and the storefront serves all of them, you don't need client apps — one site at one domain is enough. For a platform that hosts multiple distinct products under one corporate identity (e.g. an agency running 50 client micro-SaaS, or a parent company with a portfolio of brands), client apps map cleanly.

Surface

GET/client-appJWT
GET/client-app/:idJWT
POST/client-appJWT
PUT/client-app/:idJWT
DELETE/client-app/:idJWT

Standard REST. GET /client-app accepts a ?status= filter; GET /client-app/:id returns one. Permissions follow the platform's RBAC: read, create, update, delete on the content type.

const app = await fetch('/api/client-app', {
  method: 'POST',
  headers: { orgid: ORG_ID, Authorization: `Bearer ${jwt}` },
  body: JSON.stringify({
    name: 'Acme Tools - Client A',
    appCode: 'acme-clienta',
    status: 'active',
    config: {
      brandColor: '#cc0000',
      logo: 'https://cdn.../clienta-logo.svg',
      domain: 'tools.clienta.com',
    },
    features: ['storefront', 'crm', 'support-tickets'],
  }),
}).then(r => r.json());

Tenant isolation

Client apps within an org share the underlying data layer but use the appCode as a filter on every record. Application code (or the platform's automatic scoping) appends where: { appCode: 'acme-clienta' } to every query, so data created in one client app isn't visible in another.

This is a softer boundary than org-level tenancy:

  • Org boundary — enforced at the auth-and-data layer. Organisation A literally cannot read organisation B's data.
  • Client app boundary — enforced by application convention. A bug in your code that omits the appCode filter would cross the boundary.

For PHI/PII-sensitive multi-tenancy, prefer separate orgs. For brand/UX multi-tenancy where the operator team is shared, client apps work.

Configuration

The config field on a client app is a free-form JSON blob — branding, feature flags, custom copy, integration overrides. The platform's site rendering layer reads config when a request hits a domain mapped to a client app and applies it to the response (theme tokens, logo, favicon).

Common config keys:

  • brandColor, logo, favicon — visual branding
  • domain — the primary public hostname for the app
  • supportEmail — destination for support ticket creation
  • disabledFeatures — list of platform features to hide from this app's users
  • customCSS / customJS — inline overrides applied to served pages

Wiring a domain

A client app doesn't have its own domain registration. It points at one or more sites:

  1. 1

    Create the client app

    POST /client-app as above. Note the id and appCode.

  2. 2

    Create or claim a site

    POST /site/add (see Site and deployment). Pass clientAppId so the site is associated.

  3. 3

    Attach a domain to the site

    POST /site/attach-domain with the domain. The site's container starts serving on that domain with the client app's branding applied.

Updating

PUT /client-app/:id accepts a partial — send only the fields you're changing. The platform writes a modifyuser and modifydate so changes are auditable.

Deletion

DELETE /client-app/:id is a soft delete by default — the record is marked state: deleted so historical data attribution still resolves. Sites attached to the app become orphaned (still serving, but not associated with a live app); the platform's site management UI surfaces these for cleanup.

For a hard delete (purge data and tear down sites), pair with the org-management delete endpoint or use the data-layer's purge utilities.

When not to use client apps

  • Single SaaS, multiple customers — your customers are Customer principals; you don't need client apps. One site, one product.
  • Multi-region of the same product — use multiple sites, not multiple client apps. Region is a routing concern, not a tenancy concern.
  • A/B testing — same site, different feature flags via the rule engine. Don't fork into a new client app for an experiment.

The client-app abstraction is most useful for agency-style platforms and white-label resellers. If you're building a single product, ignore this module entirely — the platform works fine without it.

Related