The Problem
Keystatic CMS fails GitHub OAuth authentication on Cloudflare Workers with 401 errors on /api/keystatic/github/refresh-token. The OAuth login flow appears to work, but token exchange fails silently.
This happens because Keystatic sends redirect_uri during the initial authorization request to GitHub, but omits it during token exchange. GitHub’s OAuth requires that if you send redirect_uri in the authorization step, you must also send the identical value during token exchange.
Why This Happens
GitHub’s OAuth flow has two steps:
- Authorization: User authorizes your app → GitHub redirects back with a code
- Token exchange: Your app exchanges the code for an access token
If you include redirect_uri in step 1, GitHub requires the exact same redirect_uri in step 2. Keystatic includes it in step 1 but forgets it in step 2, causing GitHub to reject the request with a 401.
Solution: Patch Keystatic’s OAuth Handler
Create a postinstall script that patches @keystatic/core to include redirect_uri in token exchange requests.
Step 1: Create the Patch Script
Save this to scripts/patch-keystatic.cjs:
|
|
Important: Replace https://your-site.com with your actual production URL.
Step 2: Configure Package Scripts
Add to your package.json:
|
|
This ensures the patch runs after every npm install and before each build.
Step 3: Set Environment Variables
Don’t put secrets in wrangler.jsonc or wrangler.toml. Set these in the Cloudflare dashboard (Settings → Environment Variables):
KEYSTATIC_GITHUB_CLIENT_ID- Your GitHub App client IDKEYSTATIC_GITHUB_CLIENT_SECRET- Your GitHub App client secretPUBLIC_KEYSTATIC_GITHUB_APP_SLUG- Your GitHub App slugKEYSTATIC_SECRET- Random secret for session encryption
Step 4: Create and Configure GitHub App
Following the approach from m4rrc0’s keystatic-deploy-test with adjustments for Cloudflare:
-
Go to GitHub Settings → Developer settings → GitHub Apps → New GitHub App
-
Basic Information:
- GitHub App name: Choose a unique name (e.g.,
your-site-keystatic-cms) - Homepage URL:
https://your-site.com/(use your root domain, not/keystatic) - Callback URL:
https://your-site.com/api/keystatic/github/oauth/callback
- GitHub App name: Choose a unique name (e.g.,
-
Webhook:
- Uncheck “Active” (Keystatic doesn’t need webhooks)
-
Permissions:
- Repository permissions → Contents: Read and write access
- Repository permissions → Metadata: Read-only (automatically selected)
-
User permissions:
- Leave all unselected
-
Where can this GitHub App be installed?
- Select “Only on this account”
-
Create the GitHub App
-
After creation:
- Copy the Client ID (starts with
Iv) - Click “Generate a new client secret” and copy it immediately
- The app slug will be shown in the URL (e.g.,
1ar-labs-keystatic)
- Copy the Client ID (starts with
-
Install the app:
- Go to your newly created app’s page
- Click “Install App” in the left sidebar
- Select your account
- Choose “Only select repositories” and pick the repository for your site
- Click “Install”
Step 5: Deploy
|
|
What About Just Recreating the GitHub App?
In my case, recreating the GitHub App from scratch fixed the issue even without the patch. This suggests the original app had some corrupted OAuth configuration. However, the patch fixes a real bug in Keystatic and ensures consistent redirect_uri handling, so it’s worth applying regardless.
Debugging Tips
If OAuth still fails after applying the fix:
- Check environment variables: Verify all four variables are set in Cloudflare dashboard, not in config files
- Verify callback URL: Must match exactly between GitHub App settings and your production URL
- Check GitHub App installation: Make sure the app is installed on the correct repository
- Clear cache:
rm -rf node_modulesand reinstall to ensure patch applies - Inspect network logs: Look for 401 errors on
/api/keystatic/github/refresh-tokenor/oauth/callback
Why This Matters
Keystatic is a solid CMS for Astro/Next.js projects, but its OAuth implementation has this quirk that breaks on serverless platforms like Cloudflare Workers. This patch ensures reliable authentication without modifying your application code or switching deployment platforms.