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. The operator then
hands the token to the agent as PRODUCT_LANDMARK_TOKEN.
The same verb works for human emails too. The canonical use case is
still a service-account row. A service-account row is 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 who run the verb against a Supabase project. Engineers do not run it.
Prerequisites
-
SUPABASE_URL,SUPABASE_SERVICE_ROLE_KEY, andJWT_SECRETavailable in your environment.-
Local stack —
just env-setupwrites all three to.env. - 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-terrain substrate 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: PRODUCT_LANDMARK_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 you export PRODUCT_LANDMARK_TOKEN in the
agent's environment, every fit-landmark invocation
resolves identity directly from the token:
PRODUCT_LANDMARK_TOKEN=$JWT fit-landmark voice
The agent needs no magic-link and no refresh flow. The long-lived
JWT verifies under JWT_SECRET on the Postgres side. RLS
clamps the result to the service-account's row class. 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. Use GitHub Actions secrets, AWS Secrets Manager, or HashiCorp Vault. Any store with audit logging works.
-
Scope per agent. Mint a separate token per agent
identity. You can then contain a compromise when you ban that one
auth.usersrow. - Rotate proactively. A year is the default ceiling. It is 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. Every outstanding JWT
for it then fails 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-terrain substrate 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.
What's next
Demonstrate Engineering Progress
Walk into a quarterly review with system-level trends, marker evidence, and engineer voice. Demonstrate progress and do not single out individuals.
Provision Engineer Auth Users
Reconcile Supabase Auth users against the activity roster so identity-derived row-level security works.
Sign In to Landmark
Sign in with a Supabase magic-link so Landmark commands resolve your identity. You do not manage a long-lived token.