How to add amesh to your existing application. Each recipe is self-contained — pick the one that matches your setup.
┌─────────────────────────────────────────────────────────────────────┐
│ PAIRING (one-time) │
│ │
│ Your Server (target) ◄──WS──► Relay ◄──WS──► Client (controller) │
│ amesh listen amesh invite 482916 │
│ │
│ Both sides verify a 6-digit code, then exchange public keys. │
│ Trust is one-way: controller → target. Relay can be shut down. │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ RUNTIME (every request) │
│ │
│ Controller ────HTTP + AuthMesh header────► Target │
│ amesh.fetch() amesh.verify() │
│ │
│ One-way. No relay. Stateless headers. Target cannot call back. │
└─────────────────────────────────────────────────────────────────────┘
Replace Bearer token authentication with device-bound cryptographic identity.
npm install @authmesh/sdk express// server.ts
import express from 'express';
import { amesh } from '@authmesh/sdk';
const app = express();
app.use(express.json());
// Add amesh verification middleware — checks signature, timestamp, nonce, allow list
// Works with express.json(), express.text(), or no body parser at all.
app.use('/api', amesh.verify());
// Public endpoint (no auth)
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
// Protected endpoint
app.post('/api/orders', (req, res) => {
res.json({
message: 'Authenticated!',
device: req.authMesh.deviceId,
name: req.authMesh.friendlyName,
});
});
app.listen(3000, () => console.log('Server on :3000'));npm install @authmesh/sdk// client.ts
import { amesh } from '@authmesh/sdk';
const res = await amesh.fetch('http://localhost:3000/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: 100 }),
});
console.log(await res.json());
// { message: 'Authenticated!', device: 'am_...', name: 'My Laptop' }# Install the CLI
npm install -g @authmesh/cli
# On the server (target): create identity
amesh init --name "prod-api"
# On your laptop (controller): create identity
amesh init --name "my-laptop"
# Start the relay (needed only for pairing)
bunx @authmesh/relay
# On the server (target): start listening for pairing
amesh listen
# ✔ "my-laptop" added as controller.
# On your laptop (controller): pair with the server
amesh invite 482916
# ✔ "prod-api" added as target.
# Trust is one-way: laptop → server. The relay can be stopped now.When your server is remote (cloud VM, EC2, etc.), both machines need to reach the same relay.
amesh provides a free relay at relay.authmesh.dev:
# On the remote server (target — SSH in)
amesh listen --relay wss://relay.authmesh.dev/ws
# On your laptop (controller)
amesh invite 482916 --relay wss://relay.authmesh.dev/ws# On the remote server (target)
bunx @authmesh/relay # starts on port 3001
amesh listen --relay ws://localhost:3001/ws
# On your laptop (controller — use the server's public IP or domain)
amesh invite 482916 --relay ws://your-server:3001/wsFor servers where you can't run interactive commands:
# On your laptop — generate a token
amesh provision --name "prod-server" --ttl 1h
# Outputs: AMESH_BOOTSTRAP_TOKEN=eyJ...
# Set the token as an env var on the remote server, then run your app.
# The SDK auto-pairs on first request.For production, you should host your own relay. See the Self-Hosting Guide for Docker, Cloud Run, Fly.io, Kubernetes, and plain Node.js deployment options.
Each service gets its own device identity. Services pair once, then authenticate every request. Trust is one-way: the caller (controller) authenticates to the API (target), not vice versa.
import express from 'express';
import { amesh } from '@authmesh/sdk';
const app = express();
app.use(express.json());
app.use(amesh.verify());
app.get('/internal/users/:id', (req, res) => {
console.log(`Request from ${req.authMesh.friendlyName} (${req.authMesh.deviceId})`);
res.json({ id: req.params.id, name: 'Alice' });
});
app.listen(4000);import { amesh } from '@authmesh/sdk';
async function getUser(id: string) {
const res = await amesh.fetch(`http://service-b:4000/internal/users/${id}`);
return res.json();
}# On service-b machine (target — the API):
amesh init --name "service-b"
amesh listen
# On service-a machine (controller — the caller):
amesh init --name "service-a"
amesh invite 482916
# One-way trust: service-a → service-b.
# service-b's allow list has service-a as [controller].
# service-b cannot authenticate back to service-a.Bidirectional auth: If two services need to call each other, pair them twice — each side runs
amesh listenonce andamesh inviteonce. Each pairing creates a separate one-way trust relationship.
If you run multiple instances of your server (behind a load balancer), use Redis to share the nonce store. Without this, a replay attack could succeed by hitting a different instance.
npm install @authmesh/sdk ioredisimport express from 'express';
import { amesh } from '@authmesh/sdk';
import { RedisNonceStore } from '@authmesh/sdk/redis';
const app = express();
app.use(express.json());
app.use(amesh.verify({
nonceStore: new RedisNonceStore(process.env.REDIS_URL),
}));
app.post('/api/orders', (req, res) => {
res.json({ device: req.authMesh.deviceId });
});
app.listen(3000);If you don't provide a nonceStore, amesh uses an in-memory store and prints a warning in production.
Instead of signing webhooks with a shared secret (like WEBHOOK_SECRET), sign them with amesh.
import { amesh } from '@authmesh/sdk';
async function sendWebhook(url: string, event: object) {
const res = await amesh.fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event),
});
return res.ok;
}import express from 'express';
import { amesh } from '@authmesh/sdk';
const app = express();
app.use(express.text({ type: '*/*' }));
// Only accept webhooks from paired devices
app.post('/webhooks', amesh.verify(), (req, res) => {
const event = JSON.parse(req.body);
console.log(`Webhook from ${req.authMesh.friendlyName}:`, event);
res.sendStatus(200);
});No shared secret. The webhook sender proves its identity with a device-bound signature.
Use the same amesh.fetch() and amesh.verify() code paths in local development as in production. No if (NODE_ENV === 'development') { skipAuth() } hacks.
Use the encrypted-file backend for local services (passphrase auto-generated):
# Create identities for local services
AUTH_MESH_DIR=/tmp/amesh-a amesh init --name "local-service-a" --backend encrypted-file
AUTH_MESH_DIR=/tmp/amesh-b amesh init --name "local-service-b" --backend encrypted-file
# Start the relay (needed only for pairing)
bunx @authmesh/relay
# Pair them (same as production)
AUTH_MESH_DIR=/tmp/amesh-b amesh listen # on service-b (target)
AUTH_MESH_DIR=/tmp/amesh-a amesh invite 482916 # on service-a (controller)Your application code is identical everywhere:
// This is the SAME code in dev and production. No environment checks.
import { amesh } from '@authmesh/sdk';
const res = await amesh.fetch('http://localhost:4000/api/data', {
method: 'POST',
body: JSON.stringify({ query: 'test' }),
});The only difference is the key storage backend:
- Production: macOS Keychain, Secure Enclave, or TPM 2.0
- Local dev:
--backend encrypted-file(passphrase auto-generated)
- Use
AUTH_MESH_DIRto isolate each service's identity directory. - For Docker Compose, mount
AUTH_MESH_DIRas a volume so identities (and their auto-generated passphrases) persist across restarts. - The relay is only needed during initial pairing. Once devices are paired, stop it.
| Variable | Description | Default |
|---|---|---|
AUTH_MESH_DIR |
Directory for identity and keys | ~/.amesh/ |
AUTH_MESH_PASSPHRASE |
Override auto-generated passphrase for encrypted-file backend (rarely needed) | (optional) |
AMESH_BOOTSTRAP_TOKEN |
Bootstrap token for automated pairing | (optional) |
AMESH_RELAY_URL |
WebSocket relay URL | wss://relay.authmesh.dev/ws |
REDIS_URL |
Redis URL for nonce store | (optional) |
// Available on req.authMesh after amesh.verify()
interface AuthMeshIdentity {
deviceId: string; // e.g. "am_cOixWcOdI8-pLh4P"
friendlyName: string; // e.g. "prod-api"
verifiedAt: number; // Unix timestamp of verification
}
// amesh.verify() options
interface VerifyOptions {
clockSkewSeconds?: number; // Default: 30
nonceWindowSeconds?: number; // Default: 60
nonceStore?: NonceStore; // Default: InMemoryNonceStore
}- Devices not paired? Run
amesh liston the server — the client's device ID must be in the allow list. - Clock skew? Server and client clocks must be within 30 seconds. Check with
dateon both machines. - Body mismatch? The middleware handles
express.json(),express.text(), and raw streams automatically. If you use a custom body parser that transforms the body (e.g., XML parsing, decompression), ensure the original body is preserved.
The allow list file (~/.amesh/allow_list.json) was modified outside of amesh. This is the HMAC seal detecting tampering. Re-pair the devices to regenerate the file.
You're running in production without a Redis nonce store. Replay attacks could succeed by hitting different instances. See Recipe 3 above.
amesh prefers hardware-backed storage (Secure Enclave, macOS Keychain, TPM 2.0). When none is available, it falls back to the encrypted-file backend automatically with an auto-generated passphrase.
To upgrade to hardware-backed storage:
- macOS: Ensure the Swift helper binary is installed alongside the
ameshbinary for Keychain/Secure Enclave support. - Linux: Install
tpm2-toolsfor TPM 2.0 support.
Then re-run amesh init --force to regenerate with the hardware backend.