Skip to main content

Fix Frappe API Token After Restart

Background

The CTMS platform uses KrakenD as its API gateway. Every request KrakenD forwards to Frappe is authenticated using a pre-shared API token (FRAPPE_API_TOKEN) stored in .env.production. The token is a <api_key>:<api_secret> pair tied to the Frappe Administrator user.

When the platform is first deployed, zynctl.sh deploy automatically extracts this token from Frappe and injects it into both the env file and the KrakenD container.

How the token flows

.env.production  →  docker compose (env var)  →  KrakenD entrypoint.sh (envsubst)
→ krakend.json (Lua pre-scripts)
→ Authorization: token <key>:<secret>
→ Frappe backend (port 8080)

Problem

After restarting the Frappe Docker stack (docker compose restart or docker compose up -d in the frappe-marley-health/ directory), all API calls through KrakenD suddenly return 401 AuthenticationError — even though nothing was changed manually.

Root Cause

Frappe's setup init container (frappe-marley-health-setup-1) can re-run on restart and silently call generate_keys for the Administrator user. This:

  • ✅ Keeps the same api_key (so it looks unchanged in tabUser)
  • Regenerates the api_secret in the database
  • ❌ Does not update .env.production or the running KrakenD container

The result: KrakenD keeps sending the old secret, while Frappe expects the new one → 401.

Symptoms

SymptomWhere
401 AuthenticationError on all /api/v1/* endpointsKrakenD gateway (port 9080)
{"exc_type": "AuthenticationError"} in response bodyKrakenD → Frappe
Direct curl to Frappe with the .env.production token returns 401Frappe backend (port 8080)
Frappe admin panel and site UI still work fineBrowser (session-based auth, unaffected)

Solution

SSH into the server and run:

cd /opt/ctms-deployment
./zynctl.sh refresh-token

That's it. The command will:

  1. Test the current token from .env.production against Frappe
  2. Regenerate the token if invalid (using the built-in helper script)
  3. Patch .env.production with the new token
  4. Recreate the KrakenD API gateway container (force-recreate, not restart)
  5. Verify both direct Frappe auth and the KrakenD gateway
Expected output
═══════════════════════════════════════════════════════════════
Frappe API Token Refresh
═══════════════════════════════════════════════════════════════

▸ Step 1: Testing current token from .env.production...
⚠️ Token is invalid (HTTP 401). Refreshing...
▸ Step 2: Regenerating token via frappe-generate-token.py...
New token: 7b5ff5542d0120e:...
▸ Step 3: Patching .env.production...
✅ .env.production updated
▸ Step 4: Recreating API gateway (force-recreate)...
▸ Step 5: Verifying...
Direct Frappe auth: HTTP 200 ✅
KrakenD gateway: HTTP 200 ✅
Container token: 7b5ff5542d0120e:... ✅ matches

✅ All fixed! API gateway is operational.

Get the Current Token

After a refresh (or fresh deployment), extract the full token from the env file:

TOKEN=$(grep "^FRAPPE_API_TOKEN=" /opt/ctms-deployment/.env.production | cut -d= -f2)
echo "Token: ${TOKEN}"

The output should be a key:secret pair (e.g. 7b5ff5542d0120e:a1b2c3d4e5f6). If empty, re-run ./zynctl.sh refresh-token.


Manual Verification

If you want to confirm things are working without running the full refresh:

cd /opt/ctms-deployment

# Read current token
FRAPPE_TOKEN=$(grep "^FRAPPE_API_TOKEN=" .env.production | head -1 | cut -d= -f2-)

# Test direct Frappe auth
curl -s -w "\nHTTP: %{http_code}" \
-H "Authorization: token ${FRAPPE_TOKEN}" \
"http://127.0.0.1:8080/api/method/frappe.auth.get_logged_user"

# Test via KrakenD gateway (port 9080)
curl -s -w "\nHTTP: %{http_code}" \
"http://127.0.0.1:9080/api/v1/doctype/Study?limit_page_length=1&fields=[\"name\"]"
  • HTTP 200 → Token is fine, no action needed.
  • HTTP 401 → Run ./zynctl.sh refresh-token.

What the Command Does

The refresh-token command in zynctl.sh:

  1. Tests the current token from .env.production against Frappe's auth endpoint
  2. Exits early if the token is still valid (safe to run anytime)
  3. Regenerates the token using the built-in frappe-generate-token.py helper script (already mounted inside the backend container)
  4. Patches .env.production (and .env) with the new api_key:api_secret
  5. Force-recreates the KrakenD API gateway container so it picks up the new token
  6. Verifies both direct Frappe auth (port 8080) and the KrakenD gateway (port 9080)
Why --force-recreate and not restart?

docker compose restart reuses the existing container with stale environment variables baked in at creation time. The token refresh requires docker compose up -d --force-recreate which destroys and rebuilds the container, injecting the updated env vars. The refresh-token command handles this automatically.


Affected Components

ComponentRoleImpact
frappe-marley-health-setup-1Init container that runs Frappe wizardRegenerates api_secret on restart
.env.productionStores FRAPPE_API_TOKENBecomes stale after secret rotation
ctms-api-gateway (KrakenD)Injects token via Lua pre-scriptsSends stale token → 401
frappe-marley-health-backend-1Frappe backend APIRejects stale token