> For the complete documentation index, see [llms.txt](https://decentralabs-2.gitbook.io/decentralabs-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://decentralabs-2.gitbook.io/decentralabs-docs/lab-gateway/getting-started-and-deployment/deployment-architectures.md).

# Deployment architectures

This is the source of truth for the Gateway topologies. The key distinction is between the control plane (JWT issuer, provider administration and on-chain operations) and the access plane (OpenResty, Guacamole, FMU and Station/ops traffic). `ISSUER` selects the JWT authority; the lab's on-chain `accessURI` selects the access plane that serves the user.

Read this document when selecting a topology or adding a Lite Gateway. Then apply the [configuration reference](/decentralabs-docs/lab-gateway/getting-started-and-deployment/configuration.md) and validate the deployed result with [operations and health](/decentralabs-docs/lab-gateway/operations-and-verification/operations-and-health.md).

## Selection procedure

1. Use **Full only** when one institution issues credentials and serves its own laboratories from one gateway.
2. Use **Lite** when a local laboratory network needs its own access plane but a remote Full or standalone backend owns credentials and provider control.
3. Use **Full + N Lite** when one Full backend operates several local access planes.
4. Use **standalone backend + N Lite** when the control plane is intentionally independent of any access-plane gateway.

The backend role is configured separately with `BLOCKCHAIN_SERVICES_MODE=provider-consumer` or `consumer-only`. A standalone consumer backend can participate in the same remote-control-plane topology without acquiring provider registration, lab publication or access-gateway authority. `ISSUER` selects the JWT authority; it does not select the backend role.

Every Lite needs a unique public origin/gateway ID, per-Lite credentials on the control plane and the Lite, and an explicit protected provisioner route. A Full Gateway can generate a trust bundle; with a standalone provider backend, set the equivalent values in the backend and Lite configurations. Do not publish a lab to a Lite `accessURI` until those three conditions are true.

## Components and boundaries

| Component                        | Control-plane responsibility                                                                                                         | Access-plane responsibility                                                         |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- |
| Full Gateway                     | Embedded `blockchain-services` issues/validates local credentials, handles provider operations and signs institutional transactions. | OpenResty, Guacamole, FMU Runner and Ops Worker serve local labs.                   |
| Lite Gateway                     | Does not become the primary issuer. It validates credentials from the configured remote issuer and can delegate `/lab-admin`.        | Owns its local OpenResty, Guacamole, FMU Runner and Ops Worker.                     |
| Standalone `blockchain-services` | Can be the remote provider/consumer backend and JWT issuer without a local browser access plane.                                     | No local Guacamole or Station plane. It provisions configured remote gateways.      |
| Lab Station                      | No public auth role.                                                                                                                 | Windows-side execution, WinRM commands, telemetry and optional FMU station backend. |

The root Compose project keeps the embedded `blockchain-services` container dormant when a Gateway is Lite (`BLOCKCHAIN_SERVICES_ENABLED=false` or the `auto`/external-`ISSUER` rule). No Java process or local JWT key is started. OpenResty selects the remote issuer configured by `ISSUER`; the dormant service must not be mistaken for the authority of the deployment.

The local `ops-schema-migrator` is deliberately independent from that Java process. It runs Flyway before `blockchain-services` or `ops-worker` starts and provisions the Gateway-owned operational tables in both Full and Lite. A Lite therefore has a durable local operations journal even though its reservations, credentials and blockchain state belong to the connected Full Gateway or standalone `blockchain-services` control plane.

## Topology matrix

| Topology                       | JWT issuer / control plane                         | Access planes             | Required relationship                                                                              |
| ------------------------------ | -------------------------------------------------- | ------------------------- | -------------------------------------------------------------------------------------------------- |
| Full only                      | Embedded `blockchain-services` in the Full Gateway | One Full Gateway          | `ISSUER` empty; local provider features enabled.                                                   |
| Lite only (with remote issuer) | External Full or standalone `blockchain-services`  | One Lite Gateway          | `ISSUER=<remote>/auth`; Lite trust bundle or equivalent remote key/config.                         |
| Full + N Lite                  | One Full Gateway/backend                           | Full plus N Lite gateways | Each Lite has a unique origin/gateway ID and an explicit provisioner route in the Full backend.    |
| `blockchain-services` + N Lite | Standalone backend                                 | N Lite gateways           | Backend is the issuer/control plane; every Lite points to it and has an explicit route/credential. |

```mermaid
flowchart LR
    Marketplace["Marketplace / browser orchestration"]
    Control["JWT issuer + blockchain-services<br/>Full embedded or standalone"]
    Chain[("Smart contracts")]

    subgraph Full["Full gateway"]
        FEdge["OpenResty"]
        FAccess["Guacamole / FMU / Ops"]
        FEdge --> FAccess
    end
    subgraph LiteA["Lite gateway A"]
        AEdge["OpenResty"]
        AAccess["Guacamole / FMU / Ops"]
        AEdge --> AAccess
    end
    subgraph LiteN["Lite gateway N"]
        NEdge["OpenResty"]
        NAccess["Guacamole / FMU / Ops"]
        NEdge --> NAccess
    end

    Marketplace --> Control
    Control <--> Chain
    Marketplace --> FEdge
    Marketplace --> AEdge
    Marketplace --> NEdge
    Control -->|JWT / access-code / observation| FEdge
    Control -->|JWT / access-code / observation| AEdge
    Control -->|JWT / access-code / observation| NEdge
```

The same diagram describes both composite deployments: in **Full + N Lite**, `Control` is the embedded Full backend; in **standalone backend + N Lite**, it is the separate `blockchain-services` deployment and the Full subgraph is absent.

## 1. Full Gateway only

Use this for an institution that publishes and serves its own laboratories.

```env
# Gateway .env
ISSUER=
LAB_ADMIN_BACKEND_URL=
LAB_MANAGER_TOKEN=<strong-local-operator-token>

# blockchain-services/.env
BLOCKCHAIN_SERVICES_MODE=provider-consumer
FEATURES_PROVIDERS_ENABLED=true
FEATURES_PROVIDERS_REGISTRATION_ENABLED=true
```

The local `/auth` endpoints are enabled through OpenResty, the local backend issues the reservation credential, and `accessURI` for local physical labs points at the Full public origin. Physical Guacamole labs use `accessKey=guac:id:<connection_id>`. Local FMU execution remains a development option; production FMUs should run in Station mode when a Station backend is available.

## 2. Lite access gateway

Use this when another backend owns identity and provider control. The Lite gateway still owns its local Guacamole catalog, FMU facade and operations plane.

```env
# Lite Gateway .env
ISSUER=https://<issuer-origin>/auth
LAB_MANAGER_TOKEN=<unique-token-for-this-lite>
LAB_ADMIN_BACKEND_URL=https://<issuer-origin>       # optional but required for /lab-admin
LAB_ADMIN_BACKEND_TOKEN=<unique-token-from-the-trust-bundle>
LAB_ADMIN_BACKEND_TOKEN_HEADER=X-Lab-Manager-Token
```

For a Full control plane, use its trust-bundle script. It generates a unique lab-admin backend token for this Lite, registers the token and `accessURI` in `LAB_ADMIN_GATEWAY_CREDENTIALS_JSON`, and places the token in the Lite bundle. With a standalone provider backend, register the same per-Lite credential in the provider environment and transfer its token to the Lite securely. The credential binds lab listing and mutation to this Lite's Gateway; requests with a mismatched `X-Gateway-ID` are rejected. The bundle also binds the Lite origin to `ISSUER`, `FMU_GATEWAY_ID`, `FMU_JWT_AUDIENCE`, the session-observer credential, the Guacamole provisioner credential and the reservation projection feed. The technical gateway ID is the normalized public host plus `:port` for a non-default HTTPS port; `SERVER_NAME` stays hostname-only for local URL construction. A Lite setup without a remote `LAB_ADMIN_BACKEND_URL` deliberately blocks on-chain lab administration. When enabling this version for existing Lite gateways, issue one fresh trust bundle for each Lite and securely replace that Lite's full bundle. Reissuing rotates all per-Gateway credentials in the bundle. Install the replacement on the Lite and restart the provider backend and Lite gateway so both sides load the matching values. For a standalone provider backend, update its `LAB_ADMIN_GATEWAY_CREDENTIALS_JSON` entry and the Lite bundle together before restarting either side.

The local `/auth/**` surface is disabled in Lite mode. Access-code redemption, FMU ticket operations and session observations target the configured remote issuer/backend; browser access still terminates at the Lite origin.

## 3. Full + N Lite gateways

Use this when one Full gateway/backend controls several local access planes. Repeat the following for every Lite gateway; do not share gateway IDs or observer secrets:

```env
ISSUER=https://full.example.edu/auth
LAB_MANAGER_TOKEN=<unique-lite-token>
LAB_ADMIN_BACKEND_URL=https://full.example.edu
LAB_ADMIN_BACKEND_TOKEN=<unique-token-generated-for-this-lite>
LAB_ADMIN_BACKEND_TOKEN_HEADER=X-Lab-Manager-Token
```

Issue one trust bundle per public Lite origin (the generated filename replaces `:` with `_`, but the ID inside the bundle retains the port):

```bash
scripts/issue-lite-trust-bundle.sh \
  https://lite-a.example.edu https://full.example.edu
```

If a Lite already has a bundle, reissue and securely deploy the complete replacement; the old credentials stop working after the Full backend reloads its environment.

The Full backend must have an explicit `GUACAMOLE_PROVISIONER_ROUTES_JSON` entry for every remote origin, or a single shared `GUACAMOLE_PROVISIONER_TOKEN` only when the same credential is intentionally used by all Lite provisioner routes. The route map is fail-closed: an unmapped remote `accessURI` is rejected and is never sent to the Full local ops-worker.

The trust-bundle script also registers distinct `LAB_ADMIN_GATEWAY_CREDENTIALS_JSON` and `RESERVATION_PROJECTION_CREDENTIALS_JSON` entries. The first binds the Lite's lab-admin token to its Gateway ID and `accessURI`; the Lite receives that token as `LAB_ADMIN_BACKEND_TOKEN`. The Lite uses this same trust entry for `/lab-admin/notifications`; OpenResty supplies the Lite's Gateway ID and the backend stores notification settings under that identity. Reservation events use the lab's historical `accessURI` to select the matching settings. The Lite `ops-worker` polls `/reservations/projection` with that credential. The Full backend filters the response by the registered `accessURI`, so a Lite cannot request another gateway's reservations. The local worker keeps its durable operation journal; successful start/end actions are not replayed when the remote projection remains in `CONFIRMED`/`ACTIVE` state.

```mermaid
sequenceDiagram
    participant M as Marketplace
    participant F as Full backend
    participant L as Lite gateway
    participant G as Lite Guacamole
    participant S as Lab Station

    M->>F: Reserve / check-in / request access credential
    F->>F: Read lab accessURI and resolve explicit route
    F->>L: Provision reservation user through protected route
    L->>G: Create local Guacamole user/permission
    F-->>M: Opaque access code and Lite accessURI
    M->>L: POST /auth/access with code
    L->>F: Prepare redemption handle
    L->>L: Validate JWT, destination and local state
    L->>F: Commit handle, or release on failure
    L-->>M: Secure cookie and clean local Guacamole URL
    G->>S: RDP/WinRM/FMUs on private network
```

Full-owned labs use the Full local provisioner; Lite-owned labs use the route selected from `accessURI`. The booking and `SessionStarted` evidence remain owned by the Full backend, even when the session runs on a Lite gateway.

## 4. Standalone `blockchain-services` + N Lite gateways

Use this when the control plane is deployed separately from the Gateway repository. Enable provider features in the standalone backend and configure each Lite as an access plane:

```env
# standalone blockchain-services/.env
BLOCKCHAIN_SERVICES_MODE=provider-consumer
FEATURES_PROVIDERS_ENABLED=true
FEATURES_PROVIDERS_REGISTRATION_ENABLED=true
```

Each Lite uses the standalone backend as issuer and lab-admin backend. Register a unique `LAB_ADMIN_GATEWAY_CREDENTIALS_JSON` entry on that backend for each Lite, keyed by canonical Gateway ID with its token and exact `accessURI`:

```env
ISSUER=https://backend.example.edu/auth
LAB_ADMIN_BACKEND_URL=https://backend.example.edu
LAB_ADMIN_BACKEND_TOKEN=<unique-token-generated-for-this-lite>
LAB_ADMIN_BACKEND_TOKEN_HEADER=X-Lab-Manager-Token
```

Configure `GUACAMOLE_PROVISIONER_ROUTES_JSON` on the standalone backend with an explicit route and credential for every Lite origin. The standalone backend does not supply Guacamole, FMU Runner, Ops Worker or Station connectivity; each Lite must run and protect those local services itself.

## Provisioner routing invariant

For physical Guacamole labs, `accessKey` is always `guac:id:<connection_id>`. The backend resolves a provisioner in this order:

1. exact origin entry in `GUACAMOLE_PROVISIONER_ROUTES_JSON`;
2. normalized host entry in the same map;
3. local `http://ops-worker:8081/internal/guacamole` only when the origin is the backend's own gateway origin or no remote origin is involved;
4. reject the request.

Never derive a remote route or credential from untrusted lab metadata. A Lite gateway's local Guacamole database is authoritative for its own access URI; the backend must not use a remote gateway's catalog as if it were local.

## Operational checks

For every composite deployment verify:

* the issuer/JWKS origin and `accessURI` are intentionally different where required;
* every Lite has a unique `FMU_GATEWAY_ID` (including any non-default public port) and observer signing secret;
* remote access-code, FMU ticket, observation and provisioner routes use HTTPS or an explicitly controlled private link;
* `GET /gateway/mode` reports the expected local edge mode and `/gateway/health` shows the selected backend dependencies;
* remote origins are present in the backend route map before publishing a lab;
* Station WinRM, FMU internal HTTP/WSS and MySQL remain off the public edge.

See [Guacamole Session Policy](/decentralabs-docs/lab-gateway/architecture-and-connectivity/guacamole-session-policy.md) for expiry and reconnect semantics, and [Laboratory Connectivity](/decentralabs-docs/lab-gateway/architecture-and-connectivity/laboratory-connectivity.md) for the private-network protocol matrix.
