Issue Service-Account Tokens
Magic-link login works when a human is in front of the email client;
it breaks down for unattended agents. The
fit-map auth issue verb closes that gap: it mints a
Supabase-shaped JWT for an existing roster row, and the operator
hands the token to the agent as LANDMARK_AUTH_TOKEN.
The same verb works for human emails too, but the canonical use case
is a service-account row — an identity that exists solely so an
agent can take it on. Service-account rows live in the same
organization_people table as humans (with
kind = 'service_account') and share the same
row-level security clamp.
This guide is for operators running the verb against a Supabase project. Engineers do not run it.
Prerequisites
-
MAP_SUPABASE_URL,MAP_SUPABASE_SERVICE_ROLE_KEY, andMAP_SUPABASE_JWT_SECRETexported in your shell.-
Local stack —
fit-map activity startprints all three. - Hosted Supabase — find them in Project Settings → API → Project URL, Service Role Key, and JWT Secret.
-
Local stack —
-
The target email already has both an
organization_peoplerow and anauth.usersrow. Runfit-map people pushthenfit-map people provisionif it doesn't.
Mint a token
fit-map auth issue --email kata-agent-team@example.com
The verb prints the JWT followed by an export hint:
Issued JWT for kata-agent-team@example.com (service_account, ttl=8760h)
eyJhbGciOi...
Export: LANDMARK_AUTH_TOKEN=<jwt above>; never commit or echo it.
Done.
The default TTL is one year. Override with --ttl:
| Suffix | Meaning | Example |
|---|---|---|
h |
hours | --ttl 24h |
d |
days | --ttl 90d |
y |
years |
--ttl 1y (equivalent to
--ttl 365d or --ttl 8760h)
|
Service-account rows in the synthetic DSL
Terrain fixtures declare service-account rows alongside humans:
people {
count 50
...
service_account "kata-agent-team" {
name "Kata Agent Team"
email "kata-agent-team@example.com"
}
}
The renderer emits these as
kind: service_account entries with no
level, manager_email, or
team. fit-map people push accepts the
field; the DB check constraint enforces
level IS NULL when
kind = 'service_account'.
Hand the token to the agent
Write the JWT to the agent's .env (or your secret
manager). Once LANDMARK_AUTH_TOKEN is exported in the
agent's environment, every fit-landmark invocation
resolves identity directly from the token:
LANDMARK_AUTH_TOKEN=$JWT fit-landmark voice
No magic-link, no refresh flow — the long-lived JWT verifies under
MAP_SUPABASE_JWT_SECRET on the Postgres side, RLS
clamps the result to the service-account's row class, and the
agent runs unattended.
Security guidance
Treat the JWT like an SSH key:
- Never commit it. Even in a private repo, a leaked one-year token is a one-year leak.
- Store it in a secret manager. GitHub Actions secrets, AWS Secrets Manager, HashiCorp Vault — anywhere with audit logging.
-
Scope per agent. Mint a separate token per agent
identity so a compromise can be contained by banning that one
auth.usersrow. - Rotate proactively. A year is the default ceiling, not a target. Shorter TTLs cap exposure.
Revoke a token
There is no separate revocation verb. Tokens revoke at the
auth.users level: ban the row, and every outstanding
JWT for it stops resolving on the next Supabase Auth check.
# Remove the row from organization_people and re-run provision —
# the auth.users row gets banned (banned_until ≥100 years).
fit-map people provision
To bring the identity back, re-add the roster row and run
provision again. Then mint a fresh token; the old one
stays inert.