Architecture & Security
How Customermates is built, how workspace data is isolated, how API keys and MCP OAuth authenticate, what the Activity log records and how to report a flaw.
Each company is an isolated tenant in a shared PostgreSQL database. Every record belongs to one company, API keys and OAuth connections act as the user who created or authorized them, and AI agents see only what that user may see. One login can belong to several workspaces, and every session, API key and OAuth connection acts in exactly one of them; see Workspaces.
What is the stack?
- Next.js 16 (App Router, Turbopack): web app, REST API and MCP endpoint.
- PostgreSQL: primary datastore. JSONB holds field options and formats, notes, view and widget settings, webhook headers, webhook delivery payloads and audit entries.
- Prisma: ORM and migrations.
- better-auth: sessions, Google and Microsoft sign-in, API keys, MCP OAuth.
- mcp-handler: Model Context Protocol endpoint.
- Workflow: background jobs such as webhook deliveries; on a self-hosted instance its queue lives in the same PostgreSQL database.
- TypeScript end to end, Zod-validated at every boundary.
How does multi-tenancy keep each tenant isolated?
All companies share one database, and every record carries its company id. Isolation is enforced in three layers:
- Interactors: every business operation runs as the signed-in user and filters by that user's company.
- Query guard: a Prisma extension throws when a query on a workspace table lacks the current company id in its filter or names another company, and when a new row carries another company's id. Only the authentication tables are exempt (sign-in identities, sign-in accounts, sessions, verification tokens, API keys, and the MCP OAuth clients, tokens and consents), and a few explicitly marked system operations bypass the guard.
@TenantInteractor: resolves the active user, refuses inactive members, checks the role permission and opens the tenant context that the query guard reads.
A self-hosted instance can hold several workspaces side by side with the same isolation. Cross-tenant reads are not possible through the public surface. On the managed cloud a vendor operator console can read across tenants for support; it is limited to an allowlisted operator account, refuses to load outside cloud mode, and records every read and write in a separate internal log. A self-hosted deployment has no such console. The hosted assistant Mate runs in the same scope: it can read and change only what the asking user can.
How do clients authenticate?
Three ways, all resolving to the same user and company context:
- Session cookie (browser): signed, http-only,
Secureunder HTTPS. People sign in with email and password, or with Google or Microsoft where the instance has those providers configured. A new account can be used for 24 hours before its email address must be confirmed. Every request checks the session in the database, so after sign-out or a revoked session the cookie stops working at once, and an Inactive status blocks the workspace it is set in at once. The session also holds the workspace you switched to, so another browser can work in a different workspace. - API key: 64-character token of ASCII letters (
a-z,A-Z) issued by the better-auth API key plugin, stored only as a hash, tied to a user and the workspace it was created in, optionally expiring. Sent in thex-api-keyheader. See API keys. - OAuth 2.1 (MCP): remote MCP clients authorize through better-auth with PKCE and send a Bearer token instead of an API key. Access tokens live one hour, refresh tokens thirty days. A connection is bound to the workspace chosen on the consent screen, and so are the tokens it refreshes.
Does Customermates support SSO or SAML?
No. Enterprise single sign-on with your own identity provider over SAML or OIDC is not implemented, in the cloud or self-hosted. Members sign in with email and password, or with a Google or Microsoft account where the instance has those providers configured; on a self-hosted instance you set them up yourself, see Self-hosting. To end a member's access, open them in Members and set Status to Inactive.
How is authorization decided?
A role in Roles grants Create, Edit, Delete and Read access All, Assigned or None separately for each list, including lists you created. Editing and deleting require both the action permission and access to the record. Workspace permissions use the same columns, with a dash where an action does not apply. Edit on Data model is separate from record access and role administration: a schema manager cannot expand grants without Edit on Users & Roles. The built-in Admin role has every permission. Record and configuration writes recheck permissions and revisions inside the workspace transaction. API keys inherit their owner's current permissions; there is no per-key scoping.
How do I stop the API keys of a former member?
The API & Webhooks permission controls the API keys, Webhooks and Deliveries pages and the key and webhook operations behind them. MCP connections and REST calls, with a key or an OAuth token, also need its Read access All; signed-in use of the web app does not. Each call is checked against the owner's permissions on the records it touches. Removing API & Webhooks from a role therefore hides its members' keys and stops them for MCP and REST alike.
To stop a member's keys, open the member in Members, set Status to Inactive and click Save. This needs Edit and Read access All on Users & Roles; you cannot deactivate yourself, and the last active member with the Admin role cannot be deactivated. Every call with an inactive member's keys that reads or changes workspace data is refused. Nobody can list or delete another member's keys.
How does external AI access work?
MCP requests use the same authentication and authorization path:
- The agent calls
<BASE_URL>/api/v1/mcpwith an API key or an OAuth Bearer token. - The server resolves user and company from the credential.
- Every tool call runs inside a tenant-scoped context.
- Validation errors return structured messages with remediation hints and do not leak internal schema details. Unexpected errors return only a generic message.
The MCP surface adds input guardrails:
- A write changes one typed record or relationship, or up to 100 named records at once through
updateMany(one shared patch) anddeleteMany, each applied atomically. Each reference contains both a type ID and a record ID; link and unlink name the relationship definition. Omitted fields are unchanged, whilenullexplicitly clears an optional input field. Calculated fields cannot be written directly. - The backend checks the type, workspace, record permissions, version and configuration revision during the write transaction. A reference to another type or workspace cannot redirect an update.
- Deletion, including
deleteMany, names every record by reference and current version; there is no delete-by-filter shortcut. An optional impact preview hash makes the delete fail when its cascading effects changed since the preview.
See the MCP tool catalog for the full list.
How is data protected at rest and in transit, how are passwords stored, and where is data hosted?
- Postgres encryption at rest depends on your provider. On the managed cloud, data lives in an EU region and is encrypted at rest at disk level.
- Passwords and API keys are stored only as hashes.
- HTTPS everywhere on the managed cloud. On self-host, you provide the TLS; see self-hosting for the Caddy example. Sign-in cookies are marked Secure when the instance's
BASE_URLuseshttps://. - Webhook secrets and custom header values are stored retrievably, because outgoing requests need them; see How are webhook secrets and destinations secured?.
Do logs or error reports contain secrets?
The application writes plain logs to its standard output. Environment values, such as the secrets in .env, are not written to the logs, but there is no filter that masks secrets in log lines. When an error reporting service is configured, as on the managed cloud, events are sent without request cookies, headers or bodies. Unexpected errors in MCP and assistant tools reach the client, and the error report, only as a generic message such as "The operation could not be completed".
The published self-hosted image sets no error reporting address and turns off Next.js and Prisma telemetry. On a self-hosted instance, errors the app writes to its output appear only in docker compose logs app; errors it only reports to an error reporting service are not recorded anywhere. An email the provider rejected or did not answer in time is logged with its subject, the recipient's domain and the provider's reason, never with the full address.
How are webhook secrets and destinations secured?
- Webhook secrets and custom header values are stored in Postgres without hashing, because they must be retrievable to sign and send outgoing requests. The REST API and MCP never return them, the Activity log redacts them, and an opened webhook shows them only to the Admin role and to roles with Edit on API & Webhooks; a role with only Read access All sees
********in their place. To keep them elsewhere, self-host with your own secret manager. - A webhook URL may use plain
http://for any host as long as the webhook has no custom headers. A webhook with custom headers must usehttps://, except forlocalhost,127.0.0.1and::1; this is checked when the webhook is saved and again before each delivery. - Webhook URLs are not limited to public addresses: the server sends requests to any host it can reach, including internal services on its network. On self-host, restrict the app container's outbound network if internal services must stay unreachable, and give Create and Edit on API & Webhooks only to trusted roles.
You manage webhooks in Webhooks; see Webhooks.
What does the Activity log record?
Activity is the workspace's Activity log. Every change is written to it with its actor, its kind, the affected element and its payload, in the same transaction as the change itself. Record changes carry field-level previous and current values; updates to roles, webhooks, routines and connected accounts carry a field-level diff, and creates, deletes and member saves carry a snapshot. Configuration changes to the data model and to record access by role are kept with the configuration revision they produced. User registrations, record exports and accepted legal documents are recorded as well. The actor is kept even after the user is removed, and automated changes are recorded as system or as the routine owner. A failed write leaves no entry. The Activity log is included on every cloud plan and in the self-hosted edition alike.
What is not in the Activity log?
Messaging events are not part of the Activity log; they are kept only as delivery entries while a webhook or an event routine subscribes to them. Authentication events such as sign-ins, the API key lifecycle, subscription changes, saved views, widgets, agent conversations and vendor operator actions are not recorded either. Webhook secrets and header values are redacted in webhook entries. An entry written by an automated process, such as removing a connected account when a trial ends, names the user it affected rather than someone who performed it.
Who can read the Activity log, and where?
Open Activity; see Activity. Access needs Read access All on the Activity log row of your role, which offers no Create, Edit or Delete; the Admin role always has it. The page shows record changes, admin and system events and configuration changes, and can be filtered by kind. Record changes follow the reader's record access. The same permission shows the Activity log in a record's Activities view.
How long are Activity log entries kept, and can I export them?
Entries are not deleted automatically; they are kept as long as the workspace exists. Activity has no export action. The activity timeline, its REST endpoint and its MCP tool return the same entries under the same permission.
Where do I report a vulnerability?
Write to mail@customermates.com. We aim to acknowledge within one business day. There is no bug bounty program.
For agents
Record writes over MCP go through mutate_crm_record, webhook operations through manage_webhooks, and audit entries are read with get_activities.
Next
- Self-hosting: run Customermates yourself.
- API keys: key hygiene rules.
- MCP: how the AI surface is shaped.