# Product documentation

Subsalt is a query engine that produces legally de-identified synthetic data for analytics and machine learning.

You can integrate Subsalt with a [wide variety of data sources](/configuration/supported-data-sources) to provide a familiar [Postgres-compatible SQL interface](/retrieving-data/running-queries) for retrieving data along with [detailed data quality reports](/retrieving-data/data-quality) for each query.

### Learn more:

<table data-view="cards"><thead><tr><th></th></tr></thead><tbody><tr><td><i class="fa-gauge-max">:gauge-max:</i> <a href="/product/quick-start">Quick start</a></td></tr><tr><td><i class="fa-box">:box:</i> <a href="/product/architecture">Deployment options</a></td></tr><tr><td><i class="fa-trophy">:trophy:</i> <a href="/retrieving-data/data-quality">Data quality</a></td></tr></tbody></table>


# Overview

Subsalt is a Postgres-compatible query engine that legally de-identifies query results.

Subsalt is a Postgres-compatible generative database; it mirrors other databases containing sensitive data (healthcare information, user data, financial records, etc) and can generate legally de-identified responses to standard SQL queries.&#x20;

In practice, data from generative databases can dramatically accelerate access to sensitive data sources for a wide range of analytics use cases.

### Using Subsalt

As an analyst or data scientist, using Subsalt feels very similar to using a traditional Postgres database: you write the same queries you would've written on the sensitive data source, and get results back. You can connect using all of the tools you're used to.

One important difference: the data Subsalt responds with is fully synthetic. In many cases, Subsalt data can be used in place of real data for machine learning applications and population-level analyses; in other cases, users will do their exploratory and development work on synthetics and then ship their code back to real data for a blind final analysis.

In either case, you get faster access to data and get to keep using the tools you know and love. Subsalt is commonly used for machine learning, research, data science, and business intelligence use cases where population-level patterns (as opposed to studying individual rows) is the primary objective.

### How it fits in

Subsalt is typically deployed in a Kubernetes cluster in a customer's cloud environment and connected to one or more data warehouses;  Subsalt serves as an alternative access point for users who can't easily access the sensitive warehouse(s).

The system periodically connects to these warehouses to train generative models, but no active connection is required between Subsalt and the warehouse for handling queries.

Users most likely to benefit are those who can't access (or can't easily access) the underlying warehouse data due to compliance or privacy concerns, but need to complete projects that depend on that data. Because of Subsalt's automatic de-identification capabilities, these users can often get dramatically faster access to Subsalt than they could to the real data.

### Managed vs Cloud

Subsalt instances can be provisioned directly in customer environments ([Subsalt Managed](/product/subsalt-managed)) or in Subsalt's secure cloud ([Subsalt Cloud](/product/subsalt-cloud)).

<table><thead><tr><th width="205.26953125">Factor</th><th width="260.9375">Subsalt Managed</th><th width="279.75390625">Subsalt Cloud</th></tr></thead><tbody><tr><td><strong>Infrastructure Management</strong></td><td>Customer-owned cloud account, Subsalt manages cluster</td><td>Fully managed by Subsalt</td></tr><tr><td><strong>Data Location</strong></td><td>Customer's cloud environment</td><td>Stored in customer's cloud <br>Accessible from Subsalt's cloud</td></tr><tr><td><strong>Compliance Requirements</strong></td><td>Customer-controlled environment</td><td>SOC2 + HIPAA certified environment, BAA available on request.</td></tr><tr><td><strong>Time to Deploy</strong></td><td>Requires cluster provisioning</td><td>No infrastructure setup</td></tr><tr><td><strong>Network Requirements</strong></td><td>Deployed within customer VPC</td><td>Private Link or similar for data access</td></tr><tr><td><strong>Infrastructure Access</strong></td><td>Customer has full infrastructure-level access</td><td>None</td></tr><tr><td><strong>Support</strong></td><td>24/7 monitoring, automatic updates, email support </td><td>24/7 monitoring, automatic updates, email support </td></tr></tbody></table>


# Quick start

{% hint style="info" %}
This guide assumes you have access to either Subsalt Cloud or a managed instance. Contact us for trial access to our Cloud-based sandbox environment.
{% endhint %}

This guide will provide a step-by-step process on how to build your first synthetic table based on a one-million row source dataset.

### Creating the database

*Detailed documentation:* [Creating synthetic databases](/configuration/creating-synthetic-databases)

1. Log into the Subsalt web portal and click the "New connection" button on the "Databases" tab.
2. Select `MySQL` , click Next, and then add the following connection information:
   1. **Synthetic database alias:** first\_database
   2. **Privacy standard:** HIPAA
   3. **Hostname:** subsalt-onboard.subsalt.svc.cluster.local
   4. **Database name:** healthcare
   5. **Username:** onboard
   6. **Password:** password
3. After clicking the "Next" button, a scan status screen will appear with a progress bar indicating that a database scan is underway. During this process, the application analyzes the source dataset schema so that data columns can be properly configured in a subsequent step.\ <br>

   <figure><img src="https://1365759658-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FX4JpqAkQu4oYdnmHdEWB%2Fuploads%2FM0GMDVQE6pXs8A3KpiAC%2Fdatabase-scan-simple.png?alt=media&amp;token=4cd5a65e-c2fa-494a-b583-baff4257b8f4" alt=""><figcaption></figcaption></figure>
4. Once the scanning process completes, the database configuration page will appear with a few tables listed. Check the boxes beside the `patients`, `visits`, and `medications` tables.

These three tables will be included in your new synthetic database. The next step is to configure each of them to maximize synthetic data quality and apply appropriate privacy protections.

### Configuring tables

*Detailed documentation:* [Tables](/configuration/creating-synthetic-databases/tables)

Next we need to select each of the three tables we've included and configure their schemas.

1. First select the `patients` table and add the "Indirect identifier" property to the `weight` column. Then click "Mark reviewed" to go back to the database configuration page.
2. Then select the `visits` table and add the "Join key" property to the `patient_id` column. An extra dropdown will appear, and you should select `patients.patient_id` to indicate which column the `visits.patient_id` references. Then select "Mark reviewed."
3. Lastly, select the `medications` table and add the "Join key" property to `patient_id`, just as you did with the `visits` table above. Then select "Mark reviewed."

Then click the "Next" button, click "Next" on the [Constraints](/configuration/creating-synthetic-databases/constraints#adding-constraints) page, and review your configurations for all three tables. Check the box indicating that you've reviewed the configurations and click the "Next" button.

Congratulations! You've completed onboarding for your first synthetic database!

{% hint style="info" %}
This dataset is non-sensitive, so feel free to try different configurations.
{% endhint %}

### Building your table(s)

In order to populate your synthetic database, you need to build each table. "Building" a table instructs the system to pull in data from the source system to train and evaluate generative models on. You can build tables in one of two ways:

* Click the "Build all" button on the table list page - `/databases/<id>/tables`
* Click on an individual table and click the "Build" button - `/databases/<database_id>/tables/<table_id>`

Building may take several hours depending on dataset size. You can click on the status indicator beside each table to monitor the progress each build job.

Once a table enters "Ready" status it'll be immediately queryable by authorized users.

### Querying a table

*Detailed documentation:* [Running queries](/retrieving-data/running-queries)

You can retrieve synthetic data from a table as soon as its marked as Ready. Prior to querying there are a few one-time steps to enable access to the synthetic database:

1. If your Subsalt portal account uses SSO, you need to set a distinct password for querying Subsalt. Go to Profile > Access Credentials in the portal and configure that.
2. Next, go to the Databases page and select the `first_database` you created above. Check the "Conditions" label and review any agreements that may be required for your organization.

Once those steps are complete, you're ready to query your database.

Open your preferred Postgres client - [TablePlus](https://tableplus.com/) is a good option if you don't have a favorite. Create a new Postgres connection using the following connection information:

* Hostname: \[Your access point hostname]
* User: \[Email address you used for the portal]
* Password: \[Password you set in Step 1 above]
* Database: `first_database`
* Port: 5432

Once authenticated, run the simple query below to view a sample of the results from your first table:

```sql
select * from patients limit 5000;
```

### Joining tables

Joins work the same way as single-table queries, but require an additional build step the first time. Run this query on `first_database` :

```sql
select * from patients
join medications on patients.patient_id = medications.patient_id
limit 5000;
```

If the `patients` and `medications` tables have never been joined before, you'll receive a message indicating that a new build job needs to run along with a link to a status page to track progress. Once that job completes, these tables will be joinable for all future users.


# Architecture

Subsalt is commonly deployed into secure, sensitive environments - as a result, the product is intentionally architected to follow modern security best practices. Most importantly, Subsalt never moves sensitive data out of it's designated environment, and never stores sensitive data on disk.

{% hint style="info" %}
We've got a detailed enterprise security manual appropriate for architecture and security reviews. [Reach out to our client support team](mailto:support@getsubsalt.com) to obtain a copy.
{% endhint %}

### Deployment models

Subsalt is available both as a cloud-hosted offering and a managed instances deployed in customer cloud environments.

* [Subsalt Cloud](/product/subsalt-cloud) runs in Subsalt-managed infrastructure
* [Subsalt Managed](/product/subsalt-managed) runs in customer cloud environments

### Architecture

The product is architected to enable de-identified data interactions at enterprise scale - customers can provide access to a single team or many teams across many organizations using the same components. There are three core components for Subsalt's product that coordinate to support a wide range of deployment configurations and use cases:

<table><thead><tr><th width="142.796875">Component</th><th width="331.6328125">Responsibility</th><th data-type="checkbox">Sensitive data access?</th><th data-type="checkbox">End user access?</th></tr></thead><tbody><tr><td>Secure Training Environment</td><td>Train and evaluate generative models on raw data</td><td>true</td><td>false</td></tr><tr><td>Access Point</td><td>Run queries to retrieve de-identified results</td><td>false</td><td>true</td></tr><tr><td>Hub</td><td>Coordination plane for connected training environments and access points</td><td>false</td><td>false</td></tr></tbody></table>

All components run in Kubernetes, and can be deployed in a single cluster or scaled across many clusters. The Secure Training Environment component is the only element of the system that ever has access to raw data, and is therefore typically deployed in a customer's sensitive data environment. Access Points do not have direct connections to Secure Training Environments (or raw data), and sensitive data is not accessed during the process of generating results for a query.

### Data minimization

Subsalt's application implements data minimization best practices to further reduce the risk to your datasets. You can be sure that:

* Subsalt's system only retrieves sensitive data from data sources that you've configured, and only for the purposes of producing synthetic, de-identified versions of the same data.
* Your raw data never leaves the Secure Training Environment(s), and is never persisted to disk. It is purged from the system as soon as the training and evaluation process is completed.
* Subsalt does not grant permission to others to access your de-identified data assets.


# Subsalt Managed

Fully managed instance in your cloud environment

{% hint style="success" %}
Managed instances are supported on Azure and AWS. Contact Subsalt for support on other cloud providers.
{% endhint %}

Managed instances are single-tenant deployments that run in your cloud environment, providing you with infrastructure-level control as well as the ability to use all Subsalt interfaces and tools.

We have packaged the best parts of Subsalt into a Helm chart and can deploy and operate them in your own account, bringing you the best of SaaS with the added benefit of a deployment free of noisy neighbors, enhanced support, and additional security guarantees.

With Subsalt Managed, it is more than just an on-premises deployment of your database; you are getting the Subsalt team operating your deployment alongside your team for a fully managed database solution.

### How does a Subsalt Managed deployment work?

Subsalt Managed instances are deployed in a Kubernetes cluster in your cloud account; Subsalt maintains the cluster and ensures that your instance is always in a healthy state, including technical support, 24x7 monitoring, and automatic application + security updates.

Check out the [Installation Guide](/product/subsalt-managed/installation-guide) for more information, or review the [Cluster Requirements](/product/subsalt-managed/cluster-requirements).

### Responsibility matrix

<table><thead><tr><th width="320.91796875">Area</th><th>Customer</th><th>Subsalt</th><th>Shared</th></tr></thead><tbody><tr><td><strong>Infrastructure</strong></td><td></td><td></td><td></td></tr><tr><td>Cloud account management</td><td>✓</td><td></td><td></td></tr><tr><td>Kubernetes cluster provisioning</td><td></td><td>✓</td><td></td></tr><tr><td>Kubernetes cluster patching / upgrades</td><td></td><td>✓</td><td></td></tr><tr><td>Node scaling decisions</td><td></td><td></td><td>✓</td></tr><tr><td><strong>Application</strong></td><td></td><td></td><td></td></tr><tr><td>Subsalt application deployment</td><td></td><td>✓</td><td></td></tr><tr><td>Application configuration</td><td></td><td>✓</td><td></td></tr><tr><td>Security updates</td><td></td><td>✓</td><td></td></tr><tr><td>Major version upgrades</td><td></td><td></td><td>✓</td></tr><tr><td><strong>Data &#x26; Security</strong></td><td></td><td></td><td></td></tr><tr><td>Data governance policies</td><td>✓</td><td></td><td></td></tr><tr><td>Access control configuration</td><td></td><td></td><td>✓</td></tr><tr><td>Network security (VPC, firewalls)</td><td>✓</td><td></td><td></td></tr><tr><td>Application security</td><td></td><td>✓</td><td></td></tr><tr><td><strong>Operations</strong></td><td></td><td></td><td></td></tr><tr><td>24/7 monitoring</td><td></td><td>✓</td><td></td></tr><tr><td>Incident response</td><td></td><td>✓</td><td></td></tr><tr><td>Backup execution</td><td></td><td>✓</td><td></td></tr><tr><td>Disaster recovery testing</td><td></td><td></td><td>✓</td></tr><tr><td>Performance tuning</td><td></td><td>✓</td><td></td></tr><tr><td><strong>Support</strong></td><td></td><td></td><td></td></tr><tr><td>Initial stakeholder training</td><td></td><td>✓</td><td></td></tr><tr><td>End-user training</td><td>✓</td><td></td><td></td></tr><tr><td>Technical support</td><td></td><td>✓</td><td></td></tr><tr><td>Change management</td><td></td><td></td><td>✓</td></tr></tbody></table>

### Support and maintenance

Subsalt provides Terraform templates for creating a compatible cluster; once you've created the cluster, the Subsalt support team will install the Subsalt application and work with you to match your internal networking requirements.

Your Subsalt support team will then take the lead in ensuring that your cluster is always available and up to date.

#### Software updates

Your Subsalt support team will ensure that security and product updates are applied in a timely manner. Subsalt publishes releases using [semantic versioning](https://semver.org/), and will automatically apply minor and patch updates during maintenance windows.&#x20;

Major releases will be coordinated with your internal stakeholders since they often involve more significant infrastructure or user-facing changes.

#### Email and Slack support

Subsalt Managed customers can reach out to a Subsalt engineer via email or Slack for assistance.


# Installation Guide

### Install the Subsalt CLI

The Subsalt CLI allows you to perform administrator operations on your instance.

{% tabs %}
{% tab title="Linux" %}
**arm64:**

<pre class="language-bash"><code class="lang-bash"><strong>wget https://subsalt-cli-public.s3.us-west-2.amazonaws.com/releases/latest/linux/arm64/subsalt.tar.gz &#x26;&#x26; \
</strong><strong>    tar -xvzf subsalt.tar.gz
</strong></code></pre>

**amd64:**

```bash
wget https://subsalt-cli-public.s3.us-west-2.amazonaws.com/releases/latest/linux/amd64/subsalt.tar.gz && \
    tar -xvzf subsalt.tar.gz
```

{% endtab %}

{% tab title="Mac" %}
**arm64:**

```bash
wget https://subsalt-cli-public.s3.us-west-2.amazonaws.com/releases/latest/darwin/arm64/subsalt.tar.gz && \
    tar -xvzf subsalt.tar.gz
```

**amd64:**

```bash
wget https://subsalt-cli-public.s3.us-west-2.amazonaws.com/releases/latest/darwin/amd64/subsalt.tar.gz && \
    tar -xvzf subsalt.tar.gz
```

{% endtab %}

{% tab title="Windows" %}
**arm64:**

```bash
wget https://subsalt-cli-public.s3.us-west-2.amazonaws.com/releases/latest/windows/arm64/subsalt.tar.gz && \
    tar -xvzf subsalt.tar.gz
```

**amd64:**

```bash
wget https://subsalt-cli-public.s3.us-west-2.amazonaws.com/releases/latest/windows/amd64/subsalt.tar.gz && \
    tar -xvzf subsalt.tar.gz
```

{% endtab %}
{% endtabs %}

### Create a cluster

Generate a Terraform template for your cluster by running:

```bash
$ subsalt generate-infrastructure
```

Follow the instructions provided by the CLI, including reviewing configuration values to confirm that they're accurate for your environment. It's important to configure `backend.tf` to target a bucket/storage account container that you have access to when running the terraform commands.

Create your cluster by running:

```wasm
$ terraform apply
```

{% hint style="warning" %}
Make sure you're authenticated into a cloud account with sufficient permissions in your terminal before running `terraform apply` .
{% endhint %}

### Configuring Subsalt team access

Subsalt support engineering needs access to your Kubernetes cluster in order to manage the instance.&#x20;

#### Azure guest accounts

This method provides "contributor" access to your Subsalt cluster for Subsalt's support engineering team, which will allow us to monitor system health and apply configuration changes or updates as needed.

To issue the invites:

1. Log in to the Azure portal
2. Navigate to **Users**
3. Click on the arrow to the left of **+ New User** and click **Invite external user**
4. Fill out the form for user `managed-support-eng@getsubsalt.com`
   1. No initial Groups or Roles are required to be assigned in this part of flow
5. Find the resource group with the name starting with **MC\_subsalt-cluster**
6. Assign the **Contributor** role on this resource group for the invited user
   1. You can do this in the **Access Control (IAM)** page for the resource group
   2. The **Contributor** role will be under **Privileged administrator roles**
7. Find the resource group with the name **subsalt-cluster**
8. Assign the **Contributor** role on this resource group for the invited user
   1. You can do this in the **Access Control (IAM)** page for the resource group
   2. The **Contributor** role will be under **Privileged administrator roles**


# Cluster Requirements

Subsalt provides [Terraform](https://www.terraform.io/) templates for each of the major Kubernetes cloud providers to make it easy to set up compatible clusters. The Terraform templates are accessible through [Subsalt's CLI tool](/product/subsalt-managed/installation-guide#install-the-subsalt-cli).

### Cluster resource requirements

* Subsalt runs on Kubernetes v1.32+, and can be deployed in multiple configurations depending on your needs. Multiple components can be deployed in a single cluster, or across multiple clusters.
  * We follow Kubernetes [release support lifecycle](https://kubernetes.io/releases/) for the versions of Kubernetes that we aim to support.
* Subsalt must have cluster-level permission to add operators at installation time.
* Below are tables that note the minimum requirements for node pools configured in each cloud environment.

### Microsoft Azure AKS

<table data-full-width="true"><thead><tr><th width="125.671875">Name</th><th width="137.546875">Node count</th><th width="228.62890625">Recommended instance type</th><th width="296.5625">Taints</th><th>Labels</th></tr></thead><tbody><tr><td>common</td><td>2 <br>(fixed)</td><td>Standard_E16s_v3</td><td></td><td>subsalt.io/node-purpose=common</td></tr><tr><td>serving</td><td>0-1<br>(auto-scaling)</td><td>Standard_NV12ads_A10_v5</td><td>nvidia.com/gpu=present:NoSchedule</td><td>subsalt.io/node-purpose=serving, subsalt.io/has-gpu=true</td></tr><tr><td>pipelines_cpu</td><td>0 - 4 <br>(auto-scaling)</td><td>Standard_E16s_v3</td><td>subsalt.io/node-purpose=pipelines:NoSchedule</td><td>subsalt.io/node-purpose=pipelines, subsalt.io/has-gpu=false</td></tr><tr><td>pipelines_gpu</td><td>0 - 3 <br>(auto-scaling)</td><td>Standard_NC8as_T4_v3</td><td>nvidia.com/gpu=present:NoSchedule</td><td>subsalt.io/node-purpose=pipelines, subsalt.io/has-gpu=true</td></tr></tbody></table>

### Amazon Web Services (AWS) EKS

<table data-full-width="true"><thead><tr><th width="134.06640625">Name</th><th width="129.78515625">Node count</th><th width="155.8046875">Recommended instance type</th><th>Taints</th><th>Labels</th></tr></thead><tbody><tr><td>common</td><td>2 <br>(fixed)</td><td>r6a.4xlarge</td><td></td><td>subsalt.io/node-purpose=common</td></tr><tr><td>serving</td><td>0-1 <br>(auto-scaling)</td><td>g5.xlarge</td><td>nvidia.com/gpu=present:NoSchedule</td><td>subsalt.io/node-purpose=serving, subsalt.io/has-gpu=true</td></tr><tr><td>pipelines_cpu</td><td>0 - 4 <br>(auto-scaling)</td><td>r6a.4xlarge</td><td>subsalt.io/node-purpose=pipelines:NoSchedule</td><td>subsalt.io/node-purpose=pipelines, subsalt.io/has-gpu=false</td></tr><tr><td>pipelines_gpu</td><td>0 - 3<br>(auto-scaling)</td><td>g5.4xlarge</td><td>nvidia.com/gpu=present:NoSchedule</td><td>subsalt.io/node-purpose=pipelines, subsalt.io/has-gpu=true</td></tr></tbody></table>

Assuming you're using EC2 autoscaling groups for cluster autoscaling the following tags will also need to be set (assumes recommended instance types).

{% hint style="info" %}
CPU and Memory values should be set to between 85-90% of the actual instance type's resource values to account for Kubernetes system pods.
{% endhint %}

#### **Serving node pool**

<table><thead><tr><th width="560.61328125">Key</th><th>Value</th></tr></thead><tbody><tr><td>k8s.io/cluster-autoscaler/node-template/resources/cpu</td><td>3</td></tr><tr><td>k8s.io/cluster-autoscaler/node-template/resources/memory</td><td>14G</td></tr><tr><td>k8s.io/cluster-autoscaler/node-template/resources/nvidia.com/gpu</td><td>1</td></tr><tr><td>k8s.io/cluster-autoscaler/node-template/taint/nvidia.com/gpu</td><td>present:NoSchedule</td></tr><tr><td>k8s.io/cluster-autoscaler/node-template/label/subsalt.io/has-gpu</td><td>true</td></tr></tbody></table>

#### **Pipelines (CPU) node pool**

<table><thead><tr><th width="559.0234375">Key</th><th>Value</th></tr></thead><tbody><tr><td>k8s.io/cluster-autoscaler/node-template/resources/cpu</td><td>14</td></tr><tr><td>k8s.io/cluster-autoscaler/node-template/resources/memory</td><td>116G</td></tr><tr><td>k8s.io/cluster-autoscaler/node-template/taint/subsalt.io/node-purpose</td><td>pipelines:NoSchedule</td></tr><tr><td>k8s.io/cluster-autoscaler/node-template/label/subsalt.io/node-purpose</td><td>pipelines</td></tr><tr><td>k8s.io/cluster-autoscaler/node-template/label/subsalt.io/has-gpu</td><td>false</td></tr></tbody></table>

#### **Pipelines (GPU) node pool**

<table><thead><tr><th width="565.48828125">Key</th><th>Value</th></tr></thead><tbody><tr><td>k8s.io/cluster-autoscaler/node-template/resources/cpu</td><td>14</td></tr><tr><td>k8s.io/cluster-autoscaler/node-template/resources/memory</td><td>56G</td></tr><tr><td>k8s.io/cluster-autoscaler/node-template/resources/nvidia.com/gpu</td><td>1</td></tr><tr><td>k8s.io/cluster-autoscaler/node-template/taint/nvidia.com/gpu</td><td>present:NoSchedule</td></tr><tr><td>k8s.io/cluster-autoscaler/node-template/label/subsalt.io/node-purpose</td><td>pipelines</td></tr><tr><td>k8s.io/cluster-autoscaler/node-template/label/subsalt.io/has-gpu</td><td>true</td></tr></tbody></table>

### Networking

#### Ingress

Subsalt requires that the cluster has a [Gateway integration](https://gateway-api.sigs.k8s.io/docs/introduction/) for web access. [Here's an example using Envoy Gateway.](/product/subsalt-managed/cluster-requirements/example-using-envoy-gateway-for-ingress)

There are three hosts to configure:

| Host                    | Description                         |
| ----------------------- | ----------------------------------- |
| portal.subsalt.acme.com | Web portal.                         |
| auth.subsalt.acme.com   | Internal authentication management. |
| mcp.subsalt.acme.com    | (*Coming soon)* MCP integrations.   |

[A Record's](https://www.cloudflare.com/learning/dns/dns-records/dns-a-record/) for each host should be configured in your DNS provider to point at the Gateway IP address.

A certificate (TLS/SSL) should be added to the `Gateway` routes that accounts for all hosts. The Subsalt Helm Chart comes with [cert-manager](https://cert-manager.io/) by default which can be configured to provision the certificates and their secrets automatically.

#### Load balancer

Subsalt requires [Service (Load Balancer)](https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer) support for the [query endpoint](/retrieving-data/running-queries) to serve synthetic data.

An [A Record](https://www.cloudflare.com/learning/dns/dns-records/dns-a-record/) should be configured in your DNS provider to point at this load balancer's IP address.

### Other

If your team uses another Kubernetes provider (IBM, DigitalOcean, etc), please reach out and we can find a way to support your deployment.


# Example: Using Envoy Gateway for ingress

How to install and use Envoy's Gateway implementation to manage ingress for Subsalt.

### Envoy Gateway

If you don't already have a Gateway implementation in your cluster, we recommend using [Envoy's](https://gateway.envoyproxy.io/). It's straightforward to install and manage.

#### Install

**1. Create the Envoy Gateway namespace**

```bash
kubectl create namespace envoy-gateway-system
```

**2. Create the registry pull secret**

Envoy Gateway runs from Subsalt's hardened wrapper image in `subsaltprod.azurecr.io`. Create a pull secret in the Envoy Gateway namespace using the registry credentials Subsalt provided:

```bash
kubectl create secret docker-registry subsalt-registry-auth \
  --namespace envoy-gateway-system \
  --docker-server=subsaltprod.azurecr.io \
  --docker-username='<your-subsaltprod-username>' \
  --docker-password='<your-subsaltprod-password>'
```

**3. Install the Envoy Gateway control plane**

Install the upstream chart from its OCI registry, pinned to the version that matches your Subsalt release, and point the control-plane image at Subsalt's hardened image. Replace `<subsalt-version>` with the version tag of your Subsalt platform (the Envoy Gateway image is published under the same tag as the rest of your Subsalt images).

```bash
helm install envoy-gateway oci://docker.io/envoyproxy/gateway-helm \
  --version v1.8.2 \
  --namespace envoy-gateway-system \
  --set fullnameOverride=envoy-gateway \
  --set deployment.envoyGateway.image.repository=subsaltprod.azurecr.io/envoy-gateway \
  --set deployment.envoyGateway.image.tag=v1.0.0 \
  --set deployment.envoyGateway.imagePullSecrets[0].name=subsalt-registry-auth \
  --wait
```

**4. Create the GatewayClass**

The `GatewayClass` binds `Gateway`s to the Envoy Gateway controller. It is operator-managed (the analog of an `IngressClass`) and is not created by the chart.

```bash
kubectl apply -f - <<'EOF'
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: envoy-gateway
spec:
  controllerName: gateway.envoyproxy.io/gatewayclass-controller
EOF
```

**5. Create the Gateway**

Create the Gateway in the **same namespace as the Subsalt services and TLS secrets** (`subsalt` here). Choose one option only. Both examples declare `subsalt-frontend-gateway`.

{% tabs %}
{% tab title="Public Gateway" %}
Use a public Gateway when the portal, authentication, and MCP hostnames resolve publicly. Replace each example hostname and secret name with your own values.

```bash
kubectl apply -f - <<'EOF'
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: subsalt-frontend-gateway
  namespace: subsalt
spec:
  gatewayClassName: envoy-gateway
  listeners:
    - name: portal-https
      protocol: HTTPS
      port: 443
      hostname: portal.example.com
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Secret
            name: portal-tls
      allowedRoutes:
        namespaces:
          from: Same
    - name: auth-https
      protocol: HTTPS
      port: 443
      hostname: auth.example.com
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Secret
            name: auth-tls
      allowedRoutes:
        namespaces:
          from: Same
    - name: mcp-https
      protocol: HTTPS
      port: 443
      hostname: mcp.example.com
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Secret
            name: mcp-tls
      allowedRoutes:
        namespaces:
          from: Same
EOF
```

{% endtab %}

{% tab title="Private Gateway (Azure)" %}
Use a private Gateway when these hostnames must resolve to an RFC 1918 address within your VNet. It is reachable through your private network, such as VPN, ExpressRoute, a peered VNet, or a jump host.

```bash
kubectl apply -f - <<'EOF'
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
metadata:
  name: internal-frontend
  namespace: subsalt
spec:
  provider:
    type: Kubernetes
    kubernetes:
      envoyService:
        type: LoadBalancer
        annotations:
          service.beta.kubernetes.io/azure-load-balancer-internal: "true"
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: subsalt-frontend-gateway
  namespace: subsalt
spec:
  gatewayClassName: envoy-gateway
  infrastructure:
    parametersRef:
      group: gateway.envoyproxy.io
      kind: EnvoyProxy
      name: internal-frontend
  listeners:
    - name: portal-https
      protocol: HTTPS
      port: 443
      hostname: portal.example.com
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Secret
            name: portal-tls
      allowedRoutes:
        namespaces:
          from: Same
    - name: auth-https
      protocol: HTTPS
      port: 443
      hostname: auth.example.com
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Secret
            name: auth-tls
      allowedRoutes:
        namespaces:
          from: Same
    - name: mcp-https
      protocol: HTTPS
      port: 443
      hostname: mcp.example.com
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Secret
            name: mcp-tls
      allowedRoutes:
        namespaces:
          from: Same
EOF
```

The `EnvoyProxy` configuration applies to this Gateway only. Azure provisions one internal load balancer for Envoy. The listeners share port `443` and route by hostname.
{% endtab %}
{% endtabs %}

**6. Create the HTTPRoutes**

These routes send the three frontend hostnames to their Subsalt services. Use the same hostnames configured in the Gateway.

```bash
kubectl apply -f - <<'EOF'
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: subsalt-portal
  namespace: subsalt
spec:
  parentRefs:
    - name: subsalt-frontend-gateway
      sectionName: portal-https
  hostnames:
    - portal.example.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: subsalt-portal-http
          port: 80
      # Extended timeout for long-running query operations.
      timeouts:
        request: "3600s"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: subsalt-auth
  namespace: subsalt
spec:
  parentRefs:
    - name: subsalt-frontend-gateway
      sectionName: auth-https
  hostnames:
    - auth.example.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: subsalt-keycloak
          port: 80
      timeouts:
        request: "3600s"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: subsalt-mcp
  namespace: subsalt
spec:
  parentRefs:
    - name: subsalt-frontend-gateway
      sectionName: mcp-https
  hostnames:
    - mcp.example.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: subsalt-mcp-server
          port: 80
      timeouts:
        request: "3600s"
EOF
```


# Subsalt Cloud

Host your instance in Subsalt's cloud infrastructure

{% hint style="success" %}
Subsalt Cloud provides the fastest path to using the product with zero infrastructure management. Contact us for trial access to our cloud sandbox environment.
{% endhint %}

Subsalt Cloud is our fully-managed, multi-tenant deployment option that runs entirely in Subsalt's secure cloud infrastructure. It provides all the power of Subsalt's safe synthetic data generation without any infrastructure overhead, making it ideal for teams who want to focus on analytics rather than operations.

With Subsalt Cloud, you get enterprise-grade synthetic data capabilities with the simplicity of SaaS - no clusters to manage, no software to update, and no infrastructure to monitor.

### How does Subsalt Cloud work?

Subsalt Cloud instances run in our secure, SOC 2 certified cloud infrastructure. We handle all aspects of deployment, scaling, monitoring, and maintenance while ensuring your sensitive data remains protected.

Your team accesses Subsalt through standard database connections and our web portal, with data flowing securely between your environment and ours via Private Link or similar secure connectivity options. Your sensitive data is never persisted in Subsalt's environment.

### Support and maintenance

#### Software updates

Your Subsalt support team will ensure that security and product updates are applied in a timely manner. Subsalt publishes releases using [semantic versioning](https://semver.org/), and will automatically apply minor and patch updates during maintenance windows.&#x20;

Major releases will be coordinated with your internal stakeholders since they often involve more significant infrastructure or user-facing changes.

#### Email and Slack support

Subsalt Managed customers can reach out to a Subsalt engineer via email or Slack for assistance.

### Security and compliance

Subsalt Cloud maintains enterprise-grade security standards:

* **SOC 2 Type II certified** infrastructure
* **HIPAA compliant** with BAA available
* **End-to-end encryption** for all data in transit
* **No persistent storage** of sensitive data
* **Regular third-party security audits**


# Subsalt Self-Hosted

Host a Subsalt instance in your cloud environment and manage it yourself

{% hint style="success" %}
Self-hosted instances are supported on Azure and AWS. Contact Subsalt for support on other cloud providers.
{% endhint %}

Single-tenant deployment in customer's environment, managed by customer

Differences vs Subsalt Managed:

* Customer is responsible for applying updates, monitoring, and system availability. Subsalt is still available for technical support but the customer is responsible for initiating the support request.
* We should still get temporarily access to the cluster when debugging a specific issue.
* Same email SLAs but no monitoring SLAs since we have nothing to monitor.


# Installation Guide


# Supported data sources

Currently, Subsalt directly integrates with:

* MySQL
* Redshift
* Snowflake
* S3 (Parquet)
* Azure storage accounts (Parquet)
* Databricks
* Microsoft SQL Server

If your organization's data source is not listed here, don't worry! Subsalt can connect to any database that supports tabular structured data. Please reach out to the Subsalt team for more information on connecting to your database.

### Connecting to Parquet-based file systems

Subsalt automatically interprets each folder in the target directory as a potential table, with the data being nested in that folder. For example, if you have an S3 bucket called `my-bucket` that is structured like:

* **my-bucket**
  * **my-database**
    * 📁 **customers** <- "customers" will be a table
      * *file\_1.parquet* <- all files in this folder will be combined to make a single "customers" table
      * *file\_2.parquet*
      * *file\_3.parquet*
    * 📁 **product\_imgs** <- since this folder does not have any tabular data it will be ignored
      * *img1.png*
      * *img2.png*
    * 📁 **sales**
      * *file\_1.parquet*
      * *file\_2.parquet*
      * *file\_3.parquet*
      * *file\_4.parquet*

### Connecting to an S3 Bucket

When connecting to an S3 bucket, you will provide a URL; for the example above, the URL would look like this:

**S3 Bucket URL:** `s3://my-bucket/my-database/`

### Connecting to Azure storage

When connecting to an Azure storage account, you will provide a [connection string](https://learn.microsoft.com/en-us/azure/storage/common/storage-configure-connection-string) and a path.

For the example above the configuration would something look like this:

**Connection string:** `BlobEndpoint=https://my-bucket.blob.core.windows.net/;SharedAccessSignature=sv=2022-11-02&ss=b&srt=co&sp=rl&se=2024-10-19T05:27:46Z&st=2024-10-18T21:27:46Z&spr=https&sig=JTrgXgQNvbmPRXumK5rWChznHQRLdaUrTnCjCUmac44%3D`

**Path:** `my-database/`


# Creating synthetic databases

A synthetic database is a de-identified, synthetic representation of a sensitive database and can be queries by authorized end users. Synthetic databases must be set up before they can be used, and can be periodically refreshed when the underlying data sources change.

By default, synthetic databases will include a subset of the tables that exist in the source database; you can also define [Views](/configuration/creating-synthetic-databases/views) that expose custom subsets of data if required.

### Creating a new synthetic database

#### Configuring data connector

Every synthetic database is tied to a source data system and mimics its schema and data distributions. The first step for creating a new synthetic database is to to configure the connection to the source data system, which you can do by clicking the "New connection" button on the **Databases** tab in the Subsalt portal.

Select the connector you'd like to use, and fill out the connection information that the Subsalt system should use to read data from the system.

{% hint style="info" %}
The credentials that you provide need to authorize the Subsalt system to make read-only queries (SELECT) to the namespaces and/or databases that you provide in the form.
{% endhint %}

#### Specifying a privacy standard

You'll also need to specify a privacy standard to enforce a particular de-identification policy. Privacy standards are typically defined by third-party auditors and cannot be modified manually; contact your administrator or Subsalt support if you're unsure of what privacy standard to select when onboarding a new data source.

Once you've configured your data connector and privacy standard, the system will attempt to connect to the source system and discover what data assets are accessible.

#### Schema and metadata configuration

Once the discovery process completes, each table that you want to include in your synthetic database needs to be configured with certain metadata in order to be processed; read more about the configuration process in [Tables](/configuration/creating-synthetic-databases/tables).

For more complex databases, it may be easier to download a representation of the table schema as a file, edit the file, and upload the full schema in one shot. This functionality is available via the "import" and "export" buttons visible on the table configuration page.

{% hint style="info" %}
You can come back and modify table configurations later, so you can start with onboarding a few tables if you don't want to onboard an entire warehouse at once.
{% endhint %}

#### Patient identifiers

An important piece of information for privacy analysis is the **patient identifier** field; this is the field that uniquely identifies patients in the dataset. You'll need to specify this field as a part of the database creation flow so that privacy evaluations can be completed successfully.

The patient identifier must be defined on a table that's included in your synthetic database.

#### Define constraints

In certain cases it may be useful to configure [Constraints](/configuration/creating-synthetic-databases/constraints) on the database; these are useful for when there are explicit relationships between certain fields - for example, you can define that:

* `birth_year` should always match the year of `birth_datetime`, or&#x20;
* `arrival_date` should always occur before `departure_date`

#### Review database configuration

After your database has been configured, the final step is to review and attest that the configuration is correct and complete. It's essential that the configuration is accurate for Subsalt's automated privacy checks to accurately assess risk, so be sure to review all details before finalizing the configuration.

### Updating a synthetic database

For periodic updates, you can simply click the "Build all" button on the database table list page. This will automatically rebuild all configured tables using freshly sampled data from the configured data source.

You can also add or remove tables from your synthetic database by navigating to the database tables page and clicking the "Configure tables" button.

{% hint style="info" %}
Rescanning a database is currently not supported. In order to add tables that didn't exist in the original scan, you should create a new synthetic database.
{% endhint %}


# Tables

Tables map one-to-one with relations in the source system; in some cases they'll contain a subset of the fields from the source table, but they will never include fields that don't exist in the source system.

### Setup

It's important that you configure your tables properly during onboarding to ensure that data quality is high and privacy is properly measured. There are two properties that need to be configured for each field: the semantic **content** of the column and any specific **properties** that apply to the fields, including privacy-related properties.

#### Content

Content labels describe what sort of data is contained in each column, which affects how the system models and trains on your data. Content labels will usually be auto-populated, but it's important that you review all labels for correctness.

<table><thead><tr><th width="127.6796875">Content</th><th width="341.60546875">Description</th><th>Examples</th></tr></thead><tbody><tr><td>Categorical</td><td>A nominal field with a discrete set of values</td><td>Gender, ZIP codes, ICD-10 codes</td></tr><tr><td>Numeric</td><td>An ordinal numeric field</td><td>Age, Height</td></tr><tr><td>Datetime</td><td>A date or datetime representation</td><td>7/15/22 10:41:55, August 11 2022</td></tr><tr><td>Currency</td><td>A string that corresponds to a USD ($) amount. Currency symbol must be the first character.</td><td>$99.99, $1.05</td></tr><tr><td>Binary</td><td>Any field that contains 2 unique values</td><td>1/0, yes/no, on/off</td></tr></tbody></table>

#### Properties

Properties provide additional metadata that's important for privacy evaluation and other important tasks. Subsalt can provide support from third-party auditors for populating HIPAA-compliant privacy labels if necessary.

<table><thead><tr><th width="175.71875"></th><th></th><th></th></tr></thead><tbody><tr><td>Indirect identifier</td><td>A field that combined with other information would help single out an individual in a dataset</td><td>Age, Gender, Home state</td></tr><tr><td>Direct identifier</td><td>A field that can be used to directly single out an individual in a dataset</td><td>Names, SSNs, Contact info</td></tr><tr><td>Person's age</td><td>A field that indicates a person's age</td><td>Age, Birthdate</td></tr><tr><td>Foreign key</td><td>A field that can be used to join two or more synthetic tables.</td><td>Patient ID, Facility ID</td></tr><tr><td>Medical code</td><td>A field that contains ICD-10 codes or other classification codes</td><td>Diagnoses, procedures</td></tr><tr><td>Entity identifier</td><td>A field that contains unique IDs for entities that need to be modeled over time</td><td>Patient ID</td></tr><tr><td>Context field</td><td>A field that is static for an entity over time</td><td>Birthdate</td></tr><tr><td>Sequence key</td><td>Datetime fields that indicate the sequence of events for the entity</td><td>Visit dates</td></tr></tbody></table>

#### Ineligible fields

The only requirement for any field in a table in Subsalt is that the field must be at least 50% non-null; fields that do not meet this requirement will be automatically marked as ineligible. These fields will not be included in the synthetic database schema, so they will not be visible to or queryable by data consumers.

### Lookup tables

Lookup tables are static fact tables that contain non-personal information, such as an OMOP Concept Tables or a list of ICD-10 codes and their classifications and/or definitions. These tables have two important properties:

* They have no relationship to patients or patient populations on their own, and therefore carry no privacy risk until they're joined with patient-related information
* It's important to be able to join synthetic patient information with accurate lookup table information; the definition of a particular Concept ID shouldn't change from row to row.

Tables that have these two properties can be configured as "lookup tables" during data onboarding; Subsalt copies lookup tables into the Subsalt cluster, and *these tables are not synthesized and are exempt from privacy audits.*&#x20;

{% hint style="info" %}
It's unnecessary to configure foreign keys for or to a Lookup table. Foreign keys are only necessary to identify relationships between synthetic tables.
{% endhint %}

Be sure to review potential lookup tables with appropriate stakeholders before marking a table as a lookup table; this setting has significant privacy implications.


# Views

A view is a virtual table derived from one or more underlying database tables or views in the source system. A view can be defined via a direct SQL query and allows you to produce custom slices of data.

### Setup

The setup process is very similar to the process for [Tables](/configuration/creating-synthetic-databases/tables), except that you provide your own view alias and a SQL query to define the view's contents. The alias is how users will reference the view in queries — `SELECT a, b, c FROM <your_view_alias>` — and the SQL query will determine the schema and contents of the resulting relation.

#### SQL definitions

The SQL query will be executed directly against the source system, and should therefore be written in the source system's native dialect. Currently Subsalt only supports single-query views.

#### Immutability

Views are structurally immutable; you cannot modify the schema or reconfigure metadata once the view has been created. If you need to modify the structure of a view, you should delete and recreate it.


# Patient identifiers

To build the database we require that you identify the field that represents a single patient.

This allows our system to count the number of patients in the dataset, which is crucial for accurate privacy evaluations.

### Setting the identifier

As a part of onboarding your database you will be prompted to set a patient identifier right after you have configured your tables.

To set the identifier, select the `Patient Table` and `Patient ID Field` that represent one patient.

Typically this will be a dedicated "patients" table with one row per patient, but if that's not how your database is structured it's also okay for this to be a table like "visits" that still captures the patient IDs.

After you've selected the correct table and field, press `Save` and you're ready to continue with onboarding.

### Updating the identifier

If you need to can change your patient identifier after the fact you can by navigating to the `Settings` tab in your database. Once you change the table and field, press `Save`.


# Cohorts

Cohorts define subsets of a population that you'd like to include in a dataset; cohorts are configured at a database level and restrict the content in all tables to records related to members of the cohort.&#x20;

For example, if you restrict your healthcare database to patients with leukemia, then all tables (visits, diagnoses, labs, etc) will all be restricted to that same set of patients; records for other patients will not be present in the synthetic database.

Cohorts will automatically be applied to all synthetic tables, and can be optionally referenced in views. Lookup tables are not affected by cohort definitions.

### Defining a cohort

Cohort definitions involve two components:

1. A query that provides a list of IDs for members of the cohort
2. A reference to the field in the entities table (ex: `person`) that contains the IDs

For example, you might commonly select `Person.PersonID` for the entity field, and provide a query like this to filter down to a specific set of people / patients (specifically, those diagnosed with COVID-19):

```sql
-- Patients with a COVID-19 diagnosis in a standard OMOP database
WITH covid_concepts AS (
    SELECT descendant_concept_id AS concept_id
    FROM concept_ancestor
    WHERE ancestor_concept_id = 37311061  -- COVID-19 (SNOMED)
)
SELECT DISTINCT person_id
FROM condition_occurrence
WHERE condition_concept_id IN (SELECT concept_id FROM covid_concepts);
```

### Configuring a cohort

You can define cohorts while [Creating synthetic databases](/configuration/creating-synthetic-databases), and you can update cohort definitions for existing databases on the `Settings > Cohorts` section of the web portal.

Applying a new cohort definition will also recompute row counts for all tables and views; this will take place in the background any may take a few minutes to complete.

### Referencing cohorts in view definitions

Cohorts do not automatically apply to views, but they can be easily referenced for users that want to leverage the database's designated cohort: the list of IDs will be accessible as a relation called `cohort`:

```sql
SELECT *
FROM visit_occurrence
WHERE person_id IN (SELECT person_id FROM cohort) -- `cohort` is pre-defined
  AND visit_start_date >= DATE '2024-01-01'
  AND visit_start_date <  DATE '2025-01-01';
```


# Constraints

Synthetic data is made to match the underlying statistical patterns of a given source dataset, with some added noise. Sometimes there are strict rules that a dataset must follow in order to be valid that data synthesis will not recognize by default. For example, if you have a `state` column mixed with a `zip_code` column, there is a strict set of combinations allowed between these two columns - for example, you cannot have a California ZIP code in a row whose state is Texas.&#x20;

Note that defining these characteristics will generally not impact the statistical fidelity and they are generally only impactful cosmetically. In the above example, even without defining a characteristic you will still mostly see realistic ZIP/state pairs - adding a constraint will ensure that *all* rows follow a particular set of rules.

### Adding constraints

You can add constraints to a database during onboarding; the standard onboarding flow has a step for adding constraints, and you can also add them later via the "Constraints" tab on the database details page.

The full list of supported constraints is below.

<table><thead><tr><th width="129.1640625">Constraint</th><th>Description</th><th>Example</th></tr></thead><tbody><tr><td>Derive</td><td>Ensures that specified columns will be populated with data from another column</td><td><code>birth_year</code> should be populated based on the "year" portion of <code>birth_date</code></td></tr><tr><td>Group</td><td>Ensures that the columns specified are never seen in a new unique combination</td><td>State, city, ZIP can only be combined as observed in source data</td></tr><tr><td>Conditional</td><td>Ensures that a target column will be populated with a specific value when a column contains a specified value</td><td>Values generated for <code>dischargable</code> will be "Y" when <code>healthy</code> is 'true'</td></tr><tr><td>Calculate</td><td>Ensures that a target column will be populated with the results of calculation between two columns</td><td>Values generated for 'total_cost' will be the result of 'base_cost' + 'fee'</td></tr><tr><td>SpecialValues</td><td>Ensures that special values within columns are preserved</td><td>'-1' has a special meaning in a continuous column and should be modeled independently from the primary distribution</td></tr><tr><td>Existence</td><td>Ensures that a target column will be populated with 1 if the conditional matches on the user input or 0 otherwise. <strong>NOTE</strong>: Only integer columns are supported</td><td>Mask age column by setting values to 1 if age is greater than 20 else 0</td></tr><tr><td>Delta</td><td>Ensures that the gap between two related columns stays realistic by learning the actual distribution of distances in the source data</td><td><code>admit_date</code> and <code>discharge_date</code> are modeled as the admit date plus the duration between them, ensuring discharge always follows admission by a realistic interval</td></tr></tbody></table>


# System catalogs

The system catalogs are the place where users can access schema metadata, such as information about tables and columns, and other bookkeeping information. Subsalt supports two standard system catalogs: `pg_catalog` and `information_schema`. All catalog tables are read-only, and can be queried with standard SQL statements.

### pg\_catalog

`pg_catalog` is [Postgres' system catalog](https://www.postgresql.org/docs/current/catalogs.html) and contains information about tables, namespaces, data types, and many other entities. Subsalt supports the full schema for all tables for compatibility reasons, though some values don't apply to generative databases and aren't populated.

### information\_schema

`information_schema` is the ANSI SQL standard system catalog and contains information about entities common to all relational databases (tables, columns, etc). Subsalt supports the [set of tables supported by Postgres](https://www.postgresql.org/docs/current/information-schema.html).


# Privacy configurations


# Privacy alerts


# Running queries

Once a synthetic database has been built, it can be queried just like any other Postgres database, which means the same tools used to connect to other Postgres databases can be used to connect to Subsalt synthetic databases.

### Connecting to Subsalt's query endpoint

You can find connection details on the **Profile > Connection details** page in the Subsalt portal; these details won't change unless you modify your password and can be stored in your Postgres client.

{% hint style="info" %}
If you don't have a preferred client, we recommend [TablePlus](https://tableplus.com/) as a graphical application, [psql](https://www.postgresql.org/docs/current/app-psql.html) for command-line access, and [psycopg2](https://docs.subsalt.io/) for Python access.

Additionally, you can execute queries directly in the Subsalt portal by navigating to the **Query** tab for the database you want to query. Results are limited to 1000 rows of data.
{% endhint %}

For users associated with SSO-based organizations, you'll need to set a Subsalt-specific password to be able to connect - *do not use the password associated with your identity provider*.

{% hint style="info" %}
Working with an AI agent? Subsalt also exposes a **Model Context Protocol (MCP)** server that lets agents explore schemas, query synthetic data, and compute de-identified answers on real data over a single authenticated connection. See the MCP documentation.
{% endhint %}

### Data quality

You can retrieve a detailed quality assessment for the results for any query from the Subsalt portal; read more about what information is available and how to access it in [Data quality](/retrieving-data/data-quality).

#### `__rare__`

You may notice the value `__rare__` appearing in your data when the value was not present in the source table. This is an automatic mask to any categorical field that has a very low occurrence rate, and is used to automatically reduce long-tail privacy risk.&#x20;

If your column is part of your schema as a numeric field whose context is set as categorical, this `__rare__` value will appear as `-9999999` to ensure that your schema type is consistent with the source.

### Supported SQL features

Subsalt supports the PostgreSQL dialect of SQL, and you should use this dialect regardless of the source system's native dialect.

The Subsalt query interface is *read only* and only supported `SELECT` queries; writes and updates are not allowed and must be applied to the original data source.

Subsalt currently supports inner joins, but does not support other join types (left joins, outer joins, etc).


# Data quality

Every query you issue to Subsalt generates a quality report that compares the synthetic data you received to the data in the source system. You can access this report in the **Queries** section of the Subsalt portal.

### Interpreting the report

In most cases there will be four sections of the quality report:

* Univariate similarity
* Bivariate similarity
* Multivariate similarity
* Machine learning efficacy

#### Univariate similarity

Univariate similarity measures how similar each field in your synthetic data is to its source, ignoring all other column dependencies. This is done using a variety of statistical tests like Kolmogorov-Smirnov and chi-squared tests. Each field has a score from 0-1, where 1 means the synthetic data has a statistically identical distribution for the given field, and 0 means it was not at all similar. There is also a set of side by side histograms/bar charts to help interpret these scores.

<figure><img src="https://1365759658-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FX4JpqAkQu4oYdnmHdEWB%2Fuploads%2F6fL4yniYRIkc1KPC5e8g%2Fvehicle-age.png?alt=media&amp;token=53e590b0-5b32-4fa5-87ab-7ca97593618f" alt=""><figcaption></figcaption></figure>

#### Bivariate similarity

Bivariate similarity measures how similar each pair of fields in your synthetic data is to its source by assessing the similarity of relationships between pairs of fields in the synthetic vs source data.&#x20;

* For continuous fields, the score is based on correlations where a score of 1 means there is no difference in correlation between the real and synthetic data and 0 means completely inverse correlations.&#x20;
* For categorical fields, the score is calculated by creating a contingency table for both the source and synthetic data, then computing the difference between each cell in the source/synthetic contingency table. A score of 1 means there is no difference between the contingency tables, while 0 means they are extremely dissimilar.

<figure><img src="https://1365759658-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FX4JpqAkQu4oYdnmHdEWB%2Fuploads%2FvbnVHbTTsVTajiD9sCoU%2Fbivariate-similarity.png?alt=media&amp;token=0c37c0ba-dbcf-4bc1-bbbf-690b5f1637bf" alt=""><figcaption></figcaption></figure>

#### Multivariate similarity

Multivariate similarity measures how well deeper multivariate relationships are maintained in your synthetic data. Currently multivariate similarity only considers continuous fields, as it is based on a principal component analysis.&#x20;

Your data will go through a PCA reduction to plot the source data on a 2D plane (using the first two principal components). We use the same PCA to plot the synthetic data onto the same 2D plane. Similar deep multivariate relationships will yield similarly shaped pictures.

<figure><img src="https://1365759658-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FX4JpqAkQu4oYdnmHdEWB%2Fuploads%2FDNa3GOf5b9hE3dqRf3n7%2Fpca.jpg?alt=media&amp;token=f489da8b-0e2e-44ac-9732-1fa917034e59" alt=""><figcaption></figcaption></figure>

#### Machine learning efficacy

ML efficacy is measured by running an A/B test on model accuracy for a model trained on the source data vs a model trained on synthetic data. The scores reported are based on the accuracy when predicting on the same set of real holdout data from the source. This is meant to replicate a pattern where you develop models on synthetic data and use them in a production setting on real data.

In all cases you will see a table with model scores, populated based on the target type. Continuous targets will get scores like MSE while categorical targets will get scores like AUC. You will also always see a feature importance plot comparing the two models' reasoning. Depending on specific attributes of your target, you may also see ROC curves or cumulative gain plots.

<figure><img src="https://1365759658-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FX4JpqAkQu4oYdnmHdEWB%2Fuploads%2F7xYMpdqTVknjKWsF314q%2FScreenshot%202025-07-08%20at%204.06.26%E2%80%AFPM.png?alt=media&amp;token=cf831538-67f2-434a-b755-f5d8ec76a8b5" alt=""><figcaption></figcaption></figure>


# Projects

Subsalt allows you to set a project ID for a session as a data consumer. Queries for that session will be tagged with the project ID and auditable by organization administrators.

### Setting project IDs

To set your project ID via SQL, set the `subsalt.project_id` property in your SQL session prior to running your queries. For example:

```sql
SET subsalt.project_id TO "my_project";
```

#### Requiring projects

As an organization administrator, you can require that project IDs be set before any queries are issued. To do this, navigate to **Organizations > Projects** and enable the "Require projects" setting.

#### Auditing projects

As an organization administrator, you can audit projects in two ways:

#### **Summary**

To view a summary of queries issued for projects, navigate to **Organizations > Projects**.

These project summaries are grouped by:

* **Project:** The project ID
* **Organization:** The organization that the user belongs to
* **User:** The user that issued the query
* **Database:** The synthetic database that was queried

**Query-by-query**

To view a breakdown of queries issued for projects, navigate to Organization > Projects and click the "Download project query report" link.

This will download a CSV report of all queries issued that have been tagged with a project ID.

It will include the following information per row:

* **ProjectId:** The project ID
* **Organization:** The organization that the user belongs to
* **QueryId:** The internal ID of the query issued
* **UserId:** The internal ID of the user that issued the query
* **UserEmail:** The email of the user that issued the query
* **Database:** The synthetic database that was queried
* **QueryOutcome:** Whether or not the query was successful
* **QueryRequestedAt:** When the user issued the query (UTC date time)
* **QueryExecutionStartedAt:** When the query started to execute (UTC date time)
* **QueryExecutionCompletedAt:** When the query completed execution (UTC date time)


# Data request

Data Requests provide an AI-powered workflow for users to describe, in natural language, what data they need from on-boarded databases. The system analyzes the request and recommends a set of columns and tables that best satisfy it, ultimately producing a synthetic view that the user can query.

### Overview

The Data Request workflow consists of four stages:

1. **Describe Data Requirements** — Submit a natural-language description of the data you need.
2. **Review Data Dictionary** — Review and configure the AI-generated recommendation (columns, preferences, filters).
3. **Processing** — Once approved, the system automatically creates and builds the synthetic view(s).
4. **Results** — Access the generated view(s) via CSV export, PostgreSQL, or Python.

***

### Creating a Data Request

To create a new request, navigate to **Data Requests → New Request**.

#### Selecting a database

Choose the target database from the dropdown. Only databases you have read access to will be shown.

#### Describing your requirements

Enter a natural-language description of the data you are looking for in the text area. The system will use this prompt — along with metadata from the selected database — to generate a recommendation.

> **Tip:** The more specific your request, the better the recommendation. Include details such as table names, column names, date ranges, or any constraints you have in mind. Responses are non-deterministic and may require refinement.

#### Refining your request

After an initial recommendation is generated, you can refine your requirements by clicking **Refine Requirements**. This allows you to update your prompt and resubmit for a new recommendation. Click **Cancel Refining** to discard changes and return to the current recommendation.

***

### Reviewing the Data Dictionary

Once the system processes your request, it produces a **data dictionary** — a table of recommended columns that best answer your query. Each entry includes:

| Field           | Description                                                                        |
| --------------- | ---------------------------------------------------------------------------------- |
| **Table**       | The source table the column belongs to.                                            |
| **Column Name** | The name of the column.                                                            |
| **Data Type**   | The column's data type (e.g. `string`, `integer`, `float`, `boolean`, `datetime`). |
| **Description** | A brief description of the column's contents.                                      |
| **Preference**  | Whether the column is **Required** or **Optional**.                                |

#### Column preferences

Each column in the data dictionary can be marked as one of two preferences:

* **Required** — The column is essential to your request and must be included in the final output. A view containing only Required columns is always generated, ensuring you receive a guaranteed result. At least one column must be marked as Required to proceed.
* **Optional** — The column is desired but not critical. When Optional columns are present, the system generates a second view that includes both Required and Optional columns. This second view represents the ideal output; however, it may fail to build if the inclusion of Optional columns introduces privacy risks. In that case, the Required-only view remains available.

To change a column's preference, use the dropdown in the **Preference** column of the data dictionary table.

> **Note:** You must have at least one column marked as **Required** before you can submit the request for approval. If no Required columns are set, the **Process Data Request** button will be disabled.

#### Adding new columns

If the AI-generated recommendation is missing a column you need, you can add it manually:

1. Click the **Add entry** button below the data dictionary table.
2. A new row will appear with a column selector dropdown. This dropdown lists all available columns from the database that are not already in the data dictionary.
3. Select the desired column.
4. Optionally, enter a **description** for the column.
5. Set the column **preference** (defaults to Required).
6. Click the **checkmark** button to save the entry, or the **trash** button to cancel.

#### Removing columns

To remove a column from the data dictionary, click the **trash icon** on the corresponding row.

> **Note:** You cannot remove the last remaining column from the base table. The system requires at least one column from the base table to be present.

#### Translation notes

After the recommendation is generated, the system may display **translation notes** below the data dictionary. These provide context about how the AI interpreted your request:

* **FYI** — Informational notes about assumptions the system made.
* **WARNING** — Potential issues that may affect the output.
* **CRITICAL** — Significant issues that may affect the output.

***

### Configuring Data Filters

Data filters allow you to narrow the rows included in the final output by applying WHERE-clause conditions. Filters are configured in the **Data Filters** section below the data dictionary.

#### Filter structure

Each filter consists of three parts:

| Part             | Description                                                  |
| ---------------- | ------------------------------------------------------------ |
| **Column**       | The column to filter on (selected from the data dictionary). |
| **Operation**    | The comparison operator to apply.                            |
| **Column/Value** | Either a literal value or another column to compare against. |

#### Comparing against a value vs. a column

By default, the right-hand side of a filter is a **literal value** (e.g., `42`, `"active"`). You can toggle to **column mode** using the toggle button to compare against another column in the data dictionary (e.g., `start_date < end_date`).

> **Note:** When comparing two columns, both must have the same data type. A warning will be displayed if there is a type mismatch.

#### Available operations

The operations available depend on the data type of the selected column:

| Operation                | Symbol  | integer | float | boolean | string | datetime |
| ------------------------ | ------- | ------- | ----- | ------- | ------ | -------- |
| Equals                   | `=`     | Yes     | Yes   | Yes     | Yes    | Yes      |
| Not Equals               | `!=`    | Yes     | Yes   | Yes     | Yes    | Yes      |
| Less Than                | `<`     | Yes     | Yes   |         |        | Yes      |
| Less Than or Equal To    | `<=`    | Yes     | Yes   |         |        | Yes      |
| Greater Than             | `>`     | Yes     | Yes   |         |        | Yes      |
| Greater Than or Equal To | `>=`    | Yes     | Yes   |         |        | Yes      |
| Like                     | `%`     |         |       |         | Yes    |          |
| In                       | `(...)` | Yes     | Yes   |         | Yes    | Yes      |
| Not In                   | `(...)` | Yes     | Yes   |         | Yes    | Yes      |

* **In / Not In** operations accept multiple values. When selected, the input switches to a multi-value entry where you can type and add individual values.
* **Like** supports SQL-style pattern matching (e.g., `%smith%`).

#### Filter groups (OR logic)

Filters can be organized into **groups**. Within a group, all filters are combined with **AND** logic. Between groups, filters are combined with **OR** logic.

For example:

* **Group 1:** `age > 30` AND `status = "active"`
* **Group 2:** `role = "admin"`

This produces: `(age > 30 AND status = "active") OR (role = "admin")`

To add a new filter group, click **Add filter group**. To add an additional filter within an existing group, click **Add filter to group** within that group's section.

#### Type validation

The system validates that entered values match the expected column type. A warning is displayed if:

* The entered value does not match the column's data type (e.g., entering text for an integer column).
* The selected operation is not compatible with the column type.
* Two columns being compared have different data types.

***

### Submitting for Approval

Once you have configured the data dictionary and filters, click **Process Data Request** to submit the request for review. A reviewer with the appropriate permissions will be notified.

Reviewers can:

* **Approve** the request — optionally with a comment. Approval triggers automatic view creation.
* **Deny** the request — a comment is required explaining the reason for denial.

***

### Processing

After approval, the system automatically creates the synthetic view(s). This stage involves:

1. **Initialization** — The view is registered and configured.
2. **Scanning** — The source tables are scanned for metadata.
3. **Synthesis** — Synthetic data is generated based on the configured columns and filters.

Progress is displayed in real time. If an error occurs during processing, a **Recreate views** button allows you to retry.

***

### Retrieving Data

Once the view(s) have been successfully created, you can access the data through several methods:

#### CSV Export

Click the **Download** button to export the full dataset as a CSV file.

#### PostgreSQL

A pre-configured `psql` command is provided that you can copy and run directly in your terminal to connect and query the view.

#### Python

A sample Python script using `psycopg2` and `pandas` is provided with pre-filled connection parameters and query, ready to run.

***

### Reconfiguring a Request

If the generated view does not meet your needs, you can click **Reconfigure request** to return to the review stage. This allows you to adjust columns, preferences, and filters before resubmitting.

> **Note:** Reconfiguring a request will require reapproval by a reviewer before the views are rebuilt.


# MCP

Connect an AI agent to Subsalt over the Model Context Protocol to explore   schemas, query synthetic data, and compute legally de-identified answers on   real data.

Subsalt exposes a **Model Context Protocol (MCP)** server so AI agents — Claude, Cursor, and any other MCP-compatible client — can work with your data through a single authenticated connection. An agent connected to Subsalt can explore what data exists, iterate on fully synthetic data, and compute high-precision answers on real data that are legally de-identified before they ever reach the model.

The guiding principle is the same one behind the rest of Subsalt: **neither the agent nor the user ever touches a sensitive row.** The MCP server only ever returns metadata, synthetic data, or privacy-banded aggregates.

{% hint style="info" %}
MCP is an open standard for connecting AI applications to external tools and data. If your client supports MCP, it can connect to Subsalt with no custom code — see connecting-a-client.md.
{% endhint %}

#### What an agent can do

The server groups its tools into three components, which together support the full analyst workflow:

<table><thead><tr><th width="220">Component</th><th>What it provides</th></tr></thead><tbody><tr><td><strong>Schema &#x26; metadata</strong></td><td>The canonical description of what data exists — databases, tables, and columns with types, descriptions, sample values, and foreign keys. Safe to read; it describes structure, not sensitive values.</td></tr><tr><td><strong>Synthetic data</strong></td><td>Connection details for every database's representative synthetic data over a standard PostgreSQL interface — a fast, safe loop for exploring distributions and developing an analysis.</td></tr><tr><td><strong>Secure Compute</strong></td><td>A way to run an analysis script against <em>real</em> data inside an isolated environment and get back only privacy-banded aggregates — a precise answer that is legally de-identified under HIPAA.</td></tr></tbody></table>

For the complete tool list and the recommended workflow that ties these together, see tools-and-workflow\.md.

#### How it fits with the rest of Subsalt

The MCP server is an alternative front door to the same platform described elsewhere in these docs. It authenticates with your normal Subsalt identity (including SSO), and every request it makes is authorized exactly as it would be through the portal or the PostgreSQL endpoint — an agent can only see the databases you can see.

* Prefer a SQL client or the portal for direct, interactive querying of synthetic data? See running-queries.md.
* Want an AI-assisted, natural-language path to a synthetic view without wiring up an agent? See data-request.md.

#### Getting started

1. Connect your MCP client to the Subsalt MCP endpoint and sign in.
2. Ask the agent to call the **`instructions`** tool first — it returns a getting-started guide and lists the databases available to you.
3. Explore, iterate on synthetic data, and confirm answers against real data with Secure Compute, following the standard workflow.


# Connecting a client

## Connecting a client

The Subsalt MCP server speaks the **Streamable HTTP** transport and authenticates with **OAuth 2.0**. Supported clients must send the required OAuth client ID. Sign-in happens in your browser against your normal Subsalt identity provider — the same login you use for the portal, including SSO — so there is no separate API key to manage.

#### Endpoint

Point your client at your deployment's MCP endpoint:

```
https://mcp.<your-subsalt-domain>/mcp
```

{% hint style="info" %}
Your exact endpoint hostname is specific to your deployment. If you're not sure what it is, ask your Subsalt administrator or check the **Connection details** in the Subsalt portal.
{% endhint %}

The first time a client connects, it opens a browser window for sign-in and authorization. Your client stores and refreshes the resulting token. If a session expires, sign in again. This uses standard OAuth 2.0 discovery ([RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)).

***

#### Claude Code

Add the server from the command line, then authenticate:

```bash
claude mcp add --transport http --client-id subsalt-mcp subsalt https://mcp.<your-subsalt-domain>/mcp
```

Run `/mcp` inside Claude Code to complete the browser sign-in and confirm the connection is live.

{% hint style="warning" %}
The MCP endpoint requires OAuth and the `subsalt-mcp` client ID. Clients that cannot configure this client ID are not currently supported.
{% endhint %}

***

#### Verifying the connection

Once connected, ask your agent to call the **`instructions`** tool. It returns a short getting-started guide and lists the databases available to you — a quick confirmation that authentication succeeded and that your access is what you expect. From there, follow the standard workflow.


# Tools and workflow

## Tools and workflow

The Subsalt MCP server exposes a focused set of tools across three components. This page is a reference for each tool and the recommended workflow that ties them together.

#### Schema & metadata

The canonical description of what data exists. Start here to understand the shape of the data before writing any analysis — this metadata describes structure, not sensitive values, so it is always safe to read.

<table><thead><tr><th width="342.76171875">Tool</th><th>Returns</th></tr></thead><tbody><tr><td><code>instructions</code></td><td>The getting-started guide plus the databases available to you. Call this first.</td></tr><tr><td><code>db_list_databases</code></td><td>The databases you can access, with descriptions and whether each is queryable yet.</td></tr><tr><td><code>db_get_database_details(database_id)</code></td><td>A database's description, source connector, queryability, and its tables.</td></tr><tr><td><code>db_list_tables(database_id)</code></td><td>The tables in a database, with names, namespaces, and source connector.</td></tr><tr><td><code>db_get_table_details(database_id, table_id)</code></td><td>A table's columns: types, descriptions, sample values, and foreign keys.</td></tr></tbody></table>

{% hint style="info" %}
**You always write PostgreSQL.** Each database reports a **Source** (Azure, S3, Snowflake, …) — that is provenance only. Both query paths below are PostgreSQL: the synthetic endpoint *is* a Postgres server, and Secure Compute scripts are written in PostgreSQL and transpiled to the source automatically.
{% endhint %}

#### Synthetic data

Every database serves representative **synthetic data** over a standard PostgreSQL interface. Use it to iterate quickly and safely: understand distributions, sanity-check joins and filters, and develop an analysis on data that is safe to look at.

<table><thead><tr><th width="330">Tool</th><th>Returns</th></tr></thead><tbody><tr><td><code>synthetic_get_connection_info(database_id)</code></td><td>The PostgreSQL connection details (host, port, database, username, password) for querying a database's synthetic data directly.</td></tr></tbody></table>

The agent connects **directly** to the returned endpoint with any Postgres client — it does not move result sets back through MCP, which keeps large result sets off the model's context. The password returned is the caller's own time-limited access token, so no separate Subsalt credential is needed. See running-queries.md for more on the query interface and supported SQL.

{% hint style="warning" %}
Synthetic data is statistically representative but is **not** the real data. Treat any numbers from it as approximate — a fast development loop — and confirm the final answer against real data with Secure Compute.
{% endhint %}

#### Secure Compute

When you need a precise answer grounded in the **real** data, the agent submits an analysis script to Secure Compute. The script runs against real data inside an isolated environment and returns only **privacy-banded aggregates**: the result is high-precision yet legally de-identified under HIPAA, and neither the agent nor the user ever sees a sensitive row.

<table><thead><tr><th width="360">Tool</th><th>Returns</th></tr></thead><tbody><tr><td><code>secure_compute_get_capabilities</code></td><td>The metrics you may compute, the column privacy roles they may target, and the banding/suppression rules applied before results leave the environment.</td></tr><tr><td><code>secure_compute_get_script_guide</code></td><td>How to write the script: the data-access and typed-output API, the result shape, and a worked example.</td></tr><tr><td><code>secure_compute_submit_analysis(database_id, script)</code></td><td>Submits a script; returns an analysis ID immediately. The run is asynchronous.</td></tr><tr><td><code>secure_compute_get_analysis(analysis_id)</code></td><td>The status of a submitted analysis.</td></tr><tr><td><code>secure_compute_get_analysis_results(analysis_id)</code></td><td>The privacy-banded results, once the analysis is <code>Complete</code>.</td></tr><tr><td><code>secure_compute_list_analyses</code></td><td>The analyses you have submitted, newest first, with their statuses.</td></tr></tbody></table>

Secure Compute runs asynchronously: submit a script, poll the status, then fetch results once the analysis reaches `Complete`. Compose the script **against the capabilities contract up front** — anything outside the supported metrics or allowed column roles is rejected or suppressed.

{% hint style="warning" %}
Results are coarsened: counts are rounded and small cells are suppressed for privacy purposes.
{% endhint %}

***

#### The standard workflow

These tools are designed to be used in sequence. A typical analyst session looks like this:

1. **Understand the question.** Clarify what the user wants to learn — usually in plain language, not schema or SQL terms.
2. **Explore the schema** with the `db_*` tools to find the databases, tables, and columns relevant to the question.
3. **Iterate on synthetic data.** Get connection info with `synthetic_get_connection_info`, connect directly, and build up the analysis — explore distributions, refine filters and joins, and write the script. This is the fast, safe development loop.
4. **Confirm against the capabilities contract** with `secure_compute_get_capabilities`, adjusting the script so every metric and column it uses is permitted. Read `secure_compute_get_script_guide` and write the script against that API.
5. **Run against real data.** Submit with `secure_compute_submit_analysis`, poll with `secure_compute_get_analysis`, and fetch the banded result with `secure_compute_get_analysis_results`.
6. **Report the de-identified result**, presenting banded values as approximate and explaining that they were computed without exposing any sensitive record.

#### Key points

* **Synthetic ≠ real.** Synthetic data is for exploration and development; Secure Compute is the source of a trustworthy answer.
* **The agent never touches sensitive data.** Only metadata, synthetic data, and privacy-safe aggregates ever leave the environment.
* **Access mirrors your own.** The agent authenticates as you and can only reach the databases you can reach.


# Authentication & SSO

Subsalt can integrate with many [SSO identity providers](https://workos.com/docs/integrations) and can also self-manage accounts via an internally-hosted [Keycloak instance](https://www.keycloak.org/). Organizations are limited to *either* SSO via an identity provider or Subsalt-managed accounts; if you need to support both then you can create multiple organizations to manage more complex user configurations.

Read more about [User management](/administration/user-management) for Subsalt-managed accounts.

### Authenticating to query endpoint

All users must use a Subsalt-managed password for connecting to the query endpoint; users can update their passwords themselves in the **Profile > Access credentials** section of the portal.

Administrators can also reset passwords for other users in the **Organization > Accounts** section of the portal. Read more about this functionality in the [User management](/administration/user-management) section.

{% hint style="info" %}
Subsalt requires that users in SSO-based organizations create a dedicated password for [Running queries](/retrieving-data/running-queries); this security precaution ensures that the Subsalt system is never exposed to sensitive credentials that it's not responsible for managing.
{% endhint %}


# Access control and sharing

Subsalt provides multiple ways for administrators to configure granular access to various databases, tables, and views of the application.

## Database access

Database access can be provided to registered users via **Databases >&#x20;*****\[your database]*****&#x20;> Permissions > Grant Access.** Depending on the role granted, the user(s) may be also need to be explicitly granted access to the tables/views associated with the database.

### Table/view access

Explicit table and view access may be granted or revoked for users. This may be done via ***\[your database] > Permissions > \[your user(s)] > Edit access***.&#x20;

## Roles

<table><thead><tr><th width="131.14373779296875">Role</th><th>Description</th></tr></thead><tbody><tr><td><strong>Guest</strong></td><td>A user with no access to databases by default. Access to databases and associated tables and views must be explicitly granted.</td></tr><tr><td><strong>User</strong></td><td>A user with <strong>read-only</strong> access to all databases by default as well as associated tables and views.</td></tr><tr><td><strong>Administrator</strong></td><td>A user that has <strong>read</strong> and <strong>write</strong> access to all databases by default and associated tables and views. Administrators can create and manage databases as well users.</td></tr></tbody></table>


# Data use agreements

### Set data use agreement policies

Subsalt provides a mandatory data use agreement for data consumers in most configurations. If your instance supports it, organization admins can set the policy for data use agreements as it applies to internal or external users.

The policy options are:

* **Required**: Users must accept the data use agreement for the organization before they can query any database.
* **Skip**: Users will not be required to accept, and will not see, a data use agreement.

The audiences for policy options are:

* **Internal**: Users that are a part of the organization that the database is associated with.
* **External**: Users outside of the organization. Access to specific databases for the organization has been shared explicitly.


# User management

Administrators can add or remove users from the organization from the **Organization > Accounts** section of the portal.

### Add a user

Add a new user by clicking the "add" button in the upper left. Complete the form in the next step, then click "add user". An email will be sent to the invited user with the subject *"You have been added to \[your organization name] organization"*.&#x20;

For Subsalt managed organizations using SSO, the email will provide a login link. If the organization account is Subsalt managed (*not* using SSO), the email will prompt the new user to set their password via a link prior to logging in.

#### External Users <a href="#external-user" id="external-user"></a>

Users can be marked as `External` during user creation via the **External User** checkbox. This allows creation of Subsalt managed (*not* using SSO) user accounts that have limited access to the organization's datasets. Guest users will not have access to any databases by default; access can be granted as needed via [Access control and sharing](/administration/access-control-and-sharing) functionality.

### Modify user roles

You can assign a new role to a user by clicking on their existing role on the **Accounts** page, and selecting a new role from the dropdown. You can read more about available roles in [Access control and sharing](/administration/access-control-and-sharing).

### Remove a user

Remove a user by clicking the "x" icon to the far right of a user's entry on the **Accounts** page. Confirm deletion by clicking the "remove" button. Removed users will immediately lose access to the Subsalt system.


# Directory sync

Organizations that use an SSO identity provider that supports SCIM-based user provisioning can automate user management.

### Requirements

* SCIM-based directory system
* Groups in that directory system that map to the following Subsalt roles
  * Admin
  * Viewer

### Configuration

To configure directory sync, find the SCIM-based integration that matches your identity provider [here](https://workos.com/docs/integrations).&#x20;

Email <support@getsubsalt.com> with a request to configure directory sync for that provider and we will send you a WorkOS link that enables you to complete the integration by following the documentation for your identity provider.

After you have completed the documented steps for your identity provider and initial provisioning has completed, the groups you created in your directory for the Subsalt roles should be visible in the WorkOS dashboard under the **Groups** tab.

<figure><img src="https://1365759658-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FX4JpqAkQu4oYdnmHdEWB%2Fuploads%2FKnyBVfGcbs3cCiEiJO0U%2Fimage.png?alt=media&amp;token=65abbfdd-d333-43f0-ae31-b889c78317ce" alt=""><figcaption></figcaption></figure>

From the **Directory Settings** tab, press **Configure role assignment** and assign your groups to their respective roles in Subsalt.

<figure><img src="https://1365759658-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FX4JpqAkQu4oYdnmHdEWB%2Fuploads%2FAoPDYKEJ7bEweIUkj0Im%2Fdashboard.workos.com_environment_01GGDM4H4X20PS9R8XPBQCJ26V_directory-sync_connections_directory_01K7KZDVZRXXH49FF2DCGVEVVP.png?alt=media&amp;token=99abcdf9-d4ec-46c0-9852-f80aa3a6cafa" alt=""><figcaption></figcaption></figure>

By default, all users are assigned the **Viewer** role if their role cannot be derived from group role assignments.

### Sync schedule

Subsalt will attempt to synchronize with WorkOS at the top of every hour. Your directory system will have its own independent provisioning interval with WorkOS.

The maximum delay in seeing updates from your directory system in Subsalt will be **1 hour + your directory systems provisioning interval**. For example, Azure Entra ID triggers provisioning every **40 minutes** so the max delay is **1 hour 40 minutes**.

### Sync events

When the following events occur in your directory system they will be reflected in Subsalt the next time the system synchronizes with WorkOS.

#### **User added**

The user will be created in Subsalt. They will be assigned the role associated with their group.

#### User updated

The user's **first name**, **last name**, and/or **role** will be updated in Subsalt.

#### User removed

The user will be deactivated in Subsalt.

#### Directory integration disabled

All users associated with the directory will be deactivated in Subsalt.

### Users outside of the directory

It is still possible to add/remove/update users directly in the Subsalt application. If the user is added to the directory system at a later date it will be "adopted" as a directory user the next time Subsalt synchronizes with WorkOS and all sync events will apply going forward.

All [users marked as **External**](/administration/user-management#external-user) are ignored by directory sync.


# Disaster recovery

Subsalt's system supports comprehensive disaster recovery strategies for restoring availability and data in the event of an unexpected outage or data loss/corruption.

#### Data replication

Subsalt leverages your cloud provider's block storage solution(s) for persisting data, which often have built-in resilience options available.

#### Automated back-ups

Subsalt supports each cloud provider's recommended automated Kubernetes backup solution to protect against scenarios where entire clusters are destroyed. These managed solutions provide automated, periodic backups of persistent storage to an out-of-cluster location that can be restored as needed. Recommended back-up solutions include:

* [Backup for GKE](https://cloud.google.com/kubernetes-engine/docs/add-on/backup-for-gke/concepts/backup-for-gke) on Google Cloud
* [Azure Kubernetes Service Backup](https://learn.microsoft.com/en-us/azure/backup/azure-kubernetes-service-backup-overview) on Azure
* [EKS Backup using Velero](https://aws.amazon.com/blogs/containers/backup-and-restore-your-amazon-eks-cluster-resources-using-velero/) on AWS

Third-party tools like [backy2](https://backy2.com/), [restic](https://restic.net/), and [Velero](https://velero.io/) that support filesystem-level backups are compatible with Subsalt installations as well.

#### Velero

For AKS deployments, Subsalt also supports [Velero](https://velero.io/) for backup and restore. Velero captures Kubernetes resources and persistent volume data, storing them in Azure Blob Storage. It authenticates using Azure Workload Identity, eliminating the need for static credentials in the cluster.

See the Velero backup and restore page for setup and usage details.


# Velero backup and restore

[Velero](https://velero.io/) is an open-source tool for backing up and restoring Kubernetes cluster resources and persistent volumes. Subsalt supports Velero on AKS with Azure Workload Identity for credential-free authentication.

### Overview

Velero backs up two categories of data:

* **Kubernetes resources** — Deployments, Services, ConfigMaps, Secrets, etc.
* **Persistent volume data** — CSI volume snapshots are taken and transferred to Azure Blob Storage via [Kopia](https://kopia.io/), ensuring backup data is stored outside the cluster.

Authentication to Azure Blob Storage uses [Azure Workload Identity](https://learn.microsoft.com/en-us/azure/aks/workload-identity-overview), which injects short-lived tokens into Velero pods automatically. No storage account keys or static credentials are stored in the cluster.

### Setup

Velero is installed via the official Helm chart from the [vmware-tanzu](https://vmware-tanzu.github.io/helm-charts) repository. Setup requires:

1. **An Azure Managed Identity** for Velero with a federated credential bound to the Velero ServiceAccount
2. **RBAC role assignments** — `Storage Blob Data Contributor` on the backup storage account and `Contributor` on the resource group for CSI disk snapshots
3. **An Azure Blob Storage container** to store backup data
4. **Helm values** configured with `useAAD: "true"` and `credentials.useSecret: false`

For detailed setup instructions, refer to the [Velero Plugin for Microsoft Azure](https://github.com/vmware-tanzu/velero-plugin-for-microsoft-azure#option-3-use-azure-ad-workload-identity) documentation.

{% hint style="info" %}
When using the Velero Helm chart (rather than the `velero install` CLI), the ServiceAccount is named `velero-server`, not `velero`. The federated identity credential subject must match: `system:serviceaccount:velero:velero-server` unless a custom name is specified.
{% endhint %}

### Usage

Official velero documentation can be found here: <https://velero.io/docs/main/>. The following are simple examples for getting started.

#### Creating a backup

To back up the Subsalt namespace:

```bash
velero backup create my-backup --include-namespaces subsalt --wait
```

Check the backup status:

```bash
velero backup describe my-backup
```

#### Restoring from a backup

To restore from a backup:

```bash
velero restore create --from-backup my-backup --wait
```

Verify the restore:

```bash
velero restore describe <restore-name>
kubectl get pods -n subsalt
```

{% hint style="info" %}
Persistent volumes use CSI snapshots with data movement. New volumes are provisioned during restore and populated from blob storage. Pods will remain in `Pending` until their volumes are ready.
{% endhint %}

#### Scheduling recurring backups

To create an automated backup schedule:

```bash
velero schedule create daily-subsalt \
  --schedule="0 2 * * *" \
  --include-namespaces subsalt \
  --ttl 168h
```

This creates a daily backup at 2:00 AM UTC and retains backups for 7 days. Adjust the cron expression and TTL as needed.

List existing schedules:

```bash
velero schedule get
```


