# SaaS Connection

- Canonical URL: https://docs.fairvisor.com/docs/deployment/saas/
- Section: docs
- Last updated: n/a
> How Fairvisor Edge connects to Fairvisor SaaS — registration, heartbeat, config delivery, and event batching.


When `FAIRVISOR_SAAS_URL` is set, the edge instance runs a persistent connection loop to Fairvisor SaaS. This page documents the full protocol.

## Overview

```
Edge startup
  └─ Register  →  POST /api/v1/edge/register
  └─ Pull config  →  GET /api/v1/edge/config
  └─ ACK  →  POST /api/v1/edge/config/ack
  └─ [request traffic begins]
  └─ Heartbeat loop (every HEARTBEAT_INTERVAL seconds)
  └─ Config poll loop (every CONFIG_POLL_INTERVAL seconds)
  └─ Event flush loop (every EVENT_FLUSH_INTERVAL seconds)
```

All API calls use:
- `Authorization: Bearer <FAIRVISOR_EDGE_TOKEN>`
- `Content-Type: application/json`
- Base URL: `FAIRVISOR_SAAS_URL`

## Step 1 — Registration

On startup, the edge registers itself with SaaS:

```
POST /api/v1/edge/register
{
  "edge_id":  "edge-prod-us-east-1",
  "version":  "0.1.0",
  "timestamp": 1736940000
}
```

**On success (2xx):** heartbeat, config, and event timers are initialised.
**On failure:** startup fails. The container exits non-zero.

## Step 2 — Initial config pull

Immediately after registration, the edge pulls its policy bundle:

```
GET /api/v1/edge/config
```

| Response | Meaning |
|---|---|
| `200 OK` | New bundle in response body; parse, compile, and apply |
| `304 Not Modified` | Bundle unchanged (no-op) |
| `401` / `403` | Credential problem; stop retrying |

The bundle JSON structure is the same as a local `policy.json`. After successful application, the edge sends an ACK.

## Step 3 — Config ACK

```
POST /api/v1/edge/config/ack
{
  "edge_id":   "edge-prod-us-east-1",
  "version":   "0.1.0",
  "hash":      "sha256:abcdef…",
  "status":    "applied",   // or "rejected"
  "error":     null,        // rejection reason if status="rejected"
  "timestamp": 1736940001
}
```

The ACK communicates whether the edge successfully loaded the bundle or rejected it (e.g., validation failure, schema mismatch).

## Heartbeat loop

Every `FAIRVISOR_HEARTBEAT_INTERVAL` seconds (default 5 s):

```
POST /api/v1/edge/heartbeat
{
  "edge_id":      "edge-prod-us-east-1",
  "version":      "0.1.0",
  "policy_hash":  "sha256:abcdef…",
  "uptime":       3612,
  "timestamp":    1736943612
}
```

**Response may include:**

| Field | Meaning |
|---|---|
| `server_time` | SaaS wall clock (used for clock skew detection) |
| `config_update_available: true` | Trigger an immediate config pull |

If `config_update_available` is `true`, the edge pulls the config outside its normal poll interval.

## Config poll loop

Every `FAIRVISOR_CONFIG_POLL_INTERVAL` seconds (default 30 s), the edge polls for config changes using a conditional request:

```
GET /api/v1/edge/config
If-None-Match: <current_bundle_hash>
```

This avoids re-parsing an unchanged bundle. When `304` is returned, no action is taken.

## Event batching

Decision events (every allow/reject decision, with metadata) are buffered in memory and flushed every `FAIRVISOR_EVENT_FLUSH_INTERVAL` seconds (default 60 s):

```
POST /api/v1/edge/events
Idempotency-Key: batch_<edge_id>_<timestamp>_<seq>
{
  "edge_id":              "edge-prod-us-east-1",
  "events":               [ ... ],        // up to 100 per batch
  "clock_skew_suspected": false,
  "clock_skew_seconds":   0
}
```

- Max buffer size: 1 000 events (older events are dropped when full)
- Max batch size: 100 events per flush
- Idempotency key prevents double-counting on retry

## Retry and backoff

All SaaS API calls use exponential backoff:

```
delay = min(2^attempt, 60 seconds) + random_jitter
```

Non-retriable responses (401, 403, 404) clear the retry state immediately. Retriable responses (5xx, network errors) schedule a retry with backoff.

## SaaS circuit breaker

The edge maintains a circuit breaker for SaaS connectivity:

| State | Trigger | Behaviour |
|---|---|---|
| `CLOSED` | Normal | All SaaS calls proceed |
| `OPEN` | 5 consecutive failures | SaaS calls suppressed; edge continues with cached bundle |
| `HALF_OPEN` | 30 s after opening | One probe call allowed |
| `CLOSED` | 2 successes in HALF_OPEN | Returns to normal |

When the circuit is open, the edge continues enforcing the last known bundle. The `fairvisor_saas_reachable` metric is set to `0`.

## Clock skew detection

If `|local_time − server_time| > 10 seconds`, the edge sets `clock_skew_suspected: true` in subsequent event flushes. This is informational — enforcement is not affected.

## Security considerations

- `FAIRVISOR_EDGE_TOKEN` is sent as a Bearer token on every SaaS request. Rotate it from the Fairvisor dashboard.
- The config bundle can optionally be signed. If a signing key is configured, the edge rejects bundles that fail signature verification.
- Bundle versioning is monotonically increasing — the edge rejects a bundle with a lower version than the current one.

## Graceful shutdown behavior

On worker shutdown, the edge attempts to flush buffered SaaS events before exit.

- Runtime sets `worker_shutdown_timeout` to `35s`.
- Shutdown handler calls SaaS `flush_events()` before final worker termination.
- If the process is hard-killed before graceful timeout, in-memory buffered events may be lost.

Operational recommendation: use termination grace period of at least 35 seconds for SaaS-connected deployments.

