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. You pass the network and the tags the peer is permitted to claim, and the auth service bakes them into the JWT:

curl -X POST http://127.0.0.1:8080/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 every admin endpoint (register, join, revoke) and is compared in constant time. Bootstrap-only for now — there is no self-serve issuance.

Joining the mesh

The peer presents the token as Authorization: Bearer <JWT> when it registers. Hand it to the joiner via MESH_JOIN_TOKEN:

tabbify-mesh join --name supervisor-01 \
  --coordinator https://3.124.69.92:8888 \
  --tls-cert ./peer.crt --tls-key ./peer.key --tls-ca ./ca.crt \
  --join-token $MESH_JOIN_TOKEN

When the coordinator is started with AUTH_URL set, it calls back to validate every join, 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 http://127.0.0.1:8080/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 http://127.0.0.1:8080/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.