Token Health — keep integrations connected
Diagnose first (no login, source of truth)
The dashboard is gated to Google-SSO CEO accounts, so don't try to log in to check
connections — read the DB directly (authorised, read-only — see memory
prod-connection-health-read-authorised). Supabase MCP, project znyjoyjsvjotlzjppzal:
select u.email, pc.platform, pc.is_active, pc.profile_name, pc.expires_at,
(pc.expires_at is not null and pc.expires_at < now()) as token_expired,
pc.last_sync
from platform_connections pc
join users u on u.id = pc.user_id
where pc.is_active = true and pc.deleted_at is null
order by u.email, pc.platform;
Never select access_token / refresh_token or any secret. token_expired = true
on an is_active = true row is the signature of a broken integration (connected, but
every API call uses a dead token → "not connected / no data").
How tokens are kept alive (the system)
| Cron | Cadence | Covers |
|---|---|---|
refresh-tokens |
every 6h | the 9 social platforms in lib/social (SUPPORTED_PLATFORMS) |
google-token-refresh |
every 30 min | Google data platforms — googleanalytics, searchconsole, googlebusiness, googledrive (via lib/google/google-auth getOAuthAccessToken) |
dr-gbp-oauth-refresh |
every 50 min | the single DR GBP env-var bearer (not DB rows) |
token-health |
daily 07:00 | monitor — reports active-but-expired connections + alerts the owner |
Refresh keeps tokens valid before they expire (Google access tokens last ~1h). The monitor is the safety net: it reports anything expired-but-active and warns ~7 days ahead for connections that can't self-heal (no refresh token), so a periodic-reconnect platform never dies as a surprise.
A token only stops needing manual reconnects two ways
- It auto-refreshes — needs a stored refresh token. The refresh crons keep it alive.
- It never expires — e.g. a Facebook long-lived page token (
expires_atNULL).
If neither is true, it WILL need periodic manual reconnection. The monitor's job is to make that never a surprise.
Permanent fixes (per platform)
- Facebook page tokens → make them never-expire. Page tokens derived from a
long-lived user token (
getLongLivedToken→getPagesinlib/oauth/providers/meta.ts) don't expire. A page connection with a 60-dayexpires_atwas created without that exchange — reconnect it through the long-lived flow and it becomes permanent. - LinkedIn → needs refresh tokens, which LinkedIn only issues to apps with the right product entitlement in the LinkedIn Developer portal (Sign In with LinkedIn / Marketing Developer Platform). The code already captures + refreshes a refresh token if LinkedIn returns one — the lever is the app config, not the code. If the entitlement isn't available, LinkedIn = 60-day tokens + proactive reconnect (the monitor warns ahead).
Reconnect runbook (can't self-heal)
A connection with no working refresh token can't be auto-refreshed — the refresh
cron disables it (isActive=false) and notifies the user (requires_reauth). These
need a human:
- LinkedIn — reconnect in Platforms → LinkedIn → Reconnect (until refresh-token entitlement is enabled, repeat ~every 60 days when the monitor warns).
- Facebook page tokens — reconnect through the long-lived flow so it never expires again.
- After reconnecting, re-run the diagnose query — the row's
expires_atshould be in the future (or NULL) andtoken_expired = false.
Run the monitor manually
# trigger the monitor (returns expiredActive + byPlatform)
curl -s "$BASE/api/cron/token-health" -H "Authorization: Bearer $CRON_SECRET"
Maintenance
- New Google data platform → add it to
GOOGLE_DATA_PLATFORMSinapp/api/cron/google-token-refresh/route.ts. - The monitor alerts the emails in
OWNER_EMAILS. - Root cause + history: memory
token-expiry-root-cause. Tracked: Linear SYN-1013.