Adding Services

Step-by-Step Guide

1. Choose a Port

Select an available port from the appropriate category:

Category Port Range Example
Document Processing 5100-5199 5102, 5103, …
Voice & Audio 5200-5299 5220, 5221, …
Correspondence 5300-5399 5310, 5311, …
Text Processing 5400-5499 5410, 5411, …
Events & Reporting 5500-5599 5510, 5511, …
Heritage Projects 5720-5799 5720, 5721, …

Check existing ports in: {PLATFORM_DIR}/projects/registry.json

2. Add Route Configuration

Edit {PLATFORM_DIR}/config/routes.yaml:

- path: /myservice
  port: 5150
  name: My Service Name
  domain: myservice.lettre.ai
  description: Brief description of the service

Backends on another machine, and a fallback

host: names the tailnet machine that runs the backend when it is not the tunnel machine (the editions run on openscience, 100.87.180.115; INQUA on mac-studio, 100.102.189.23). fallback_hosts: lists further machines that run the same backend on the same port:

- path: /kepler-gw
  port: 5104
  host: 100.87.180.115              # openscience: faster uplink for images and data
  fallback_hosts: [100.102.189.23]  # mac-studio runs the same editions supervisor
  health_url: http://100.87.180.115:5104/

The proxy probes every upstream (the health path of health_url on each host), forwards to the first healthy one in declared order, and fails over at once when a host refuses or does not answer the connect: a request that never reached a backend moves to the next host for any method, a request the backend may have started (read timeout) only for GET, HEAD and OPTIONS. A failed host is marked down until the next probe sees it. The answering host is in the X-Upstream response header (host:port), /_proxy/state lists upstreams with their health and the active_upstream of each route. Both proxies (mac-studio and openscience) read the same synced routes.yaml; on each machine the host that is itself becomes loopback. platform/scripts/editionsctl.py check probes every route on both proxies and on the public address.

3. Create Your Service

Your service must: - Listen on the configured port - Bind to 0.0.0.0 (all interfaces) - Serve on / (router strips the path prefix)

Example FastAPI service:

from fastapi import FastAPI
import uvicorn

app = FastAPI()

@app.get("/")
def root():
    return {"message": "Hello from My Service"}

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=5150)

4. Create LaunchAgent (Optional)

For auto-start on login, create {HOME_DIR}/Library/LaunchAgents/com.myservice.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "...">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.myservice</string>
    <key>ProgramArguments</key>
    <array>
        <string>conda</string>
        <string>run</string>
        <string>-n</string>
        <string>voicememo-py314</string>
        <string>python</string>
        <string>/path/to/your/server.py</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
</dict>
</plist>

Load with: launchctl load ~/Library/LaunchAgents/com.myservice.plist

5. Configure IONOS Domain

See IONOS Setup for configuring the subdomain.

6. Restart Router

# Kill existing router
pkill -f "ai-platform/router/server.py"

# Start new router (machine-portable)
cd {PLATFORM_DIR}
conda run -n voicememo-py314 python router/server.py

Or reload LaunchAgent:

launchctl unload ~/Library/LaunchAgents/com.ai-platform.router.plist
launchctl load ~/Library/LaunchAgents/com.ai-platform.router.plist

7. Test

# Test via local router
curl http://localhost:5001/myservice/

# Test via ngrok
curl https://lettre.ngrok.dev/myservice/

# Test via IONOS domain
curl https://myservice.lettre.ai