Skip to content

Mac Mini Edge Client: Supabase Integration Update

Date: November 13, 2025 Status: ⚠️ REQUIRED UPDATE - Edge client authentication changed Impact: Orchestrator endpoint now requires webhook secret authentication


Table of Contents

  1. Overview
  2. What Changed
  3. Update Instructions
  4. Code Changes
  5. Testing
  6. Rollback Plan

Overview

The backend has migrated to secure Supabase authentication. This affects how the Mac mini edge client authenticates with the orchestrator endpoint.

What This Means for Edge Client

Before: - Orchestrator endpoint had no authentication - Any device could send messages to backend

After: - Orchestrator endpoint requires webhook secret in Authorization header - Only authenticated edge agents can send messages


What Changed

Orchestrator Endpoint Authentication

Endpoint: POST /orchestrator/message

Before (insecure):

# ❌ No authentication
response = requests.post(
    "https://archety-backend-prod.up.railway.app/orchestrator/message",
    json={
        "chat_guid": "iMessage;-;+15551234567",
        "mode": "direct",
        "sender": "+15551234567",
        "text": "Hello Sage!",
        "timestamp": 1700000000,
        "participants": ["+15551234567"]
    }
)

After (secure):

# ✅ Requires Authorization header with webhook secret
response = requests.post(
    "https://archety-backend-prod.up.railway.app/orchestrator/message",
    headers={
        "Authorization": f"Bearer {RELAY_WEBHOOK_SECRET}",
        "Content-Type": "application/json"
    },
    json={
        "chat_guid": "iMessage;-;+15551234567",
        "mode": "direct",
        "sender": "+15551234567",
        "text": "Hello Sage!",
        "timestamp": 1700000000,
        "participants": ["+15551234567"]
    }
)


Update Instructions

Step 1: Get Webhook Secret

The webhook secret is stored in Railway environment variables as RELAY_WEBHOOK_SECRET.

✅ Current Production Secret (November 13, 2025):

# Use this exact value for production and development
export RELAY_WEBHOOK_SECRET="<REDACTED_HEX_SECRET>"

⚠️ Security Note: This secret is already configured in Railway for both dev and production environments. If you need to rotate it, follow these steps:

Option A: Retrieve from Railway

# Backend engineer can retrieve it from Railway dashboard
# Or run this command:
railway run --service archety-backend bash -c 'echo $RELAY_WEBHOOK_SECRET'

Option B: Generate new secret (for rotation)

# Generate a secure random secret
openssl rand -hex 32

# Or in Python
python3 -c "import secrets; print(secrets.token_hex(32))"

# Then update Railway dev + prod environments

Step 2: Store Secret on Mac Mini

Add the secret to your Mac mini edge client environment:

Option A: Environment variable

# Add to ~/.zshrc or ~/.bash_profile
export RELAY_WEBHOOK_SECRET="your_secret_here_64_chars_long"

# Reload shell
source ~/.zshrc

Option B: Config file

# config.py
import os
from pathlib import Path

# Try to load from .env file
ENV_FILE = Path(__file__).parent / '.env'
if ENV_FILE.exists():
    from dotenv import load_dotenv
    load_dotenv(ENV_FILE)

RELAY_WEBHOOK_SECRET = os.getenv('RELAY_WEBHOOK_SECRET')

if not RELAY_WEBHOOK_SECRET:
    raise ValueError("RELAY_WEBHOOK_SECRET not set! Add to .env file or environment.")

Option C: macOS Keychain (most secure)

# Store in macOS Keychain
security add-generic-password \
  -a "archety-relay" \
  -s "relay-webhook-secret" \
  -w "your_secret_here"

# Retrieve in Python:
# import subprocess
# secret = subprocess.check_output([
#     'security', 'find-generic-password',
#     '-a', 'archety-relay',
#     '-s', 'relay-webhook-secret',
#     '-w'
# ]).decode().strip()

Step 3: Update Edge Client Code

Find the orchestrator message sending code:

# relay.py or similar file
import requests
import os

BACKEND_URL = "https://archety-backend-prod.up.railway.app"
RELAY_WEBHOOK_SECRET = os.getenv('RELAY_WEBHOOK_SECRET')

def send_to_orchestrator(message_data: dict) -> dict:
    """
    Send iMessage to backend orchestrator.

    Args:
        message_data: Message payload with chat_guid, sender, text, etc.

    Returns:
        Orchestrator response
    """
    # ✅ NEW: Add Authorization header
    headers = {
        "Authorization": f"Bearer {RELAY_WEBHOOK_SECRET}",
        "Content-Type": "application/json"
    }

    response = requests.post(
        f"{BACKEND_URL}/orchestrator/message",
        headers=headers,  # ✅ Include headers
        json=message_data,
        timeout=30
    )

    response.raise_for_status()
    return response.json()

If using edge agent sync endpoint:

def sync_with_backend() -> dict:
    """
    Sync edge agent state with backend.

    POST /edge/sync
    """
    headers = {
        "Authorization": f"Bearer {RELAY_WEBHOOK_SECRET}",
        "Content-Type": "application/json"
    }

    response = requests.post(
        f"{BACKEND_URL}/edge/sync",
        headers=headers,
        json={
            "edge_agent_id": EDGE_AGENT_ID,
            "version": VERSION,
            "capabilities": ["schedule", "filter", "local_cache"],
            "events": [],  # Any events to report
        },
        timeout=30
    )

    response.raise_for_status()
    return response.json()

Code Changes

Complete Updated Relay Client

#!/usr/bin/env python3
"""
Mac Mini Edge Client - iMessage Relay
Forwards iMessage conversations to Archety backend.

Updated: November 13, 2025
- Added webhook secret authentication
- Improved error handling for 401/403 errors
"""

import os
import sys
import json
import time
import logging
import requests
from typing import Dict, Any, Optional
from dataclasses import dataclass
from datetime import datetime

# Configuration
BACKEND_URL = os.getenv('ARCHETY_BACKEND_URL', 'https://archety-backend-prod.up.railway.app')
RELAY_WEBHOOK_SECRET = os.getenv('RELAY_WEBHOOK_SECRET')
EDGE_AGENT_ID = os.getenv('EDGE_AGENT_ID', 'mac-mini-primary')

# Validate configuration
if not RELAY_WEBHOOK_SECRET:
    print("ERROR: RELAY_WEBHOOK_SECRET not set!")
    print("Set it with: export RELAY_WEBHOOK_SECRET='your-secret-here'")
    sys.exit(1)

# Setup logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger('archety-relay')


@dataclass
class Message:
    """iMessage data structure."""
    chat_guid: str
    mode: str  # 'direct' or 'group'
    sender: str  # Phone number in E.164 format
    text: str
    timestamp: int  # Unix timestamp
    participants: list[str]
    message_guid: Optional[str] = None
    attachments: Optional[list] = None


class ArchetyRelay:
    """
    Relay client for forwarding iMessages to Archety backend.
    """

    def __init__(self):
        self.backend_url = BACKEND_URL
        self.webhook_secret = RELAY_WEBHOOK_SECRET
        self.edge_agent_id = EDGE_AGENT_ID
        self.session = requests.Session()

        # Set default headers for all requests
        self.session.headers.update({
            'Authorization': f'Bearer {self.webhook_secret}',
            'Content-Type': 'application/json',
            'User-Agent': f'Archety-Edge-Client/{self.edge_agent_id}'
        })

        logger.info(f"Initialized Archety Relay")
        logger.info(f"Backend: {self.backend_url}")
        logger.info(f"Edge Agent ID: {self.edge_agent_id}")

    def send_message(self, message: Message) -> Dict[str, Any]:
        """
        Send message to orchestrator endpoint.

        Args:
            message: Message object to send

        Returns:
            Orchestrator response

        Raises:
            requests.HTTPError: If request fails
        """
        payload = {
            'chat_guid': message.chat_guid,
            'mode': message.mode,
            'sender': message.sender,
            'text': message.text,
            'timestamp': message.timestamp,
            'participants': message.participants,
        }

        if message.message_guid:
            payload['message_guid'] = message.message_guid

        if message.attachments:
            payload['attachments'] = message.attachments

        try:
            response = self.session.post(
                f'{self.backend_url}/orchestrator/message',
                json=payload,
                timeout=30
            )

            # Handle authentication errors
            if response.status_code == 401:
                logger.error("❌ Authentication failed - invalid webhook secret")
                logger.error("Check that RELAY_WEBHOOK_SECRET matches backend")
                raise requests.HTTPError("Invalid webhook secret")

            if response.status_code == 403:
                logger.error("❌ Forbidden - webhook secret valid but access denied")
                raise requests.HTTPError("Access denied")

            response.raise_for_status()

            result = response.json()
            logger.info(f"✅ Message sent successfully")
            logger.debug(f"Response: {result}")

            return result

        except requests.exceptions.Timeout:
            logger.error("❌ Request timed out after 30s")
            raise

        except requests.exceptions.ConnectionError as e:
            logger.error(f"❌ Connection error: {e}")
            logger.error("Check that backend is reachable")
            raise

        except requests.exceptions.HTTPError as e:
            logger.error(f"❌ HTTP error: {e}")
            logger.error(f"Response: {response.text if 'response' in locals() else 'No response'}")
            raise

    def sync_state(self, events: list = None) -> Dict[str, Any]:
        """
        Sync edge agent state with backend.

        Args:
            events: Optional list of events to report

        Returns:
            Sync response with any commands from backend
        """
        payload = {
            'edge_agent_id': self.edge_agent_id,
            'version': '1.0.0',
            'capabilities': ['schedule', 'filter', 'local_cache'],
            'events': events or [],
            'status': 'active',
            'last_sync': datetime.utcnow().isoformat()
        }

        try:
            response = self.session.post(
                f'{self.backend_url}/edge/sync',
                json=payload,
                timeout=10
            )

            response.raise_for_status()
            result = response.json()

            logger.info("✅ State synced with backend")
            logger.debug(f"Sync response: {result}")

            return result

        except Exception as e:
            logger.warning(f"⚠️  Sync failed: {e}")
            return {}

    def health_check(self) -> bool:
        """
        Check if backend is reachable and authentication is working.

        Returns:
            True if healthy, False otherwise
        """
        try:
            response = self.session.post(
                f'{self.backend_url}/orchestrator/heartbeat',
                json={'edge_agent_id': self.edge_agent_id},
                timeout=5
            )

            if response.status_code == 200:
                logger.info("✅ Backend health check passed")
                return True
            elif response.status_code == 401:
                logger.error("❌ Health check failed: Invalid webhook secret")
                return False
            else:
                logger.warning(f"⚠️  Health check returned {response.status_code}")
                return False

        except Exception as e:
            logger.error(f"❌ Health check failed: {e}")
            return False


# Example usage
def main():
    """Test the relay client."""
    relay = ArchetyRelay()

    # Health check
    if not relay.health_check():
        logger.error("Health check failed - exiting")
        sys.exit(1)

    # Test message
    test_message = Message(
        chat_guid="iMessage;-;+15551234567",
        mode="direct",
        sender="+15551234567",
        text="Hello from Mac mini relay!",
        timestamp=int(time.time()),
        participants=["+15551234567"]
    )

    try:
        result = relay.send_message(test_message)
        logger.info(f"Test message sent successfully: {result}")

    except Exception as e:
        logger.error(f"Test message failed: {e}")
        sys.exit(1)


if __name__ == '__main__':
    main()

Testing

Step 1: Test Authentication

# Test with correct secret
python3 test_relay.py

# Expected output:
# ✅ Backend health check passed
# ✅ Message sent successfully

Step 2: Test with Invalid Secret (to verify security)

# Temporarily use wrong secret
export RELAY_WEBHOOK_SECRET="wrong-secret"
python3 test_relay.py

# Expected output:
# ❌ Authentication failed - invalid webhook secret

Step 3: Integration Test

# Send real iMessage and verify it reaches backend
# Check backend logs for:
# "✅ Orchestrator message received from edge agent"

Troubleshooting

Issue: "401 Unauthorized" error

Cause: Webhook secret doesn't match backend

Solution: 1. Check that RELAY_WEBHOOK_SECRET is set correctly:

echo $RELAY_WEBHOOK_SECRET

  1. Verify it matches the backend secret in Railway:

    # Ask backend engineer for the secret
    # Or check Railway dashboard → archety-backend → Variables
    

  2. Make sure the Authorization header is formatted correctly:

    headers = {
        "Authorization": f"Bearer {RELAY_WEBHOOK_SECRET}",
        # NOT: "Authorization": RELAY_WEBHOOK_SECRET
    }
    

Issue: "Connection refused" error

Cause: Backend URL is incorrect or backend is down

Solution: 1. Check backend URL is correct:

curl https://archety-backend-prod.up.railway.app/health

  1. Verify DNS resolution:

    nslookup archety-backend-prod.up.railway.app
    

  2. Check backend logs in Railway dashboard

Issue: Messages not reaching backend

Cause: Could be network, authentication, or backend issue

Solution: 1. Check edge client logs for errors 2. Test health check endpoint:

relay.health_check()
3. Check backend logs in Railway 4. Verify message format is correct


Rollback Plan

If you need to temporarily rollback to old auth (not recommended):

Backend Rollback

Backend engineer needs to temporarily disable auth on orchestrator endpoint:

# app/api/orchestrator_routes.py
# Temporarily comment out auth dependency
@router.post("/message")
async def handle_orchestrator_message(
    request: OrchestratorRequest,
    # current_user: User = Depends(get_current_relay)  # Commented out
):
    # ... rest of code

Edge Client Rollback

Remove Authorization header:

response = requests.post(
    f"{BACKEND_URL}/orchestrator/message",
    # headers=headers,  # Comment out
    json=message_data
)

Note: This rollback is insecure and should only be temporary!


Production Checklist

Before deploying to production edge client:

  • RELAY_WEBHOOK_SECRET is set and matches backend
  • Edge client code updated with Authorization header
  • Health check passes
  • Test message sends successfully
  • Logs show no authentication errors
  • Backend logs show authenticated requests
  • Integration test with real iMessage passes

Environment Setup

Production Mac Mini

# Add to ~/.zshrc or create a startup script
export ARCHETY_BACKEND_URL="https://archety-backend-prod.up.railway.app"
export RELAY_WEBHOOK_SECRET="your-production-secret-here"
export EDGE_AGENT_ID="mac-mini-primary"

Development Mac Mini

export ARCHETY_BACKEND_URL="https://archety-backend-dev.up.railway.app"
export RELAY_WEBHOOK_SECRET="your-dev-secret-here"
export EDGE_AGENT_ID="mac-mini-dev"

Support

For issues or questions: - Backend engineer: Check /docs/implementation/SUPABASE_RLS_AND_STORAGE.md - Edge agent docs: Check /docs/edge/ - Slack: #engineering-support


Last Updated: November 13, 2025 Backend Version: v3.6+ (Supabase Auth) Required Edge Client Version: v2.0+ (Webhook auth)