> ## Documentation Index
> Fetch the complete documentation index at: https://docs.voltai.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Security and architecture

> How connectors authenticate, what data moves where, what we store, and what we never touch.

This page is written for the security reviewer who has to sign off on the integration. Everything below reflects how the system actually behaves today.

## The four things that matter most

<CardGroup cols={2}>
  <Card title="OAuth only" icon="key">
    Connections are OAuth 2.0 authorization-code flows. There is no code path that accepts a password, personal access token, or API key.
  </Card>

  <Card title="Each user's own permissions" icon="user-shield">
    We call your systems with the individual user's delegated token, so your existing ACLs apply unchanged. There is no shared service account.
  </Card>

  <Card title="Used for reads only" icon="book-open">
    Connector code issues search and fetch calls only. That is not the same as every OAuth scope being read-only: Salesforce's `api` scope can authorize writes the connecting user's profile already allows, even though we never call those operations.
  </Card>

  <Card title="No sync, no index" icon="database">
    We do not crawl, mirror, or index your content. Nothing is copied into a search index or vector store. Reads happen live, in response to a user's question.
  </Card>
</CardGroup>

## Connecting: the OAuth handshake

Your OAuth app issues a token scoped to one user. We store that token encrypted and never see the user's credentials.

```mermaid theme={"dark"}
sequenceDiagram
    actor User
    participant Voltai as Voltai API
    participant Provider as Your system

    User->>Voltai: Clicks "Connect"
    Voltai-->>User: Redirect to your provider's consent screen
    User->>Provider: Signs in and approves the requested scopes
    Provider-->>Voltai: Redirect to /connectors/oauth/callback/ with a one-time code
    Voltai->>Provider: Exchange code for access + refresh token
    Provider-->>Voltai: Tokens scoped to this user
    Voltai->>Voltai: Encrypt with AWS KMS, store against this user
    Voltai-->>User: Connected
```

A few details worth noting:

* The one-time authorization code is bound to a CSRF state value that **expires after 10 minutes**.
* Tokens are stored per **(user, organization, connector)** — one row per person, never pooled.
* Access and refresh tokens are encrypted with **AWS KMS** before they are written to the database, and decrypted only in memory for the duration of an API call.
* Refresh happens lazily when a token is close to expiry. Users do not have to reconnect on a schedule.

## Answering a question: the read path

Nothing is fetched on a timer. A connector is only touched when a user's question requires it.

```mermaid theme={"dark"}
flowchart LR
    Q["User asks a question"] --> Agent["Voltai agent"]
    Agent --> Tool["Connector tool call"]
    Tool --> Token["Decrypt that user's token via AWS KMS"]
    Token --> API["Your system's API over HTTPS (search / fetch)"]
    API --> Agent
    Agent --> Persist["Conversation.sources in PostgreSQL"]
    Persist --> Answer["Answer with citations back to your system"]
```

Because the call carries the asking user's own token, the provider decides what comes back. If a user cannot see a Slack channel or a Confluence space, neither can Voltai when answering for that user.

## What is stored, and what is not

<Note>
  Voltai does not crawl or index your corpus. It does persist connector evidence on the chat conversation that used it. Search hits store a truncated snippet. A `fetch_*` tool stores the **complete fetched document body** (Slack thread, Confluence page, Jira ticket, Salesforce record, and similar) on that conversation — not only the quoted excerpt shown in the UI.
</Note>

| Data                                                                                                                                                        | Stored?                | Where                                            | Lifetime                                                                                                                          |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
| Your OAuth client ID and secret                                                                                                                             | Yes                    | Your organization's connector configuration      | Until you rotate or remove the connector                                                                                          |
| Per-user access and refresh tokens                                                                                                                          | Yes, **KMS-encrypted** | PostgreSQL                                       | Until the user disconnects                                                                                                        |
| Connection metadata (external account ID, account label, granted scopes, provider metadata such as Atlassian site ID or Salesforce instance URL, last used) | Yes                    | PostgreSQL                                       | Survives disconnect. Disconnect clears tokens, expiry, and error state and marks the row revoked; it does not erase these fields. |
| A search index, embeddings, or vector store built from your content                                                                                         | **No**                 | —                                                | Never created                                                                                                                     |
| Search-hit snippets used as citations                                                                                                                       | Yes (truncated)        | `Conversation.sources` in PostgreSQL             | Deleted with the conversation                                                                                                     |
| Full documents returned by a `fetch_*` tool                                                                                                                 | **Yes**                | `Conversation.sources` (`content`) in PostgreSQL | Deleted with the conversation                                                                                                     |

So the precise version of "nothing is stored" is: **no bulk copy, no index, no background sync.** What survives a request is the conversation record: cited search snippets, and — when a fetch tool ran — the entire fetched document body in that source's `content` field. Deleting the conversation deletes those too.

## Deployment and network boundaries

```mermaid theme={"dark"}
flowchart TB
    Browser["Your users' browsers"]

    subgraph voltai["Voltai environment"]
        Edge["AWS WAF + Application Load Balancer (TLS only)"]
        App["Backend API — private subnet, no public ingress"]
        DB[("PostgreSQL — KMS-encrypted tokens, chat history")]
        KMS["AWS KMS"]
    end

    subgraph yours["Your systems"]
        Provider["Slack, Jira, Confluence, SharePoint, Salesforce, Assembla"]
    end

    Browser --> Edge --> App
    App --> DB
    App --> KMS
    App -->|"outbound HTTPS, per-user token"| Provider
```

* All inbound traffic terminates at an **AWS Application Load Balancer fronted by AWS WAF**, over TLS. Application containers run in private subnets with no public ingress.
* Calls to your systems are made **server-side from our backend**, outbound over HTTPS. Your users' browsers never talk to your provider APIs on our behalf, and tokens never reach the browser.
* Every request carries a 30-second timeout, so a slow or unavailable provider degrades one answer rather than the platform.

## Revoking access

You have two independent levers, and either one is enough to stop reads:

<Steps>
  <Step title="A user disconnects in Voltai">
    We immediately erase that user's stored access and refresh tokens, clear token expiry and last error, and mark the connection revoked. Subsequent questions cannot reach your system. The connection row remains, including external account ID, account label, granted scopes, provider metadata, and last used.
  </Step>

  <Step title="You revoke on your side">
    Deleting or disabling the OAuth app in Slack, Atlassian, Entra ID, Salesforce, or Assembla invalidates every token issued through it at once, for all users.
  </Step>
</Steps>

<Warning>
  Disconnecting inside Voltai deletes our copy of the token but does not call your provider's token-revocation endpoint. If your policy requires the token to be invalidated at the source, revoke the app authorization in the source product as well.
</Warning>

## Scopes we request, per connector

| Connector  | Scopes                                                                                                                                   | All read-only?                |
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
| Slack      | `search:read`, `channels:history`, `files:read`                                                                                          | Yes                           |
| Jira       | `read:jira-work`, `read:jira-user`, `offline_access`                                                                                     | Yes                           |
| Confluence | `search:confluence`, `read:confluence-content.summary`, `read:confluence-content.all`, `read:confluence-space.summary`, `offline_access` | Yes                           |
| SharePoint | `Sites.Read.All`, `Files.Read.All`, `offline_access`                                                                                     | Yes, delegated                |
| Salesforce | `api`, `refresh_token`, `id`                                                                                                             | No — `api` can include writes |
| Assembla   | `read`                                                                                                                                   | Yes                           |

`offline_access` and `refresh_token` exist only so we can refresh an expiring token without asking the user to reconnect. They grant no additional read access.

<Note>
  Two scopes deserve a comment in a security review. **SharePoint's** `Sites.Read.All` and `Files.Read.All` read broadly by name, but they are *delegated* permissions — they never exceed what the signed-in user can already open in Microsoft 365. **Salesforce's** `api` scope is not a read-only grant: it authorizes REST and SOAP API operations the connecting user's profile and sharing rules already permit, which can include create, update, and delete. Voltai only performs reads with that token.
</Note>

## Questions a reviewer usually asks next

<AccordionGroup>
  <Accordion title="Can Voltai write to our systems?">
    Connector code paths issue read and search calls only. For Slack, Jira, Confluence, SharePoint, and Assembla, the requested scopes are themselves read (or delegated read). Salesforce is the exception: `api` authorizes whatever the user's profile allows, including writes. We do not exercise those operations, but a reviewer should treat the granted Salesforce token as not read-only.
  </Accordion>

  <Accordion title="Does one user's connection expose data to their colleagues?">
    No. Tokens are stored per user and a question is always answered with the asking user's token. Two people asking the same question can legitimately get different answers, because your ACLs differ between them.
  </Accordion>

  <Accordion title="Is our content used to train models?">
    No. Retrieved content is passed to the model as context for that single answer. It is not added to a training corpus.
  </Accordion>

  <Accordion title="Where does the client secret live?">
    In your organization's connector configuration inside our database, used only to perform the OAuth code exchange and token refresh. Rotate it whenever you like and send us the new value — no user has to reconnect.
  </Accordion>

  <Accordion title="What happens if a token leaks?">
    Tokens are KMS-encrypted at rest and never leave the backend. If you suspect exposure, revoke the OAuth app on your side; that invalidates every token derived from it immediately.
  </Accordion>
</AccordionGroup>
