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

# Device Pairing Flow

> Full device pairing sequence — device side and user side — plus data model and API reference.

# Device Pairing Flow

This document describes the complete process of pairing a new device (ESP32) with a user account, covering both the device-side and user-side interactions.

## Overview

Device pairing is a two-step process that allows physical devices to be associated with user accounts:

1. **Device Publishes Pairing Code**: The device generates a pairing token and publishes it to the API along with its MAC address. This creates a time-limited pairing opportunity.
2. **User Confirms Pairing**: The user scans a QR code (or manually enters details) and confirms the pairing through the web interface, which associates the device with their account.

The pairing token expires after 15 minutes and can only be used once, providing security against unauthorised pairing attempts.

## Complete Pairing Flow

```mermaid theme={null}
sequenceDiagram
    participant Device as ESP32 Device
    participant API
    participant DB
    participant User
    participant Frontend

    Note over Device: Device powers on
    Device->>Device: Generate pairing token
    Device->>Device: Display QR code with<br/>MAC address & token
    Device->>API: POST /devices/pairing-codes<br/>{mac_address, token}
    API->>DB: Check if token exists
    alt Token already exists
        API-->>Device: 400 Bad Request
    else Token is new
        API->>DB: Create device_pairing_token<br/>(expires in 15 minutes)
        API-->>Device: 200 Success
    end

    User->>Device: Scan QR code or read details
    User->>Frontend: Navigate to /devices/pair<br/>?mac=...&token=...
    Frontend->>User: Display pairing form<br/>(pre-filled if from QR)
    User->>Frontend: Submit pairing request<br/>(optional device name)
    Frontend->>API: POST /devices/pair/confirm<br/>?mac=...&token=...<br/>{device_name?}
    Note over API: Verify JWT token
    API->>DB: Find pairing token by MAC & token
    alt Token not found
        API-->>Frontend: 400 Bad Request
        Frontend->>User: Show error
    else Token expired
        API-->>Frontend: 400 Bad Request
        Frontend->>User: Show "Token expired" error
    else Token already used
        API-->>Frontend: 400 Bad Request
        Frontend->>User: Show "Token already used" error
    else Token valid
        API->>DB: Find or create device by MAC
        alt Device doesn't exist
            API->>DB: Create new device
        end
        API->>DB: Update device: set user_id
        API->>DB: Mark pairing token as used
        API-->>Frontend: 200 Success + device details
        Frontend->>User: Show success message
        Frontend->>Frontend: Redirect to /devices/:id
    end

    Note over Device: Device can check pairing status
    Device->>API: GET /devices/pair/status?mac=...
    API->>DB: Find device by MAC
    API-->>Device: {paired: true/false, user_email?, device_id?}
```

## Device-Side Flow

### Step 1: Device Registration (Optional)

When a device first powers on, it may register itself with the API. This creates a device record but does not pair it with a user.

**Endpoint**: `POST /devices/register`

**Request Body**:

```json theme={null}
{
  "mac_address": "AA:BB:CC:DD:EE:FF",
  "device_name": "My Device",
  "firmware_version": "1.0.0"
}
```

**Response**:

```json theme={null}
{
  "id": "device-uuid",
  "mac_address": "AA:BB:CC:DD:EE:FF",
  "device_name": "My Device",
  "firmware_version": "1.0.0",
  "user_id": null,
  "is_paired": false,
  "is_online": true,
  ...
}
```

**Behaviour**:

* If a device with the same MAC address already exists, returns the existing device
* If the device is new, creates a new device record with `user_id = null`
* The device is not yet paired to any user

### Step 2: Publish Pairing Code

The device generates a pairing token (typically a short alphanumeric code) and publishes it to the API. This creates a time-limited pairing opportunity.

**Endpoint**: `POST /devices/pairing-codes`

**Request Body**:

```json theme={null}
{
  "mac_address": "AA:BB:CC:DD:EE:FF",
  "token": "ABC123"
}
```

**Response**:

```json theme={null}
{
  "success": true,
  "message": "Pairing code published successfully"
}
```

**Validation**:

* MAC address must be non-empty and contain colons (format: `XX:XX:XX:XX:XX:XX`)
* Token must be non-empty
* Token must be unique (cannot already exist in the database)

**Behaviour**:

* Creates a `device_pairing_token` record with:
  * `mac_address`: The device's MAC address
  * `token`: The pairing token
  * `expires_at`: Current time + 15 minutes
  * `used_at`: `NULL` (not yet used)
* If the token already exists, returns a 400 error

**Device Display**:
The device should display a QR code containing:

* MAC address
* Pairing token
* Optionally, a URL: `/devices/pair?mac=AA:BB:CC:DD:EE:FF&token=ABC123`

### Step 3: Check Pairing Status (Optional)

The device can periodically check if it has been paired by querying the pairing status endpoint.

**Endpoint**: `GET /devices/pair/status?mac=AA:BB:CC:DD:EE:FF`

**Response**:

```json theme={null}
{
  "paired": true,
  "user_email": "user@example.com",
  "device_id": "device-uuid"
}
```

**Or if not paired**:

```json theme={null}
{
  "paired": false,
  "user_email": null,
  "device_id": null
}
```

**Use Cases**:

* Device can stop displaying the pairing QR code once paired
* Device can show pairing status on its display
* Device can determine if it needs to request pairing again

### Step 4: Claim Device API Key

Once `paired: true`, the device exchanges its (now-consumed) `pairing_token` for a **device-scoped API key**.

**Endpoint**: `POST /devices/pair/claim-auth`

**Request Body**:

```json theme={null}
{
  "mac_address": "AA:BB:CC:DD:EE:FF",
  "pairing_token": "ABC123"
}
```

**Response**:

```json theme={null}
{
  "success": true,
  "device_auth_token": "sk_...",
  "expires_at": "2026-03-23T12:34:56Z"
}
```

The device then authenticates **protected endpoints** with:
`X-API-Key: {device_auth_token}`

Protected calls include:

* `POST /devices/{device_id}/heartbeat`
* `POST /devices/{device_id}/session/start`
* `POST /devices/{device_id}/session/stop`
* `POST /sessions/{session_id}/upload-audio`

If a request fails due to auth expiry, the device should re-call `POST /devices/pair/claim-auth` and store the new `device_auth_token` until `expires_at`.

## User-Side Flow

### Step 1: Access Pairing Page

The user navigates to the device pairing page, either:

* By scanning the QR code displayed on the device (which includes `?mac=...&token=...` query parameters)
* By manually navigating to `/devices/pair` and entering details

**Frontend Route**: `/devices/pair`

**Query Parameters** (optional, pre-filled from QR code):

* `mac`: Device MAC address
* `token`: Pairing token

**Authentication**: User must be authenticated (redirects to login if not)

### Step 2: Submit Pairing Request

The user fills in the pairing form:

* **MAC Address**: Pre-filled from QR code or manually entered
* **Pairing Token**: Pre-filled from QR code or manually entered
* **Device Name**: Optional custom name for the device

**Frontend Function**: `pairDevice(macAddress, token, { device_name? })`

**API Call**: `POST /devices/pair/confirm?mac=AA:BB:CC:DD:EE:FF&token=ABC123`

**Request Body**:

```json theme={null}
{
  "device_name": "Living Room Device"
}
```

**Response**:

```json theme={null}
{
  "success": true,
  "device": {
    "id": "device-uuid",
    "mac_address": "AA:BB:CC:DD:EE:FF",
    "device_name": "Living Room Device",
    "user_id": "user-uuid",
    "is_paired": true,
    ...
  }
}
```

### Step 3: Pairing Confirmation

**Backend Process** (`confirm_pairing_impl`):

1. **Authentication**: Verifies the user's JWT token
2. **Token Validation**:
   * Finds the pairing token by MAC address and token string
   * Checks if token exists
   * Checks if token has expired (current time > `expires_at`)
   * Checks if token has already been used (`used_at` is not null)
3. **Device Creation/Retrieval**:
   * If device doesn't exist, creates a new device with the MAC address
   * If device exists, retrieves it
4. **Device Pairing**:
   * Sets `device.user_id` to the authenticated user's ID
   * Updates `device.device_name` if provided
   * Updates `device.updated_at`
5. **Token Marking**:
   * Sets `pairing_token.used_at` to current timestamp
   * Prevents token reuse

**Error Responses**:

* `400 Bad Request`: Invalid pairing token, expired token, or token already used
* `404 Not Found`: Device not found after pairing (should not occur)

### Step 4: Redirect to Device Page

After successful pairing, the frontend:

1. Displays a success message
2. Waits 2 seconds
3. Redirects to `/devices/{device_id}` to view the paired device

## Data Model

### Device Table

The `device` table stores device information:

* `id`: UUID primary key
* `mac_address`: Unique device identifier (format: `XX:XX:XX:XX:XX:XX`)
* `device_name`: User-friendly name
* `firmware_version`: Optional firmware version string
* `user_id`: Foreign key to `users` table (NULL if not paired)
* `active_session_id`: Currently active session (if any)
* `default_workspace_id`: Default workspace for the device
* `last_seen`: Timestamp of last heartbeat
* `created_at`, `updated_at`: Timestamps

**Pairing State**:

* Device is **unpaired** when `user_id` is `NULL`
* Device is **paired** when `user_id` is set to a user's UUID

### Device Pairing Token Table

The `device_pairing_token` table stores temporary pairing tokens:

* `id`: UUID primary key
* `mac_address`: Device MAC address
* `token`: Unique pairing token string
* `expires_at`: Expiration timestamp (15 minutes from creation)
* `used_at`: Timestamp when token was used (NULL if unused)
* `created_at`: Creation timestamp

**Token Lifecycle**:

1. **Created**: When device publishes pairing code
2. **Expires**: 15 minutes after creation
3. **Used**: When user confirms pairing (`used_at` is set)
4. **One-time use**: Once `used_at` is set, token cannot be reused

## API Endpoints

### POST `/devices/pairing-codes`

Publishes a pairing code from the device. This endpoint is called by the device itself.

**Request Body**:

```json theme={null}
{
  "mac_address": "AA:BB:CC:DD:EE:FF",
  "token": "ABC123"
}
```

**Response**:

```json theme={null}
{
  "success": true,
  "message": "Pairing code published successfully"
}
```

**Errors**:

* `400 Bad Request`: Invalid MAC address format, empty token, or token already exists

**Security**: No authentication required (device endpoint)

### POST `/devices/pair/confirm`

Confirms device pairing by associating a device with a user account. Requires user authentication.

**Query Parameters**:

* `mac`: Device MAC address
* `token`: Pairing token

**Request Body**:

```json theme={null}
{
  "device_name": "Optional Device Name"
}
```

**Response**:

```json theme={null}
{
  "success": true,
  "device": {
    "id": "device-uuid",
    "mac_address": "AA:BB:CC:DD:EE:FF",
    "device_name": "Optional Device Name",
    "user_id": "user-uuid",
    "is_paired": true,
    "is_online": true,
    ...
  }
}
```

**Errors**:

* `400 Bad Request`: Invalid pairing token, expired token, token already used, or authentication failed
* `404 Not Found`: Device not found after pairing (rare)

**Security**: Requires JWT authentication (user must be logged in)

### GET `/devices/pair/status`

Checks the pairing status of a device by MAC address.

**Query Parameters**:

* `mac`: Device MAC address

**Response** (if paired):

```json theme={null}
{
  "paired": true,
  "user_email": "user@example.com",
  "device_id": "device-uuid"
}
```

**Response** (if not paired):

```json theme={null}
{
  "paired": false,
  "user_email": null,
  "device_id": null
}
```

**Errors**:

* `400 Bad Request`: Device not found

**Security**: No authentication required (device endpoint)

### POST `/devices/register`

Registers a new device or retrieves an existing device by MAC address. This is optional and separate from pairing.

**Request Body**:

```json theme={null}
{
  "mac_address": "AA:BB:CC:DD:EE:FF",
  "device_name": "My Device",
  "firmware_version": "1.0.0"
}
```

**Response**: See Device Response model above

**Security**: No authentication required (device endpoint)

## Key Points

### Pairing Token Security

* **Expiration**: Pairing tokens expire 15 minutes after creation
* **One-time use**: Tokens are marked as used (`used_at` is set) when pairing is confirmed
* **Uniqueness**: Each token must be unique in the database
* **Device-bound**: Tokens are associated with a specific MAC address

### Device Registration vs Pairing

* **Registration**: Creates a device record in the database (no user association)
* **Pairing**: Associates an existing or new device with a user account
* A device can be registered without being paired
* A device can be paired without prior registration (device is created during pairing)

### MAC Address Format

* MAC addresses are stored in uppercase format: `AA:BB:CC:DD:EE:FF`
* The API normalises MAC addresses by trimming whitespace and converting to uppercase
* MAC addresses must contain colons (`:`) to be considered valid

### Pairing Flow Variations

1. **Device registers first, then pairs**: Device calls `/devices/register`, then publishes pairing code
2. **Direct pairing**: Device only publishes pairing code, device is created during pairing confirmation
3. **Re-pairing**: If a device is already paired to a user, pairing a new token will update the `user_id` (subject to token validation)

### Frontend User Experience

1. **QR Code Scanning**: Provides seamless experience with pre-filled form
2. **Manual Entry**: Fallback for devices without QR code capability
3. **Error Handling**: Clear error messages for expired tokens, invalid tokens, etc.
4. **Success Feedback**: Visual confirmation and automatic redirect to device page

## Manual Verification Steps

1. Pair the ESP32 device:
   1. Device publishes pairing code via `POST /devices/pairing-codes`.
   2. User confirms pairing via `POST /devices/pair/confirm` (browser UI).
2. After pairing is confirmed on the device (your device sees `paired: true` via `GET /devices/pair/status`):
   1. Device calls `POST /devices/pair/claim-auth` with JSON `{ "mac_address": "...", "pairing_token": "..." }`.
   2. Verify the response is `200` and contains `device_auth_token` and `expires_at`.
3. Verify the device token can access protected endpoints:
   1. `POST /devices/{device_id}/heartbeat` with `X-API-Key: {device_auth_token}`.
   2. `POST /devices/{device_id}/session/start` with `X-API-Key: {device_auth_token}` and capture `session_id`.
   3. Upload a chunk:
      * `POST /sessions/{session_id}/upload-audio` with `X-API-Key: {device_auth_token}`
      * Expect `200` and chunk/stitch responses.
4. Negative test (wrong-device token):
   1. Create a second device and obtain its `device_auth_token`.
   2. Using device A’s token, attempt to upload audio for a session created by device B.
   3. Expect `403 Forbidden` from `POST /sessions/{session_id}/upload-audio`.
5. Expiry behaviour:
   1. Wait until `expires_at` passes.
   2. Verify a protected call fails with `401/403`, then re-run `POST /devices/pair/claim-auth` to obtain a fresh `device_auth_token`.

## References

* Backend routes: `crates/api/src/routes/device/device_routes.rs`
* Backend models: `crates/api/src/routes/device/device_routes_models.rs`
* Frontend pairing page: `web/src/routes/devices/pair/+page.svelte`
* Frontend devices list: `web/src/routes/devices/+page.svelte`
* API client: `web/src/lib/api/devices.ts`
* Database models: `crates/core/src/db/device.rs`, `crates/core/src/db/device_pairing_token.rs`
