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:- 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.
- 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.
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:
- 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:
- 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)
- Creates a
device_pairing_tokenrecord with:mac_address: The device’s MAC addresstoken: The pairing tokenexpires_at: Current time + 15 minutesused_at:NULL(not yet used)
- If the token already exists, returns a 400 error
- 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:
- 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
Oncepaired: true, the device exchanges its (now-consumed) pairing_token for a device-scoped API key.
Endpoint: POST /devices/pair/claim-auth
Request Body:
X-API-Key: {device_auth_token}
Protected calls include:
POST /devices/{device_id}/heartbeatPOST /devices/{device_id}/session/startPOST /devices/{device_id}/session/stopPOST /sessions/{session_id}/upload-audio
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/pairand entering details
/devices/pair
Query Parameters (optional, pre-filled from QR code):
mac: Device MAC addresstoken: Pairing token
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
pairDevice(macAddress, token, { device_name? })
API Call: POST /devices/pair/confirm?mac=AA:BB:CC:DD:EE:FF&token=ABC123
Request Body:
Step 3: Pairing Confirmation
Backend Process (confirm_pairing_impl):
- Authentication: Verifies the user’s JWT token
- 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_atis not null)
- Device Creation/Retrieval:
- If device doesn’t exist, creates a new device with the MAC address
- If device exists, retrieves it
- Device Pairing:
- Sets
device.user_idto the authenticated user’s ID - Updates
device.device_nameif provided - Updates
device.updated_at
- Sets
- Token Marking:
- Sets
pairing_token.used_atto current timestamp - Prevents token reuse
- Sets
400 Bad Request: Invalid pairing token, expired token, or token already used404 Not Found: Device not found after pairing (should not occur)
Step 4: Redirect to Device Page
After successful pairing, the frontend:- Displays a success message
- Waits 2 seconds
- Redirects to
/devices/{device_id}to view the paired device
Data Model
Device Table
Thedevice table stores device information:
id: UUID primary keymac_address: Unique device identifier (format:XX:XX:XX:XX:XX:XX)device_name: User-friendly namefirmware_version: Optional firmware version stringuser_id: Foreign key touserstable (NULL if not paired)active_session_id: Currently active session (if any)default_workspace_id: Default workspace for the devicelast_seen: Timestamp of last heartbeatcreated_at,updated_at: Timestamps
- Device is unpaired when
user_idisNULL - Device is paired when
user_idis set to a user’s UUID
Device Pairing Token Table
Thedevice_pairing_token table stores temporary pairing tokens:
id: UUID primary keymac_address: Device MAC addresstoken: Unique pairing token stringexpires_at: Expiration timestamp (15 minutes from creation)used_at: Timestamp when token was used (NULL if unused)created_at: Creation timestamp
- Created: When device publishes pairing code
- Expires: 15 minutes after creation
- Used: When user confirms pairing (
used_atis set) - One-time use: Once
used_atis 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:
400 Bad Request: Invalid MAC address format, empty token, or token already exists
POST /devices/pair/confirm
Confirms device pairing by associating a device with a user account. Requires user authentication.
Query Parameters:
mac: Device MAC addresstoken: Pairing token
400 Bad Request: Invalid pairing token, expired token, token already used, or authentication failed404 Not Found: Device not found after pairing (rare)
GET /devices/pair/status
Checks the pairing status of a device by MAC address.
Query Parameters:
mac: Device MAC address
400 Bad Request: Device not found
POST /devices/register
Registers a new device or retrieves an existing device by MAC address. This is optional and separate from pairing.
Request Body:
Key Points
Pairing Token Security
- Expiration: Pairing tokens expire 15 minutes after creation
- One-time use: Tokens are marked as used (
used_atis 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
- Device registers first, then pairs: Device calls
/devices/register, then publishes pairing code - Direct pairing: Device only publishes pairing code, device is created during pairing confirmation
- 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
- QR Code Scanning: Provides seamless experience with pre-filled form
- Manual Entry: Fallback for devices without QR code capability
- Error Handling: Clear error messages for expired tokens, invalid tokens, etc.
- Success Feedback: Visual confirmation and automatic redirect to device page
Manual Verification Steps
- Pair the ESP32 device:
- Device publishes pairing code via
POST /devices/pairing-codes. - User confirms pairing via
POST /devices/pair/confirm(browser UI).
- Device publishes pairing code via
- After pairing is confirmed on the device (your device sees
paired: trueviaGET /devices/pair/status):- Device calls
POST /devices/pair/claim-authwith JSON{ "mac_address": "...", "pairing_token": "..." }. - Verify the response is
200and containsdevice_auth_tokenandexpires_at.
- Device calls
- Verify the device token can access protected endpoints:
POST /devices/{device_id}/heartbeatwithX-API-Key: {device_auth_token}.POST /devices/{device_id}/session/startwithX-API-Key: {device_auth_token}and capturesession_id.- Upload a chunk:
POST /sessions/{session_id}/upload-audiowithX-API-Key: {device_auth_token}- Expect
200and chunk/stitch responses.
- Negative test (wrong-device token):
- Create a second device and obtain its
device_auth_token. - Using device A’s token, attempt to upload audio for a session created by device B.
- Expect
403 ForbiddenfromPOST /sessions/{session_id}/upload-audio.
- Create a second device and obtain its
- Expiry behaviour:
- Wait until
expires_atpasses. - Verify a protected call fails with
401/403, then re-runPOST /devices/pair/claim-authto obtain a freshdevice_auth_token.
- Wait until
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