Skip to main content

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

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:
Response:
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:
Response:
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:
Or if not paired:
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:
Response:
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:
Response:

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:
Response:
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:
Response:
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):
Response (if not paired):
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:
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