Rotate everything (no downtime)
How to swap every secret in your Synq integration — client_secret,
BYO OAuth credentials, webhook secrets, API keys, SESSION_SECRET,
JWKS signing keys — without dropping a single sign-in, webhook
delivery, or background job.
The whole-page idea: for every secret, register the new one before retiring the old one. The window where both are valid is when you flip clients. After clients are flipped, the old secret is revoked.
Rotation cadence we recommend
| Secret | Cadence | Why |
|---|---|---|
OIDC client_secret | every 90 days | Lowers the blast radius of a leak through any vector — CI logs, env-var screenshots, ex-employee laptops. |
| BYO OAuth client secrets | every 90 days | Google, Apple, Microsoft, Discord all support rotation; doing it on a schedule normalizes the workflow. |
| Webhook signing secret | every 180 days | Lower frequency because rotation requires a coordinated change in your webhook receiver. |
| API keys (user-issued) | per-revocation | Users rotate their own; you don’t have to. |
SESSION_SECRET | every 365 days or any team-member departure | A rotated session secret invalidates all existing sessions — users re-login. Pick a quiet window. |
| Synq JWKS signing keys | Synq rotates these automatically | You don’t manage these; keep your JWKS cache TTL ≤ 24 hours. |
OIDC client_secret
The dashboard supports a second active secret per App. Use it to roll without downtime.
- Dashboard → App → Credentials → “Add secondary secret”. A new secret is generated. Both secrets are now valid for authenticating to the token endpoint.
- Update your deployment env var (
SYNQ_CLIENT_SECRET) to the new secret. Wait for the deployment to finish rolling. - Verify: sign in to your app, confirm it works. The OIDC callback exchanges the auth code using the new secret.
- Dashboard → “Revoke primary secret”. The old secret is immediately rejected by Synq’s token endpoint. Any old deployment pinned to the old secret will fail to exchange new auth codes — but existing sessions remain valid until they expire (the secret is used only at the token endpoint, never for already- issued tokens).
Total downtime: 0. Total window where both secrets are valid: ~5 minutes to an hour, depending on your deployment cadence.
BYO OAuth credentials (Google, Apple, etc.)
Each provider has its own rotation flow. The Synq side is the same for all: register the new credential, run sign-ins through it, then remove the old.
Google OAuth 2.0 client
Google supports two client secrets per OAuth client out of the box.
- Google Cloud Console → APIs & Services → Credentials → your OAuth 2.0 Client → “Add secret”. You now have two valid secrets.
- Synq dashboard → Brand → Connections → Google → “Replace credentials” → paste new secret. Save.
- The first sign-in flow that runs uses the new credential. You can verify by triggering a Google sign-in from an incognito window; if it succeeds, the new credential is wired.
- Google Cloud Console → “Delete old secret”. The old secret is immediately invalidated by Google.
Apple Sign-In
Apple is the trickiest because the “client secret” is a JWT minted
from a .p8 key. To rotate, you swap the .p8 key:
- Apple Developer → Keys → Create a new key → enable Sign in
with Apple → assign your existing Service ID → download the
.p8file. Note the Key ID. - Synq dashboard → Brand → Connections → Apple → “Replace key”
→ upload the new
.p8, paste the new Key ID. Save. - Verify with an Apple sign-in from incognito.
- Apple Developer → “Disable old key”. Apple deactivates the old key. Existing sessions remain valid because Apple’s JWT is used only at the token endpoint.
Microsoft, Discord, Facebook, X, Telegram
Same shape. Each platform’s developer console supports either multi-secret or “regenerate” with a configurable grace window.
Webhook signing secret
Webhook signing rotation requires a coordinated change because your verifier must accept both old and new signatures during the window.
- Dashboard → Webhook → “Add secondary signing secret”. Synq
will now sign each delivery with both the primary and
secondary secret, in two
Synq-Signatureheaders. - Update your verifier to accept either signature:
import { verifyWebhookSignature } from '@synqid/js' const headers = request.headers.get('synq-signature')!.split(',') const ok = headers.some((sig) => verifyWebhookSignature({ signature: sig, payload: rawBody, secret: PRIMARY_OR_SECONDARY_SECRET, }) ) - Deploy. Confirm webhooks are being verified successfully under both secrets (look at your logs).
- Dashboard → “Promote secondary, retire primary”. Synq stops signing with the old secret.
- Update your verifier to drop the old secret. Deploy.
The whole rotation takes one deploy cycle. The window where both secrets are valid is bounded by the time between steps 2 and 5.
API keys
Two flavors:
- Personal API keys (users create their own at
/api-keys): the user rotates them when they want. Your app does not rotate these for them. - m2m API keys (server-to-server tokens you create for your
backend): treat exactly like
client_secret— Synq supports two-active rotation. Same flow.
SESSION_SECRET
This one breaks sessions on rotation. iron-session uses
SESSION_SECRET to encrypt the cookie; rotating the secret means
existing cookies cannot be decrypted.
Plan it for a low-traffic window. Once a year is plenty unless a team member with secret access leaves the company — then rotate immediately.
- Choose a window with the lowest traffic. (For a B2B SaaS, Saturday 2 AM your timezone is fine; for a consumer app it doesn’t matter.)
- Generate a new secret:
openssl rand -base64 32. - Update the env var. Deploy.
- All existing sessions immediately invalidate. Users see a “signed out” UI on next request and sign in again.
There is no two-secret iron-session pattern that avoids this. If
zero re-logins is a hard requirement, consider storing the session
in your own database (custom storage: option on @synqid/js)
where you can dual-key the encryption. Most teams don’t bother.
Synq JWKS signing keys
You don’t manage these. Synq rotates the keys that sign access tokens and ID tokens on a regular schedule, publishing the new key to the JWKS endpoint at least 24 hours before issuing tokens signed with it. As long as your JWKS cache TTL is ≤ 24 hours, your verifier finds the new key before any tokens signed with it arrive.
What you must do:
- Ensure your token verifier honors the
Cache-Controlheader on the JWKS response (openid-clientdoes this). - Do not pin a specific
kidin your verifier config. - Do not cache the JWKS forever (“set and forget”).
If you wrote your own verifier and pinned kid, the next rotation
will silently start failing sign-ins. The
errors reference lists what
that error looks like.
What “no downtime” actually means
The phrase “no downtime” in this guide means no user-visible failure during the rotation window, not zero invalidation of in-flight requests. Specifically:
- Existing sessions keep working through every rotation on this
page — except
SESSION_SECRET, where they don’t. - Sign-ins in progress during the rotation window succeed because the old credential is still accepted until step 4.
- Webhook deliveries in flight when the webhook secret rotates succeed because Synq signs with both during the window.
- Token verification at your backend keeps working because Synq’s signing keys rotate with a 24h overlap.
The window where you could see a failure is the moment the old secret is revoked. If your deployment is on the old secret at revocation time, the next OIDC code exchange fails until you deploy the new one. Mitigation: do step 4 only after you have visually confirmed deployments are on the new secret.
A scriptable rotation flow
The dashboard is fine for one-off rotations. For scheduled rotations, script it against the Synq Admin API:
import { SynqAdminClient } from '@synqid/js'
const synq = new SynqAdminClient({
apiKey: process.env.SYNQ_ADMIN_API_KEY!,
})
const rotation = await synq.apps.rotateSecret('app_acme_prod')
// → { secondarySecret: '...', secondaryRotateAt: '...' }
// Push to your secret manager:
await secretManager.set('SYNQ_CLIENT_SECRET', rotation.secondarySecret)
// Trigger a deploy. Wait for it to roll out.
await waitForDeployment()
// Promote the new secret as primary.
await synq.apps.promoteSecret('app_acme_prod')The above is the entire programmatic rotation. Wire it into a quarterly cron — secret management without YOLO Friday-night console clicks.
Bookmark this page. Rotation is not a thing you remember off the top of your head — it’s a thing you read step-by-step every time.