Sync Architecture
Cross-device sync for the Hone IDE. Edit on desktop and see changes on mobile (and vice versa) in real time. Sync is off by default – users opt in via Settings > Sync.
Components
Auth Service (auth.hone.codes, port 8445)
Magic-link login, device token management, project entitlements. Built with Fastify, backed by MySQL on webserver.skelpo.net.
Relay Server (sync.hone.codes, ports 8443/8444)
WebSocket message router between devices in the same room. SQLite persistence for buffering messages during disconnects.
Auth Flow
1. User enters email in IDE settings
2. IDE calls GET /auth/login?email=...
3. Auth creates 64-char random hex token, stores in magic_links table (15-min expiry)
4. Email sent to user (or logged to console in dev mode)
5. User clicks link: GET /auth/verify?token=...&deviceName=...&platform=...
6. Auth validates token, creates user + device records, generates device token
7. IDE stores deviceToken locally
Device Token Format
userId:deviceId:timestamp.hash
Where hash is a double-djb2 HMAC. A shared secret between auth and relay allows the relay to validate tokens locally without calling back to auth.
Projects and Tiers
Projects map local workspaces to relay rooms. Each project has a projectKey, name, roomId, and userId.
| Tier | Synced Projects |
|---|---|
| free | 0 |
| personal | 1 |
| pro | unlimited |
| team | unlimited |
Project registration is idempotent – registering the same project key twice returns the existing record.
Relay Protocol
Join
{
"type": "join",
"room": "<roomId>",
"device": "<deviceId>",
"token": "<deviceToken>"
}
Joined (response)
{
"type": "joined",
"room": "<roomId>",
"device": "<deviceId>"
}
Messages
{
"from": "<deviceId>",
"to": "host | broadcast | <targetDeviceId>",
"room": "<roomId>",
"payload": { ... }
}
Relay Behavior
- Room-based routing – room IDs hashed via djb2 for slot assignment
- Slot tracking – each connection occupies a slot in a room
- Host election – first device to join a room becomes host
- 60-second message buffer – messages buffered for reconnecting devices (covers brief disconnects)
- Rate limiting – per-connection rate limiting to prevent abuse
- Auth bypass – when
auth.secretis empty, token validation is skipped (dev mode only)
Auth Endpoints
| Method | Path | Purpose |
|---|---|---|
| GET | /auth/login | Initiate magic-link login (params: email) |
| GET | /auth/verify | Verify magic link and register device (params: token, deviceName, platform) |
| GET | /auth/device | Get device info (header: Authorization) |
| GET | /auth/devices | List all devices for the authenticated user |
| DELETE | /auth/device/:id | Remove a device |
| POST | /auth/project | Register a project for sync |
| GET | /auth/projects | List synced projects |
| DELETE | /auth/project/:id | Remove a synced project |
| GET | /auth/subscription | Get current subscription tier |
All endpoints except /auth/login and /auth/verify require the Authorization: Bearer <deviceToken> header.
Database Schema
users
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP
);
devices
CREATE TABLE devices (
id INT AUTO_INCREMENT PRIMARY KEY,
userId INT NOT NULL,
deviceName VARCHAR(255) NOT NULL,
platform VARCHAR(50) NOT NULL,
token VARCHAR(255) NOT NULL UNIQUE,
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
lastSeen DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE
);
magic_links
CREATE TABLE magic_links (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL,
token VARCHAR(255) NOT NULL UNIQUE,
expiresAt DATETIME NOT NULL,
used BOOLEAN DEFAULT FALSE,
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP
);
projects
CREATE TABLE projects (
id INT AUTO_INCREMENT PRIMARY KEY,
userId INT NOT NULL,
projectKey VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
roomId VARCHAR(255) NOT NULL UNIQUE,
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE KEY unique_user_project (userId, projectKey)
);
subscriptions
CREATE TABLE subscriptions (
id INT AUTO_INCREMENT PRIMARY KEY,
userId INT NOT NULL UNIQUE,
tier ENUM('free', 'personal', 'pro', 'team') DEFAULT 'free',
expiresAt DATETIME,
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE
);
All identifiers use camelCase (matching Hone’s database convention).
IDE Settings
| Setting | Default | Description |
|---|---|---|
syncEnabled | false | Enable cross-device sync |
syncRelayUrl | wss://sync.hone.codes | Relay server WebSocket URL |
syncAuthUrl | https://auth.hone.codes | Auth service URL |
syncDeviceToken | (empty) | Stored device token after login |
Deployment
Both services run on 84.32.223.50 (Ubuntu 24.04):
- TLS termination via Let’s Encrypt + nginx reverse proxy
- Auth config:
auth.conf(KEY=VALUE format) - Relay config:
relay.conf(KEY=VALUE format) - MySQL database: host=webserver.skelpo.net, user=hone
Config File Format
# auth.conf
db.host=webserver.skelpo.net
db.user=hone
db.password=...
db.name=hone
auth.secret=<shared-secret>
smtp.host=...
smtp.port=587
smtp.user=...
smtp.password=...
# relay.conf
auth.secret=<shared-secret>
sqlite.path=/var/lib/hone-relay/relay.db
Self-Hosted
Users can run their own relay and auth services. Both are single native binaries:
- Compile:
perry compile src/app.ts --output hone-auth/hone-relay - Create config file (
auth.conf/relay.conf) - Run the binary
Point the IDE settings (syncRelayUrl, syncAuthUrl) at your own server.