Workspaces, brands, and contacts
- Written for
- + Written for
- Deprecated
- + Deprecated
- Applies to
- + Applies to
Workspaces, brands, and contacts
The tenancy model in Chatly — what isolates what, and why every read in the platform sits behind two locks.
Chatly's tenancy model is three nested ideas: workspace (root tenant), brand (optional sub-tenant), contact (end-user). They shape how every page query is scoped, how RLS is enforced, and how multi-customer companies model their setup.
Workspace
A workspace is the tenant root. Everything that matters — agents, contacts, conversations, channels, KB, AI configuration, integrations, billing — lives inside exactly one workspace.
Info — Workspace is the strongest isolation boundary in Chatly
Two workspaces cannot see each other's data. Period. Application-level filtering AND Postgres Row-Level Security both enforce
workspace_id = $1on every tenant-scoped table.
Parameters
Name | Type | Description |
|---|---|---|
|
| A bare UUID, no |
|
| Display name (≤120 chars). Shown in the dashboard header and OAuth consent screens. |
|
| URL-safe identifier, unique platform-wide (≤64 chars). |
|
| Exactly five values: |
|
| Defaults for new contacts and for business-hours arithmetic. |
|
| Per-day open/close plus holidays. Its own column rather than a settings key, because the trigger normalizer reads it on every conversation event. |
|
| Workspace-level display and configuration blobs. |
|
| Platform-operator suspension. |
|
| Lifecycle. |
|
| Soft-delete. See Workspace deletion — the 30-day window is real, the automatic purge is not. |
Warning — There is no
regionfield on a workspaceNothing in the API returns or accepts a workspace region, and no code reads one. See Multi-region below for what actually exists.
Plans and limits
What each plan actually enforces
Name | Type | Description |
|---|---|---|
|
| Including |
|
| Contacts are deliberately not metered. |
|
| free 1, starter 3, growth 10, business and enterprise unlimited. This is the one resource cap actually wired to a route — creating past it is refused. |
|
| free 1, starter 5, growth 25, business 100, enterprise unlimited. |
|
| free 500, starter 10k, growth 50k, business 250k, enterprise unlimited — but enforcement sits behind a flag that defaults to OFF, so nothing is refused today. |
|
| Monthly ceilings per plan on metered LLM cost. |
GET /v1/workspaces/me/seats — Bearer token
GET /v1/workspaces/me/limits — Bearer token
Warning —
/limitsis not about seats or contactsIt answers a different question: how many workspaces may this user own (free 1, starter 2, growth 5, business 10, enterprise unlimited). It returns
owned,allowance(null = unlimited),planAtBestTier, andcanCreate. For seat usage, use/seats, which returnsused,limit,grandfathered, andplan.
Members + roles
A workspace has zero or more members (agents). Each member has a role and (optionally) belongs to one or more teams. See RBAC for the role matrix.
Joining a workspace happens via:
Leaving a workspace soft-deletes the membership row (deleted_at) so audit trails stay intact. Removal is DELETE /v1/memberships/{id} and requires members:remove — an owner-only permission.
Brands (multi-brand workspaces)
A brand is an optional sub-tenant inside a workspace, for businesses that serve multiple customer-facing products from one support team.
Use brands when you have:
Multiple consumer brands (e.g. a parent company with several product lines)
A primary product + a sister marketplace
Reseller arrangements where each downstream brand has its own customer base
The brands table is thin. What a brand row actually holds:
Parameters
Name | Type | Description |
|---|---|---|
|
| Display + identifier. Slug is unique per workspace. |
|
| One optional hostname (≤253 chars). A single field, not a set of per-purpose domains. |
|
| Logo URL, primary colour, support email, icons. |
|
| Every workspace is created with one default brand, which is why |
What a brand scopes, by other tables pointing at it:
Channels and conversations —
brand_idis required on both.Triggers, workflows, and AI bot routes — each carries its own optional
brand_id, which is how "Brand A's conversations don't get Brand B's automation" is actually expressed.Help centres —
help_centers.brand_id, so each brand can front its own KB.
Warning — No per-brand email DNS
There are no SPF / DKIM / DMARC columns on a brand, and no sending-domains table. Email authentication is a property of the BYOK email provider you connect on the channel, not of the brand.
What a brand shares with sibling brands in the same workspace:
Agents + teams (one team can support multiple brands)
Billing + plan tier (one Chatly invoice per workspace)
Integrations (one Stripe, HubSpot, Salesforce connection — agents who switch brands still use the same CRM)
Tip — Don't use brands for environments
"Production" + "staging" + "dev" should be separate workspaces, not separate brands inside one. Brands share agents and billing; that's not what you want across environments.
Contacts
A contact is an end-user — someone who messages you, not someone who answers messages. Contacts are unified across channels: if the same person reaches you via web chat and later via WhatsApp, both threads merge under one contact record.
See Contacts for the full identity stitching story.
Conversations belong to a contact + a channel + (optionally) a brand
A conversation is the thread. It lives inside a workspace, is owned by a contact, came in on a channel, and (in multi-brand setups) belongs to a brand. See Conversations.
Workspace deletion
Deleting a workspace soft-deletes: deleted_at is set, the workspace disappears from the dashboard, and there is a 30-day grace window during which a platform operator can restore it. Past 30 days, restore is refused.
Warning — The purge after 30 days is manual, not automatic
There is no scheduled job that enumerates soft-deleted workspaces and hard-deletes them. The purge is a platform-operator action (
POST /v1/admin/workspaces/{id}/purge), and it refuses to run inside the 30-day window unless explicitly forced. Until someone presses it, a soft-deleted workspace sits withdeleted_atset. Plan your compliance commitments around that, not around an automatic sweep.
The purge itself is a single DELETE FROM workspaces, relying on ON DELETE CASCADE to take every dependent row with it.
Danger — Hard-delete is irreversible
Once a purge completes, there is no recovery. Export your data first if you might want it later.
Data retention
Retention is per workspace, per scope, and opt-in. A workspace with no policy configured keeps everything forever — there is no platform default and no blanket 90-day window.
GET /v1/retention/scopes — Bearer token
GET /v1/retention/policies — Bearer token
PUT /v1/retention/policies/{scope} — Bearer token
DELETE /v1/retention/policies/{scope} — Bearer token
Parameters
Name | Type | Description |
|---|---|---|
|
| One of |
|
| 1 to 3650. Rows older than this are swept. |
|
|
|
|
| Master switch per policy. |
The sweep runs every six hours and processes up to 5,000 rows per policy per run, so a large first sweep drains over several passes. lastRunAt and lastDeletedCount on the policy are how you confirm it is actually running.
Multi-region
Warning — Multi-region is not in production
Chatly does not currently run a multi-region deployment. There is no region field on a workspace, no per-region data pinning, and no data-residency guarantee derived from one. Treat any EU-residency requirement as something to satisfy by self-hosting in the geography you need, where you control where Postgres, Redis, and object storage live.
What does exist is a per-workspace regions table — a list of (code, label, apiUrl, realtimeUrl) entries, managed at /v1/regions. It is endpoint selection, not data residency: it changes which realtime URL the widget dials, and nothing else. Do not read it as a residency control.
Self-hosted deployments are single-region by definition; you control the geography.