Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions docs/integration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,11 +286,60 @@ No shared secret. The webhook sender proves its identity with a device-bound sig

---

## Local Development

Use the same `amesh.fetch()` and `amesh.verify()` code paths in local development as in production. No `if (NODE_ENV === 'development') { skipAuth() }` hacks.

### Setup

Use the `encrypted-file` backend with a simple passphrase for local services:

```bash
# Create identities for local services
AUTH_MESH_DIR=/tmp/amesh-a amesh init --name "local-service-a" --backend encrypted-file --passphrase "dev"
AUTH_MESH_DIR=/tmp/amesh-b amesh init --name "local-service-b" --backend encrypted-file --passphrase "dev"

# 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)
```

### Same code in dev and production

Your application code is identical everywhere:

```typescript
// 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 "dev"`

### Tips

- Use a shared passphrase like `"dev"` for all local identities. Security is not the goal — dev/prod parity is.
- Use `AUTH_MESH_DIR` to isolate each service's identity directory.
- For Docker Compose, set `AUTH_MESH_PASSPHRASE=dev` and mount `AUTH_MESH_DIR` as a volume so identities persist across restarts.
- The relay is only needed during initial pairing. Once devices are paired, stop it.

---

## Environment Variables

| Variable | Description | Default |
|----------|-------------|---------|
| `AUTH_MESH_DIR` | Directory for identity and keys | `~/.amesh/` |
| `AUTH_MESH_PASSPHRASE` | Passphrase for encrypted-file backend | (optional) |
| `AMESH_BOOTSTRAP_TOKEN` | Bootstrap token for automated pairing | (optional) |
| `RELAY_URL` | WebSocket relay URL | `wss://relay.authmesh.dev/ws` |
| `REDIS_URL` | Redis URL for nonce store | (optional) |
Expand Down
2 changes: 2 additions & 0 deletions landpage/src/lib/components/Footer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
<div class="mt-3 flex flex-col gap-2">
<a href="/use-cases/microservices" class="text-sm text-zinc-500 hover:text-zinc-300 no-underline transition">Microservices</a>
<a href="/use-cases/webhooks" class="text-sm text-zinc-500 hover:text-zinc-300 no-underline transition">Webhooks</a>
<a href="/use-cases/cron-jobs" class="text-sm text-zinc-500 hover:text-zinc-300 no-underline transition">Cron Jobs</a>
<a href="/use-cases/internal-tools" class="text-sm text-zinc-500 hover:text-zinc-300 no-underline transition">Internal Tools</a>
</div>
</div>
</div>
Expand Down
2 changes: 2 additions & 0 deletions landpage/src/lib/components/Nav.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
const useCases = [
{ slug: 'microservices', title: 'Microservices', desc: 'Service-to-service identity' },
{ slug: 'webhooks', title: 'Webhooks', desc: 'Prove sender identity' },
{ slug: 'cron-jobs', title: 'Cron Jobs', desc: 'Scheduled task identity' },
{ slug: 'internal-tools', title: 'Internal Tools', desc: 'Per-developer audit trail' },
];

let dropdownOpen = $state(false);
Expand Down
36 changes: 36 additions & 0 deletions landpage/src/routes/docs/integration/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,48 @@ app.post(<span class="text-emerald-400">'/webhooks'</span>, amesh.verify(), (req
</div>
</section>

<!-- Local Development -->
<section class="py-8 border-t border-zinc-800">
<h2 class="text-xl font-semibold text-zinc-50">Local Development</h2>
<p class="mt-2 text-sm text-zinc-400">Use the same code paths in local dev as production. No environment checks needed.</p>

<h3 class="mt-6 text-base font-semibold text-zinc-50">Setup</h3>
<div class="mt-3">
<CodeBlock code={`<span class="text-zinc-500"># Create local identities (encrypted-file backend for dev)</span>
AUTH_MESH_DIR=/tmp/amesh-a amesh init --name <span class="text-emerald-400">"local-service-a"</span> --backend encrypted-file --passphrase <span class="text-emerald-400">"dev"</span>
AUTH_MESH_DIR=/tmp/amesh-b amesh init --name <span class="text-emerald-400">"local-service-b"</span> --backend encrypted-file --passphrase <span class="text-emerald-400">"dev"</span>

<span class="text-zinc-500"># Pair them (same ceremony as production)</span>
AUTH_MESH_DIR=/tmp/amesh-b amesh listen <span class="text-zinc-500"># on service-b (target)</span>
AUTH_MESH_DIR=/tmp/amesh-a amesh invite 482916 <span class="text-zinc-500"># on service-a (controller)</span>`} />
</div>

<h3 class="mt-6 text-base font-semibold text-zinc-50">Same code, dev and production</h3>
<div class="mt-3">
<CodeBlock code={`<span class="text-zinc-500">// Same code in dev and production. No environment checks.</span>
import {'{'} amesh {'}'} from <span class="text-emerald-400">'@authmesh/sdk'</span>;

const res = await amesh.fetch(<span class="text-emerald-400">'http://localhost:4000/api/data'</span>, {'{'}
method: <span class="text-emerald-400">'POST'</span>,
body: JSON.stringify({'{'} query: <span class="text-emerald-400">'test'</span> {'}'})
{'}'});`} />
</div>

<div class="mt-4 border-l-2 border-emerald-400/60 pl-4 py-1">
<p class="text-sm text-zinc-400">
<strong class="text-zinc-300">Production:</strong> macOS Keychain, Secure Enclave, or TPM 2.0<br />
<strong class="text-zinc-300">Local dev:</strong> <code class="text-emerald-400">--backend encrypted-file --passphrase "dev"</code>
</p>
</div>
</section>

<!-- Environment variables -->
<section class="py-8 border-t border-zinc-800">
<h2 class="text-xl font-semibold text-zinc-50">Environment Variables</h2>
<div class="mt-4 rounded-lg border border-zinc-800 divide-y divide-zinc-800">
{#each [
{ name: 'AUTH_MESH_DIR', desc: 'Directory for identity and keys', def: '~/.amesh/' },
{ name: 'AUTH_MESH_PASSPHRASE', desc: 'Passphrase for encrypted-file backend', def: 'optional' },
{ name: 'AMESH_BOOTSTRAP_TOKEN', desc: 'Bootstrap token for automated pairing', def: 'optional' },
{ name: 'RELAY_URL', desc: 'WebSocket relay URL', def: 'wss://relay.authmesh.dev/ws' },
{ name: 'REDIS_URL', desc: 'Redis URL for nonce store', def: 'optional' },
Expand Down
8 changes: 5 additions & 3 deletions landpage/src/routes/use-cases/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
<script lang="ts">
import { Network, Webhook, ChevronRight } from '@lucide/svelte';
import { Network, Webhook, Clock, Terminal, ChevronRight } from '@lucide/svelte';

const cases = [
{ slug: 'microservices', icon: Network, title: 'Microservices', desc: 'Every service gets its own device-bound identity. No shared API keys. Per-service audit trail.' },
{ slug: 'webhooks', icon: Webhook, title: 'Webhooks', desc: 'Prove webhook sender identity cryptographically. No HMAC secrets to share and rotate.' },
{ slug: 'cron-jobs', icon: Clock, title: 'Cron Jobs', desc: 'Replace plaintext API keys in scheduled tasks. Per-server audit trail, instant revocation.' },
{ slug: 'internal-tools', icon: Terminal, title: 'Internal Tools', desc: 'Each developer\'s laptop IS their identity. Know who ran what. SOC2-ready audit trail.' },
];
</script>

<svelte:head>
<title>Use Cases — amesh</title>
<meta name="description" content="How teams use amesh to replace API keys in microservices and webhooks with device-bound cryptographic identity." />
<meta name="description" content="How teams use amesh to replace API keys in microservices, webhooks, cron jobs, and internal tools with device-bound cryptographic identity." />
<link rel="canonical" href="https://authmesh.dev/use-cases" />
<meta property="og:title" content="Use Cases — amesh" />
<meta property="og:description" content="How teams use amesh to replace API keys in microservices and webhooks." />
<meta property="og:description" content="How teams use amesh to replace API keys in microservices, webhooks, cron jobs, and internal tools." />
<meta property="og:url" content="https://authmesh.dev/use-cases" />
</svelte:head>

Expand Down
62 changes: 62 additions & 0 deletions landpage/src/routes/use-cases/cron-jobs/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<script lang="ts">
import { Clock } from '@lucide/svelte';
import UseCasePage from '$lib/components/UseCasePage.svelte';
</script>

<svelte:head>
<title>Cron Job Authentication Without API Keys — amesh</title>
<meta name="description" content="Replace plaintext API keys in cron jobs with device-bound identity. Per-server audit trail, instant revocation, nothing to leak." />
<link rel="canonical" href="https://authmesh.dev/use-cases/cron-jobs" />
<meta property="og:title" content="Cron Job Authentication Without API Keys — amesh" />
<meta property="og:description" content="Replace plaintext API keys in cron jobs with device-bound identity. Instant revocation, nothing to leak." />
<meta property="og:url" content="https://authmesh.dev/use-cases/cron-jobs" />
</svelte:head>

<UseCasePage
icon={Clock}
badge="Cron Jobs"
headline="Your cron job has a plaintext API key. So does the attacker."
subtitle="A billing sync, a health check, a nightly data export. The API key is in a .env file or crontab on the server. If the server is compromised, the key works from anywhere."
painTitle="The problem with API keys in scheduled tasks"
painPoints={[
{ lead: 'Keys sit in plaintext on disk', detail: 'The API key lives in a .env file, a crontab, or a systemd unit. Anyone with server access can read it and use it from any machine.' },
{ lead: 'No way to distinguish the cron job from an attacker', detail: 'If someone copies the key, their requests look identical to the cron job\'s. Your logs show the same API key for both.' },
{ lead: 'Rotation means updating every server', detail: 'If you rotate the key, every cron job on every server that uses it needs updating simultaneously. Miss one and the job fails silently at 3 AM.' },
]}
codeTabs={[
{ filename: 'billing-sync.ts', code: `<span class="text-zinc-500">// Cron script — signs every request with device identity</span>
import { amesh } from <span class="text-emerald-400">'@authmesh/sdk'</span>;

const res = await amesh.fetch(
<span class="text-emerald-400">'https://api.internal/billing/sync'</span>,
{ method: <span class="text-emerald-400">'POST'</span>, body: JSON.stringify({ date: <span class="text-emerald-400">'2026-04-01'</span> }) }
);

console.log(<span class="text-emerald-400">\`Sync complete: \${res.status}\`</span>);` },
{ filename: 'api-server.ts', code: `<span class="text-zinc-500">// API server — verifies which device called</span>
import { amesh } from <span class="text-emerald-400">'@authmesh/sdk'</span>;

app.use(amesh.verify());

app.post(<span class="text-emerald-400">'/billing/sync'</span>, (req, res) => {
console.log(<span class="text-emerald-400">\`Sync by: \${req.authMesh.friendlyName}\`</span>);
<span class="text-zinc-500">// "Sync by: cron-server-1" — not "someone with BILLING_KEY"</span>
res.json({ synced: true });
});` },
{ filename: 'Terminal', code: `<span class="text-zinc-500"># On the API server (target):</span>
<span class="text-zinc-500">$</span> <span class="text-zinc-50">amesh init --name "billing-api"</span>
<span class="text-zinc-500">$</span> <span class="text-zinc-50">amesh listen</span>

<span class="text-zinc-500"># On the cron server (controller):</span>
<span class="text-zinc-500">$</span> <span class="text-zinc-50">amesh init --name "cron-server-1"</span>
<span class="text-zinc-500">$</span> <span class="text-zinc-50">amesh invite 482916</span>

<span class="text-zinc-500"># Server compromised? Revoke just that one device:</span>
<span class="text-zinc-500">$</span> <span class="text-zinc-50">amesh revoke am_7f2e8a1b</span>` },
]}
changes={[
{ before: 'API key in .env or crontab', after: 'No key. Device identity signs the request.' },
{ before: 'Can\'t tell cron job from attacker', after: 'req.authMesh.deviceId on every request' },
{ before: 'Compromised server = key works anywhere', after: 'amesh revoke kills that device only' },
]}
/>
67 changes: 67 additions & 0 deletions landpage/src/routes/use-cases/internal-tools/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<script lang="ts">
import { Terminal } from '@lucide/svelte';
import UseCasePage from '$lib/components/UseCasePage.svelte';
</script>

<svelte:head>
<title>Internal Tools & Audit Trail — amesh</title>
<meta name="description" content="Replace shared admin API keys with per-developer device identity. Know exactly who ran what. SOC2 compliance without Okta or AWS IAM." />
<link rel="canonical" href="https://authmesh.dev/use-cases/internal-tools" />
<meta property="og:title" content="Internal Tools & Audit Trail — amesh" />
<meta property="og:description" content="Replace shared admin API keys with per-developer device identity. SOC2 compliance without Okta." />
<meta property="og:url" content="https://authmesh.dev/use-cases/internal-tools" />
</svelte:head>

<UseCasePage
icon={Terminal}
badge="Internal Tools"
headline="Five developers share one admin key. Your SOC2 auditor is not impressed."
subtitle="Admin scripts hit production with a shared API key from 1Password. No way to know who ran what. When someone leaves, nobody rotates the key."
painTitle="The problem with shared admin keys"
painPoints={[
{ lead: 'No individual accountability', detail: 'Multiple developers use the same key to run scripts against production. Your logs show one API key for every admin action. When something breaks, you can\'t tell who did it.' },
{ lead: 'Offboarding is a coordination nightmare', detail: 'When a developer leaves, you should rotate the shared key. But that means updating every script, every developer\'s .env, and every pipeline that uses it. So nobody does it.' },
{ lead: 'SOC2 auditors ask who accessed what', detail: 'Compliance requires attributing actions to individuals. A shared API key makes this impossible without building a separate logging layer on top.' },
]}
codeTabs={[
{ filename: 'admin-script.ts', code: `<span class="text-zinc-500">// Admin script — identity comes from the developer's laptop</span>
import { amesh } from <span class="text-emerald-400">'@authmesh/sdk'</span>;

<span class="text-zinc-500">// No API key. The developer's device IS the credential.</span>
const res = await amesh.fetch(
<span class="text-emerald-400">'https://admin.internal/cache/purge'</span>,
{ method: <span class="text-emerald-400">'POST'</span>, body: JSON.stringify({ pattern: <span class="text-emerald-400">'users:*'</span> }) }
);

console.log(<span class="text-emerald-400">\`Purged: \${(await res.json()).count} keys\`</span>);` },
{ filename: 'admin-api.ts', code: `<span class="text-zinc-500">// Admin API — knows exactly who called</span>
import { amesh } from <span class="text-emerald-400">'@authmesh/sdk'</span>;

app.use(<span class="text-emerald-400">'/admin'</span>, amesh.verify());

app.post(<span class="text-emerald-400">'/admin/cache/purge'</span>, (req, res) => {
<span class="text-zinc-500">// "alice-macbook (am_3d9f1a2e) purged users:* at 10:15 AM"</span>
audit.log({
who: req.authMesh.friendlyName,
device: req.authMesh.deviceId,
action: <span class="text-emerald-400">'cache.purge'</span>,
params: req.body,
});
res.json({ count: 847 });
});` },
{ filename: 'Terminal', code: `<span class="text-zinc-500"># Each developer's laptop gets its own identity (uses macOS Keychain):</span>
<span class="text-zinc-500">$</span> <span class="text-zinc-50">amesh init --name "alice-macbook"</span>
<span class="text-zinc-500">$</span> <span class="text-zinc-50">amesh invite 482916</span>

<span class="text-zinc-500"># Alice leaves the team? Revoke her device. Bob is unaffected:</span>
<span class="text-zinc-500">$</span> <span class="text-zinc-50">amesh revoke am_3d9f1a2e</span>
<span class="text-emerald-400">&#10004;</span> Removed. Access revoked immediately.

<span class="text-zinc-500"># No shared key to rotate. No other developer's access changes.</span>` },
]}
changes={[
{ before: 'Shared admin key in 1Password', after: 'Each developer\'s laptop IS their identity' },
{ before: '"Someone ran the reset-db script"', after: '"Alice ran it from alice-macbook at 10:15 AM"' },
{ before: 'Offboarding = rotate shared key', after: 'amesh revoke <device-id>. Others unaffected.' },
]}
/>
Loading