GitLoom

Authentication

API keys and dashboard sessions — what each one can do, and the one thing only a session can.

Two credentials reach the same API. An API key is what your server uses. A dashboard session is a Cognito ID token, held by the browser app at https://app.gitloom.cloud. Both are presented the same way:

Authorization: Bearer <credential>

Authorization happens at the gateway, in a Lambda authorizer, before any handler runs. It accepts either credential and resolves both to exactly one account. A missing, malformed, expired or revoked credential is 401 with no detail about which — telling you which half of a guess was right is a gift to whoever is guessing.

GET /health is the only unauthenticated route.

Which credential am I holding?

curl -s "$GITLOOM_API/v1/whoami" \
  -H "Authorization: Bearer $GITLOOM_KEY"
{"account":"acme","auth":"api_key","env":"live"}
field values
account your account id — the owner of every namespace you can reach
auth api_key or jwt
env live or test for a key; empty for a dashboard session

API keys

A key looks like this:

gl_live_3f9a2b7c1d4e6f80_kQ8vN2xR7mZpLd3Wc0Ty5Ub9Aj1Hs4Ge
└┬─┘└─┬┘ └──────┬───────┘ └──────────────┬──────────────┘
 │    │         │                        └─ secret: 24 random bytes, base64url
 │    │         └─ key id: 16 hex chars, appears in logs, used to revoke
 │    └─ environment: live | test
 └─ prefix, so secret scanners can match it

Only the SHA-256 of the secret is stored. The full key exists exactly once, in the 201 response that created it. There is no endpoint that returns it again, and no operator can recover it. Lost key: revoke it, mint another.

Creating a key

Key management requires a dashboard session. An API key calling any /v1/keys route gets:

{"error":{"code":"dashboard_only","message":"API keys cannot manage API keys; sign in to the dashboard"}}

403. That is the whole point: a leaked key that can mint its own replacements survives the revocation meant to contain it.

With a dashboard token:

curl -s -X POST "$GITLOOM_API/v1/keys" \
  -H "Authorization: Bearer $ID_TOKEN" \
  -H "content-type: application/json" \
  -d '{"name":"production worker","env":"live"}'
{
  "id": "3f9a2b7c1d4e6f80",
  "env": "live",
  "name": "production worker",
  "key": "gl_live_3f9a2b7c1d4e6f80_kQ8vN2xR7mZpLd3Wc0Ty5Ub9Aj1Hs4Ge",
  "note": "store this now; it cannot be retrieved again"
}

env defaults to live when omitted. Anything other than live or test is 400 invalid_env.

Listing and revoking

curl -s "$GITLOOM_API/v1/keys" -H "Authorization: Bearer $ID_TOKEN"
{"keys":[{"id":"3f9a2b7c1d4e6f80","env":"live","name":"production worker","revoked":false,"created_at":"2026-07-31T09:14:02Z"}]}

Revoke by key id — the middle segment, never the secret:

curl -s -X DELETE "$GITLOOM_API/v1/keys/3f9a2b7c1d4e6f80" \
  -H "Authorization: Bearer $ID_TOKEN"
{"id":"3f9a2b7c1d4e6f80","revoked":true}

An unknown id is 404 not_found.

### What a key can do

Everything except key management: create and list namespaces, write memories, retrieve, read usage. There is no scoping — a key is account-wide read and write over every namespace. Two consequences:

live and test are not a data boundary

The environment is in the prefix and reported by whoami, and that is all it does today. A gl_test_ key reads and writes exactly the same namespaces as a gl_live_ key on the same account. It is useful for spotting a production credential pasted into a test config, and for secret scanners. It is not a sandbox. If you need one, use a separate namespace — that is a hard boundary — or a separate account.

Dashboard sessions

The dashboard signs in through the Cognito hosted UI and calls the same API with the resulting ID token (not the access token). The account comes from the custom:tenant_id claim, which a Pre-Token-Generation trigger injects from DynamoDB at sign-in — the user cannot set it, and the app client's writable attributes are restricted so a signup cannot claim someone else's tenant.

Tokens expire. A 401 in the dashboard means refresh the session, not that anything is wrong with the account.

Browsers and CORS

CORS on the API allows GET and OPTIONS only, from the dashboard's own origins, with authorization and content-type headers. So from a browser:

This is a real limitation of the current stack rather than a security model. The advice would be identical without it: call GitLoom from your backend and let your backend decide what a browser may ask for.

Handling failures

status code what to do
401 unauthorized check the key is set, unrevoked, and not a stale token
403 dashboard_only you used a key on /v1/keys; sign in instead
404 namespace_not_found create the namespace before reading or writing
429 quota_exceeded or rate_limited see Plans and pricing and Limits. A monthly limit will not clear in a second — a rate limit will, within a minute
5xx internal retry with exponential backoff and jitter

Every route returns the same envelope:

{"error":{"code":"namespace_not_found","message":"create namespace \"user-8213\" before writing to it"}}

Branch on code. message is prose and may be reworded.