## TL;DR
Supabase JWTs expire (user sessions default to one hour), and an expired token fails every call until it is refreshed. Catch the expired-token failure, call refresh_session, retry the request once, and track expiry so you refresh before it lapses. Never hardcode a long-lived key to dodge this; handle the refresh.

## Error
```text
JWT expired
```

## Steps

1. Reproduce and confirm the failure is expiry, not a bad key. Decode the token payload (the middle segment of the JWT) and check the exp claim against the current time.
Expected output: exp is in the past. If the token never parses, it is malformed or the wrong key, which is a different fix.

2. Refresh the session and retry the failed call once:
```python
session = supabase.auth.refresh_session()
result = supabase.table("events").select("*").execute()
```
Expected output: the refresh returns a new session and the retried query succeeds. One retry is enough; looping retries on a still-expired token just burns calls.

3. Wrap data access so expiry is handled in one place instead of every call site:
```python
def query_with_refresh(fn):
    try:
        return fn()
    except Exception as e:
        if "JWT expired" in str(e):
            supabase.auth.refresh_session()
            return fn()
        raise
```
Expected output: transient expiries self-heal. Genuine auth failures (revoked user, wrong key) still raise.

4. Track expiry proactively for long jobs: read exp from the session at startup and refresh when the remaining lifetime drops under a few minutes, rather than waiting for a failure mid-run.
Expected output: long-running pipelines stop dying halfway with JWT expired.

## When to use
- Supabase calls fail with "JWT expired" after working earlier
- Long-running jobs or agents die partway with auth errors
- You rotated anon or service keys and old tokens are in flight

## When not to use
- Every call fails from the start (that is a wrong key or wrong project URL)
- The error is "invalid JWT" rather than expired (check the key value and format)
- Row-level security blocks the query (that returns a policy error, not expiry)

## Variant phrasings

### anon key itself expired after rotation
You rotated keys in the dashboard; clients holding the old key get JWT expired. Ship the new key and refresh sessions.

### JWT expired only in one environment
That environment has a stale key baked into its config. Compare the key against the dashboard value.

## Why it happens
Supabase auth is stateless JWTs: the token carries its own expiry and the server rejects it once exp passes, with no grace period. Sessions are meant to be refreshed, not immortal. Anything long-lived (a pipeline, an agent loop) will outlive its first token unless the code handles refresh.

## Edge cases
- Clock skew between client and server can make a fresh token look expired; sync the clock before debugging further.
- The refresh token can also expire after long inactivity; then the user must sign in again, so fail loudly with that instruction.
- Service-role keys do not expire the same way; if server-side code uses one, expiry handling belongs on the user-session path only.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_oRNvWch1CjeQHcB1Fn-phw
