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

# Architecture

> How Confiroll fits together: a thin BFF, a headless SDP, Soroban contracts, and a browser that does the confidential heavy lifting.

Confiroll is a `pnpm`/`apps/` monorepo. The design principle behind everything below: **the
confidential work happens in the browser, and the server stays thin.** Confiroll's backend
holds no user keys. It authenticates sessions, fee-bumps already-signed transactions, and
relays them to the chain.

Two live hosts run the deployment: the `payroll-api` BFF at `https://api.confiroll.com`, and
the headless SDP at `https://sdp.confiroll.com`. Everything settles on the Stellar testnet.

## The pieces

<CardGroup cols={2}>
  <Card title="Web app (apps/web)" icon="window">
    Vite + React + TypeScript + Tailwind + shadcn. Sign-in with Stellar Wallets Kit (SEP-10)
    or Privy (email). This is where confidential amounts are encrypted, proofs are built, and
    balances are decrypted.
  </Card>

  <Card title="payroll-api (apps/payroll/api)" icon="server">
    A Fastify BFF (backend-for-frontend). Issues session JWTs, and exposes the core
    `POST /transfer` that fee-bumps a browser-signed confidential transfer. Holds operational
    keys only, never yours.
  </Card>

  <Card title="Confidential pipeline (apps/payroll/client)" icon="lock">
    The TypeScript toolkit and vendored `ctd-sdk` (OpenZeppelin confidential-token crypto +
    Noir/bb.js proving) that implements register, deposit, transfer, merge, and withdraw, plus
    the fee-bump sponsor.
  </Card>

  <Card title="Soroban contracts (contracts/)" icon="file-contract">
    <code>payslip-anchor</code> (tamper-evidence), <code>cctp-vault</code> (bridged-USDC
    landing point), and <code>passkey-wallet</code> (seedless WebAuthn account). Deployed on
    testnet.
  </Card>

  <Card title="Headless SDP (apps/sdp)" icon="gears">
    A vendored Stellar Disbursement Platform (v6.6.1) used **only** as a channel-account pool

    * fee-bump + submit + retry, not its disbursement API. One small upstream patch.
  </Card>

  <Card title="Tools (tools/)" icon="wrench">
    Supporting CLIs: CCTP burn, Wallets-Kit signing helpers, cost and network-settings
    utilities.
  </Card>
</CardGroup>

## Repository layout

Every part of the system maps to one directory in the monorepo. When you read the source, this
is where each responsibility lives:

| Path                          | Role                  | What lives here                                                                                                                                 |
| ----------------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `apps/web`                    | The UI                | Vite + React + TypeScript + Tailwind + shadcn. Stellar Wallets Kit and Privy sign-in, client-side proving calls, session JWT in `localStorage`. |
| `apps/payroll/api`            | The Fastify BFF       | Session auth (SEP-10 and Privy), `POST /transfer` fee-bump, `POST /batch`, health. Holds operational keys only.                                 |
| `apps/payroll/client`         | Confidential pipeline | The TypeScript scripts for register, deposit, transfer, merge, withdraw, auditor disclosure, and the fee-bump sponsor.                          |
| `apps/payroll/vendor/ctd-sdk` | Proving + crypto      | OpenZeppelin confidential-token crypto plus Noir and bb.js zero-knowledge proving.                                                              |
| `contracts/`                  | Soroban contracts     | `payslip-anchor`, `cctp-vault`, `passkey-wallet`, compiled to Wasm and deployed on testnet.                                                     |
| `apps/sdp`                    | Headless SDP          | Vendored Stellar Disbursement Platform v6.6.1, used only as a channel-account pool, fee-bump, submit, and retry engine.                         |
| `tools/`                      | CLIs                  | CCTP burn, Wallets-Kit helpers, cost accounting, network-settings.                                                                              |

<Info>
  `apps/web` is the only part a signed-in person touches directly. `apps/payroll/client` and
  `apps/payroll/vendor/ctd-sdk` carry the confidentiality logic: the same proving code runs from
  CLI scripts on the testnet today and from the browser in the app.
</Info>

## How a confidential payout flows

The core write path (**Fork B**) keeps the employer's keys in the browser and lets Confiroll
sponsor the fee without ever holding a secret:

```mermaid theme={"system"}
sequenceDiagram
    participant B as Browser (employer)<br/>wallet key + viewing key
    participant API as payroll-api (BFF)
    participant S as Fee sponsor
    participant RPC as Soroban RPC
    participant CT as Confidential Token

    B->>B: 1. Build confidential_transfer<br/>(amount encrypted, ZK proof via bb.js)
    B->>B: 2. Sign envelope (employer = tx source)<br/>plain signTransaction
    B->>API: 3. POST /transfer { signedXDR }<br/>Authorization: Bearer <session>
    API->>API: 4. Verify session, tx.source == session.sub
    API->>S: 5. feeBump (allow-list, fee cap, quota)
    S->>RPC: 6. Submit CAP-15 fee-bump (sponsor pays fee)
    RPC->>CT: 7. confidential_transfer executes
    RPC-->>API: 8. SUCCESS + tx hash
    API-->>B: 9. { hash, status: "SUCCESS" }
```

The employer signs the *inner* transaction as its source account, so the confidential
token's `from.require_auth()` is satisfied by a **plain signature**, with no fragile
auth-entry signing. Confiroll wraps that signed transaction in a **fee-bump** and pays the
outer fee, so the employer spends **0 XLM**. The amount is never an argument. It lives
inside the proof. See [The non-custodial model](/developers/non-custodial-model) for why this
"Fork B" design is the pivotal decision.

## Request lifecycle: one Fork B transfer

Read the diagram above as a story. Here is the same path in prose, so you can see where each
guarantee comes from:

1. **You log in.** The browser runs SEP-10 against `payroll-api`: it fetches a challenge from
   `GET /auth/sep10/challenge?account=G...`, signs it with your wallet, and posts it to
   `POST /auth/sep10/verify`. The API returns a session JWT whose `sub` is your `G-address`.
   The challenge is single-use and expires in 5 minutes.
2. **The browser builds the transfer.** Using your viewing key `sk`, the client encrypts the
   amount to the recipient's public viewing key and generates the zero-knowledge proof with
   bb.js. The `confidential_transfer` call carries no plaintext amount.
3. **You sign the envelope.** Your wallet signs the inner transaction with a plain
   `signTransaction`. You are the transaction source, so source-account auth satisfies
   `from.require_auth()`. The signature never leaves as a secret: only the signed transaction
   travels.
4. **The browser posts `{ signedXDR }`.** The request goes to `POST /transfer` with your
   session JWT in the `Authorization: Bearer` header.
5. **The API checks the binding.** `requireSession` validates the JWT, then the route asserts
   `inner tx.source == session.sub`. If you try to fee-bump someone else's transaction, the API
   returns `403`. A Privy session cannot use this route and returns `501`; only SEP-10 sessions
   fee-bump.
6. **The sponsor validates before it signs.** It checks the contract allow-list, the fee cap,
   and the per-account quota. If it refuses, the API returns `422`. If it accepts, it builds a
   CAP-15 fee-bump with an outer fee of twice the inner fee, signs the bump, and submits over
   Soroban RPC.
7. **The chain settles.** The confidential token executes the transfer. The RPC polls to
   `SUCCESS`, and the API returns `{ hash, status: "SUCCESS" }`. The fee account on-chain is
   the sponsor, and your account moves by `0 XLM`.

The sponsor never needs the employer's secret. It only pays the outer fee. That single fact,
plus browser-side proving, is what makes the path non-custodial and gas-free at the same time.

## What runs where

| Concern                                           | Where it runs                        | Why                                                         |
| ------------------------------------------------- | ------------------------------------ | ----------------------------------------------------------- |
| Amount encryption, ZK proving, balance decryption | **Browser**                          | Needs your viewing key `sk`, which never leaves your device |
| Transaction signing                               | **Browser / wallet**                 | Non-custodial: Confiroll never holds the signing key        |
| Session auth (SEP-10 / Privy)                     | `payroll-api`                        | Issues short-lived session JWTs                             |
| Fee-bump + submit + retry                         | `payroll-api` sponsor / headless SDP | Sponsors the fee; holds only the sponsor key                |
| Confidential token, vault, anchor                 | **Soroban (testnet)**                | On-chain settlement and tamper-evidence                     |

## Design principles

Three rules explain almost every choice in the codebase:

<CardGroup cols={3}>
  <Card title="The browser does the confidential work" icon="lock">
    Encryption, proving, and decryption all need your viewing key `sk`. That key stays on your
    device, so the confidential logic runs client-side in `apps/payroll/client` and
    `vendor/ctd-sdk`.
  </Card>

  <Card title="The server stays thin" icon="feather">
    `payroll-api` authenticates sessions and fee-bumps signed transactions. It does not prove,
    does not decrypt, and does not decide amounts. Its job is small on purpose.
  </Card>

  <Card title="Confiroll holds zero user keys" icon="shield-halved">
    Neither your Stellar signing key nor your confidential `sk` reaches a Confiroll server. The
    only keys Confiroll holds are operational: the fee sponsor, the testnet USDC faucet, and
    SDP's channel accounts.
  </Card>
</CardGroup>

Because those three hold, amounts stay hidden from the public, from other employers, and from
Confiroll itself. That property has a name in these docs: *Confiroll-blind*.

## Keep reading

<CardGroup cols={2}>
  <Card title="The ecosystem" icon="diagram-project" href="/developers/ecosystem">
    Where Confiroll sits among Stellar, Soroban, the confidential token, SDP, and Circle CCTP.
  </Card>

  <Card title="The non-custodial model" icon="key" href="/developers/non-custodial-model">
    Two secrets per user, browser-side proving, and why Fork B is the pivotal decision.
  </Card>

  <Card title="Confidential token flow" icon="right-left" href="/developers/confidential-token-flow">
    Register, deposit, transfer, merge, and withdraw, and exactly what is public vs hidden.
  </Card>

  <Card title="Security and claims" icon="scale-balanced" href="/developers/security-and-claims">
    The confidentiality-not-anonymity boundary and the live-vs-designed line.
  </Card>
</CardGroup>

## FAQ

<AccordionGroup>
  <Accordion title="Why a BFF instead of calling Soroban RPC straight from the browser?">
    The browser could reach an RPC on its own, but then the employer would pay the fee and there
    would be no fee sponsorship. The BFF exists so Confiroll can fee-bump your signed transaction
    (you spend `0 XLM`) while still never holding your key. It also holds the SDP database
    credentials internally and never exposes them to the client.
  </Accordion>

  <Accordion title="Does the server ever see an amount or a secret?">
    No. The browser sends a signed transaction whose amount lives inside a proof and encrypted
    event fields, never a plaintext argument. `sk` is derived and used only on your device. The
    server sees a session JWT and a signed transaction, nothing more.
  </Accordion>

  <Accordion title="What is the difference between the SDP relay and the Fork B path?">
    They are two different write mechanisms. The deployed non-custodial path is Fork B:
    `POST /transfer` fee-bumped by the API sponsor. The SDP queue-relay (writing a row into
    `tss.submitter_transactions`) powers the batch path. See
    [SDP integration](/developers/sdp-integration) for the relay and
    [Fee sponsorship](/developers/fee-sponsorship) for the sponsor.
  </Accordion>
</AccordionGroup>

<Note>
  The confidential write path is proven end to end on Stellar testnet via the client scripts in
  `apps/payroll/client`, and this page describes that architecture. See
  [Security & claims](/developers/security-and-claims) for the live-vs-designed boundary.
</Note>
