@aikofy/client-db-sync
v0.1.2
Published
Minimal WebRTC signaling server for @aikofy/client-db with Ed25519 JWT auth
Maintainers
Readme
@aikofy/client-db-sync
Minimal WebRTC signaling server for @aikofy/client-db.
Handles peer discovery (offer/answer/ICE exchange) over WebSocket. Protected by Ed25519 JWT tokens with configurable TTL — not freely accessible without a valid token.
How It Works
Your backend ──POST /token──► Signaling server (issues JWT)
│
Client A ──WS /signal?token=JWT──► │ ◄──WS /signal?token=JWT── Client B
◄── peer-list ────┤
── offer ────────►│──────────────────────────►
◄── answer ───────│◄──────────────────────────
── ice-candidate ►│──────────────────────────►After the WebRTC handshake, all sync data flows directly peer-to-peer — nothing passes through this server.
Quick Start
1. Generate keys
npx @aikofy/client-db-sync-keygen
# or
bun run keygen # if installed locallyCopy the output into your .env file.
2. Configure
Production:
# .env
PRIVATE_KEY_JWK=<output from keygen>
PUBLIC_KEY_JWK=<output from keygen>
ADMIN_SECRET=a-strong-random-secret
PORT=8080Local development (no auth):
# .env
AUTH_DISABLED=true
PORT=8080⚠️
AUTH_DISABLED=truelets any client connect without a token. Never use it in production. The server logs a warning at startup when this is set.
3. Run
# With npx (no install needed)
npx @aikofy/client-db-sync
# Or install globally
npm install -g @aikofy/client-db-sync
client-db-sync
# Or as a local dependency
npm install @aikofy/client-db-sync
node node_modules/@aikofy/client-db-sync/dist/index.jsEnvironment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| AUTH_DISABLED | No | Set to true to disable JWT auth (dev only) |
| PRIVATE_KEY_JWK | Yes (auth on) | Base64-encoded Ed25519 private key JWK |
| PUBLIC_KEY_JWK | Yes (auth on) | Base64-encoded Ed25519 public key JWK |
| ADMIN_SECRET | Yes (auth on) | Secret for the POST /token endpoint |
| PORT | No | Server port (default 8080) |
Docker
FROM node:22-alpine
RUN npm install -g @aikofy/client-db-sync
EXPOSE 8080
CMD ["client-db-sync"]docker run -p 8080:8080 \
-e PRIVATE_KEY_JWK="..." \
-e PUBLIC_KEY_JWK="..." \
-e ADMIN_SECRET="..." \
your-imageAPI
GET /health
Returns server status and connected peer count. No authentication required.
{ "status": "ok", "peers": 3, "ts": "2026-01-01T00:00:00.000Z" }GET /public-key
Returns the Ed25519 public key as a JWK. No authentication required.
Clients can use this to verify tokens locally if needed.
{
"alg": "EdDSA",
"crv": "Ed25519",
"kty": "OKP",
"x": "..."
}POST /token
Issues a signed JWT. Requires the admin secret.
Headers:
x-admin-secret: <your ADMIN_SECRET>or:
Authorization: Bearer <your ADMIN_SECRET>Body:
{
"ttl": "24h",
"subject": "my-app"
}| Field | Type | Description |
|-------|------|-------------|
| ttl | string | Token lifetime — e.g. "1h", "7d", "30m" |
| subject | string (optional) | Label for auditing (e.g. app or user name) |
Response 201:
{
"token": "eyJ...",
"expiresAt": "2026-01-02T00:00:00.000Z",
"subject": "my-app"
}WS /signal?token=<jwt>&nodeId=<uuid>
WebSocket endpoint for WebRTC signaling.
| Query param | Required | Description |
|-------------|----------|-------------|
| token | Yes | JWT issued by POST /token |
| nodeId | No | Client's node UUID (falls back to JWT subject) |
Close codes:
| Code | Reason |
|------|--------|
| 4001 | Missing token |
| 4003 | Invalid or expired token |
| 1000 | Replaced by a new connection from the same nodeId |
Connecting from @aikofy/client-db
Your backend issues a token and passes it to the client:
// Backend (Node.js / any server)
const res = await fetch('https://your-signal-server.example.com/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-admin-secret': process.env.ADMIN_SECRET,
},
body: JSON.stringify({ ttl: '24h', subject: req.user.id }),
});
const { token } = await res.json();
// Return token to the client// Client (React / browser)
import { createDB } from '@aikofy/client-db';
const db = await createDB({
name: 'myapp',
version: 1,
collections: { todos: { indexes: ['status'] } },
sync: {
signalingServer: `wss://your-signal-server.example.com/signal?token=${token}`,
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
},
});Programmatic API
Use as a library inside your own server:
import { createServer, generateKeyPairJwk, issueToken } from '@aikofy/client-db-sync';
// Generate keys once and store them in env/secrets manager
const { privateKeyJwk, publicKeyJwk } = await generateKeyPairJwk();
const app = await createServer({
port: 8080,
adminSecret: 'my-secret',
privateKeyJwk,
publicKeyJwk,
});
await app.listen({ port: 8080, host: '0.0.0.0' });Security Notes
- Never expose
PRIVATE_KEY_JWK— only the server needs it PUBLIC_KEY_JWKis safe to share with clients for local token verificationADMIN_SECRETshould only be known by your backend — never sent to browsers- Tokens are signed with Ed25519 (
EdDSA) and verified on every WebSocket connection - Expired tokens are rejected — reconnect logic in
@aikofy/client-dbhandles token refresh
Project Structure
src/
types.ts # Message types + config interfaces
keys.ts # Ed25519 key pair loading and generation
auth.ts # JWT issuance and verification
signaling.ts # Peer registry + message routing
server.ts # Fastify server + routes
index.ts # CLI entry point + programmatic exports
scripts/
generate-keys.ts # Key generation helper (bin: client-db-sync-keygen)Contributing
bun install
bun run build
bun run typecheckLicense
MIT © Lwin Maung Maung
