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

# Deployment Topology

> What runs where — Vercel, Fly.io, Temporal Cloud, Supabase — and why production and staging run different worker topologies

# Deployment Topology

Three Next.js apps on Vercel, one worker fleet on Fly.io, orchestrated by Temporal Cloud
against Supabase Postgres.

<Warning>
  **Production and staging run different worker topologies.** Production is one large process
  running the whole fleet; staging is four right-sized process groups. Do not reason about one from
  the other.
</Warning>

## Production — monolithic workers

```mermaid theme={null}
flowchart TB
    subgraph Vercel["Vercel"]
        WebAppP["@cona/webapp<br/>cona.app"]
        PortalP["@cona/portal"]
        ConsoleP["@cona/console"]
    end

    Temporal["Temporal Cloud<br/>33 task queues"]

    subgraph FlyProd["Fly.io fra · fly.prod.toml"]
        WorkerMono["process: app<br/>node dist/worker.js<br/>8 CPU / 8 GB · heap 6144 MB<br/>ALL 28 workers in one process<br/>≥2 Machines for HA"]
    end

    subgraph Stores["Data"]
        Postgres["Supabase Postgres<br/>via Supavisor pooler"]
        Redis["Redis"]
        Storage["Supabase Storage"]
    end

    WebAppP -->|start workflow| Temporal
    Temporal -->|task queue poll| WorkerMono
    WebAppP --> Stores
    PortalP --> Postgres
    ConsoleP --> Postgres
    WorkerMono --> Stores

    classDef boundary fill:#f8fafc,stroke:#94a3b8,color:#334155

    classDef app fill:#dbe2fb,stroke:#3B56C5,color:#111827
    classDef async fill:#e3f7ea,stroke:#2f9e5c,color:#111827
    classDef data fill:#fde8e8,stroke:#c53b3b,color:#111827

    class WebAppP,PortalP,ConsoleP app
    class WorkerMono,Temporal async
    class Postgres,Redis,Storage data
    class Vercel,FlyProd,Stores boundary
```

## Staging — split worker groups

Same Vercel and data layer; the worker fleet is split into four right-sized processes.

```mermaid theme={null}
flowchart LR
    Temporal["Temporal Cloud"]

    subgraph FlyStage["Fly.io fra · fly.staging.toml"]
        WImports["imports<br/>4 CPU / 4 GB · heap 3072<br/>19 workers"]
        WAccounting["accounting<br/>2 CPU / 2 GB · heap 1536<br/>2 workers"]
        WRecon["reconciliation<br/>2 CPU / 2 GB · heap 1536<br/>2 workers"]
        WMisc["misc<br/>2 CPU / 2 GB · heap 1536<br/>5 + 1 workers"]
    end

    Postgres["Supabase Postgres"]

    Temporal --> WImports
    Temporal --> WAccounting
    Temporal --> WRecon
    Temporal --> WMisc
    WImports --> Postgres
    WAccounting --> Postgres
    WRecon --> Postgres
    WMisc --> Postgres

    classDef boundary fill:#f8fafc,stroke:#94a3b8,color:#334155

    classDef async fill:#e3f7ea,stroke:#2f9e5c,color:#111827
    classDef data fill:#fde8e8,stroke:#c53b3b,color:#111827

    class WImports,WAccounting,WRecon,WMisc,Temporal async
    class Postgres data
    class FlyStage boundary
```

## Fly configs

Five files. Only two are live.

| File                      | App                             | Model                          | Live           |
| ------------------------- | ------------------------------- | ------------------------------ | -------------- |
| `fly.prod.toml`           | `cona-temporal-workers-prod`    | monolithic — one `app` process | **production** |
| `fly.staging.toml`        | `cona-temporal-workers-staging` | split — 4 groups               | **staging**    |
| `fly.split.prod.toml`     | `cona-temporal-workers-prod`    | split — 4 groups               | no             |
| `fly.split.staging.toml`  | staging                         | split                          | no             |
| `fly.legacy.staging.toml` | staging                         | legacy                         | no             |

<Warning>
  `fly.split.prod.toml` targets the **same app name** as the live `fly.prod.toml`. A `flyctl deploy
      -c fly.split.prod.toml` would silently replace the production fleet with a different topology.
  Deleting or renaming the unused configs would remove the risk.
</Warning>

All configs share: `rolling` deploy strategy, HTTP health check on `:8080/health` every
30s, `restart = always`, `SIGTERM` with `kill_timeout = 300` (worker force deadline is
4m30s, leaving 30s for connection, logger, and telemetry cleanup).

## Worker groups

`WORKER_GROUP` selects a subset at startup
(`apps/temporal-workers/src/workers/index.ts:61-74`). Unset starts the whole fleet — which
is exactly what production does.

| Group                                                       | Factories                                                                                               | Workers |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ------: |
| `imports`                                                   | shopify, paypal, amazon, docmorris, stripe, mirakl, otto, bank-account, fx, tiktok, orderchamp, xentral |      19 |
| `misc`                                                      | gdpr, data-retention, organization-deletion, billing, datev-export                                      |       5 |
| `accounting`                                                | accounting                                                                                              |       2 |
| `reconciliation`                                            | reconciliation                                                                                          |       2 |
| **production total**                                        |                                                                                                         |  **28** |
| `+ test-doc-generation` — non-production only, joins `misc` |                                                                                                         |      29 |

<Note>
  `apps/temporal-workers/WORKER_GROUPS.md` lists `imports` at 18. That count is **stale** — it omits
  `xentral`. The code produces 19. This matters because the same table derives the Prisma
  client-slot budget (43 for `imports`, 62 for a full split set). Recompute it before any cutover to
  the split production model.
</Note>

## Release paths

The two halves of the platform ship differently — the apps automatically, the workers by
hand.

```mermaid theme={null}
flowchart TB
    Main["main"] -->|preview| VPreview["Vercel preview"]
    Main --> RC["rc/x.y.z"]
    RC --> Release["release/x.y"]
    Release --> Prod["prod"]
    Main --> Stage["stage"]

    Stage -->|auto| Staging["cona.run"]
    Prod -->|auto| Production["cona.app"]

    Dev["engineer"] -->|"manual flyctl deploy"| FlyApps["Fly.io worker apps"]

    classDef app fill:#dbe2fb,stroke:#3B56C5,color:#111827
    classDef async fill:#e3f7ea,stroke:#2f9e5c,color:#111827
    classDef actor fill:#eceff4,stroke:#64748b,color:#111827

    class Staging,Production,VPreview app
    class FlyApps async
    class Dev actor
```

Vercel deploys from `main`, `prod`, `stage`, `release/*`, `rc/*`, and `feature/*`;
everything else is blocked. Identical policy across all three apps.

**Nothing in `.github/workflows/` references Fly.** Worker deploys are manual `flyctl`,
which is why they are drawn as a separate path. Database migrations run via the
`migrate-db.yml` workflow.

## Environments

| Environment | Webapp                | Workers                                  | Axiom datasets                                    |
| ----------- | --------------------- | ---------------------------------------- | ------------------------------------------------- |
| Production  | `cona.app`            | `cona-temporal-workers-prod`, monolithic | `cona_temporal-workers`, `cona_temporal-activity` |
| Staging     | `cona.run`            | `cona-temporal-workers-staging`, split   | same, `_staging` suffix                           |
| Preview     | per-branch Vercel URL | —                                        | —                                                 |

Staging additionally sets `OBSERVABILITY_PROBE_ENABLED = 1`; production sets it to `0`.

## Notes

**Region is `fra` for workers.** Vercel regions are not pinned in any `vercel.json`.

**Secrets are Fly secrets, not config.** `fly.prod.toml` documents
`NEXT_PUBLIC_AXIOM_TOKEN`, `UPLOADTHING_TOKEN`, and `ENCRYPTION_KEY` as
`fly secrets set` values — they are deliberately absent from the committed config.

**Workers expose no public port.** The `:8080/health` endpoint is an internal Fly Machine
check; the workers are not behind Fly Proxy.

**Connection pooling is the scaling constraint.** `WORKER_GROUPS.md` documents Supavisor
transaction-mode pools per group, with the accounting process additionally using a lazy
`prismaDirect` pool capped at `ACCOUNTING_QUEUE_LANES * 6 + 4`.
