Documentation
ReferenceArchitecture

Auth & join tokens

tabbify-auth is the single signing authority for your office. It issues Ed25519-signed JWTs in two flavors — user-auth tokens for login and node-join tokens for mesh peers — and every other service validates against it. It matters because join tokens are what decide who gets onto your private mesh, and the token claims — not the peer — are what the coordinator trusts.

Two token kinds

A user-auth token is minted on login (kind: auth). Its claims are sub, iat, exp, jti; default TTL is 24h. It carries no network or tags.

A node-join token is minted for a peer joining the mesh (kind: join). It carries network (the mesh network name) and tags — both authoritative, so a node cannot self-assert what it is allowed to be. Default TTL is 1h.

Both are signed with one Ed25519 key. The public key is served as JWKS at GET /v1/jwks; the kid in each JWT header points at the right key.

Issuing a join token

Join tokens are admin-only. The auth service runs inside your mesh — it is not a public endpoint, and the coordinator and admins reach it over plain HTTP on the internal network. You pass the network and the tags the peer is permitted to claim, and the auth service bakes them into the JWT ($AUTH_URL below is the auth service's internal address):

curl -X POST $AUTH_URL/v1/tokens/join \
  -H 'Authorization: Bearer $AUTH_ADMIN_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"network":"alice","tags":["tag:user-alice"],"ttl":3600,"subject":"alice-laptop"}'
# {"token":"eyJ0eXAiOiJKV1Q...","jti":"019e5ac6-...","kind":"join","expires_at":1779643046}

AUTH_ADMIN_TOKEN guards the raw admin endpoints (register, join, revoke), compared in constant time. Self-serve issuance is also live: the admin panel's your network → Add a node mints a join token scoped to your network (the server stamps the network + tags authoritatively), so you don't handle the admin token directly.

Joining the mesh

The peer presents the token as Authorization: Bearer <JWT> when it registers. In production you almost never type this by hand — the one-command installer joins a machine to the mesh for you, and the coordinator's address is baked into the joiner binary (zero-config), so a join is just tabbify-mesh join --name supervisor-01 --join-token <JWT>.

For a local dev run you point the joiner at your own coordinator and pass the token explicitly:

tabbify-mesh join --name supervisor-01 \
  --coordinator http://127.0.0.1:8888 \
  --insecure-no-mtls \
  --join-token $MESH_JOIN_TOKEN

A few things worth keeping straight:

  • --coordinator is a plain HTTP base URL. In production it is baked in, so you omit the flag entirely; the override above is only for local dev.
  • The --tls-cert / --tls-key / --tls-ca flags (omitted here) are peer-to-peer mTLS for the mesh control plane — they secure the joiner ↔ coordinator transport, not the coordinator's endpoint protocol. For a local smoke test you skip them with --insecure-no-mtls against a coordinator launched the same way.

When the coordinator is started with AUTH_URL set, it calls back to validate every join (POST $AUTH_URL/v1/validate over plain HTTP), then reads network and tags from the token claims, not the request. This is the deliberate fix for ACL spoofing: a node's own --tag flags are ignored once a token is in play.

curl -X POST $AUTH_URL/v1/validate \
  -H 'Content-Type: application/json' -d '{"token":"eyJ0eXAiOiJKV1Q..."}'
# {"valid":true,"subject":"alice-laptop","network":"alice","tags":["tag:user-alice"],"kind":"join","exp":1779643046}

/v1/validate always returns 200 — even for garbage. Validation is a result (valid: true|false), not an error, and it checks signature, exp (no leeway), and revocation.

Revocation

Every issued token's jti is recorded. Revoking is instant:

curl -X DELETE $AUTH_URL/v1/tokens/019e5ac6-ec8d-7b32-bfd0-205066b12dc2 \
  -H 'Authorization: Bearer $AUTH_ADMIN_TOKEN'
# {"jti":"019e5ac6-...","revoked":true}  → next /v1/validate returns {"valid":false}

Revocation is enforced at /v1/validate time. Services that verify locally via JWKS accept eventual consistency until they re-check or the (short) TTL expires.

Honest about scope

This is P1. Mesh coordinator integration is live — it validates join tokens on register today. Without AUTH_URL set on the coordinator (the dev / E1 escape hatch), there is no validation and --tag flags are trusted from the request. Local JWKS validation in hot-path services, key rotation, and self-serve registration are deferred. See self-hosting a node to wire a peer end to end.