Payload signing
Every payload FortressFlag sends to one of your apps carries a signature, and every SDK checks it before a single flag value changes. You don’t have to do anything to get this — the SDKs ship with the production public key built in and verify by default. This page explains what that buys you, what the two policy settings mean, and which keys your apps trust today.
What’s signed, and why
Section titled “What’s signed, and why”When your app polls, FortressFlag answers with two things: the payload (your device’s flag values on the client plane, the full ruleset on the server plane) and a signature over the exact bytes of that payload. The signature is made with a private key only FortressFlag holds; your SDK checks it with the matching public key it shipped with.
If the bytes were changed anywhere between our servers and your app — a proxy, a CDN, a cache file someone edited on the device — the check fails and the SDK ignores the response. It keeps serving the last values it verified (see the fallback promise) rather than trusting bytes it can’t vouch for.
Two things the signature does not do, so nobody over-relies on it:
- It doesn’t hide anything. The payload isn’t encrypted — a client key is public by design, and so is everything served against it. Don’t put secrets or your users’ personal data in rules.
- It doesn’t replace TLS. The SDKs talk to FortressFlag over HTTPS as well; the signature is the second lock, for the parts of the path TLS doesn’t cover (caches, on-device storage).
The two policies
Section titled “The two policies”Every SDK has a signature policy with two settings:
required— the default. The SDK verifies every payload against its trusted keys and rejects anything unsigned, signed with a key it doesn’t know, or signed over different bytes. This is what you want in every build that talks to FortressFlag.disabled— accept unsigned payloads. It exists for one situation: running against a local FortressFlag backend that has no signing key configured. It’s a named, greppable setting rather than a silent fallback, so “why isn’t this verifying?” always has an answer in your own source. Never ship a production build with it.
There’s no “warn” mode in between. A payload either verifies or it’s ignored.
The keys your apps trust
Section titled “The keys your apps trust”FortressFlag signs with one key per environment, and the SDKs ship trusting the production
key only. If you test against staging (fortressflag.net), add the staging key to your SDK’s
trusted keys explicitly.
| Environment | Key id | Public key (base64url) |
|---|---|---|
Production (fortressflag.com) |
prod-2026-09-k1 |
EaEF8MHNu3onHxemTg3-OcrKrq7ODsZIVEp-IVV2ojg |
Staging (fortressflag.net) |
staging-2026-09-k1 |
RHFhzt8i3hQ4NKv_IrB92YuXmyNfDXx8eWN8cOrEBc8 |
The same keys as raw bytes, in hex, if that’s what your configuration takes:
prod-2026-09-k1 11a105f0c1cdbb7a271f17a64e0dfe39cacaaeaece0ec648544a7e215576a238staging-2026-09-k1 447161cedf22de143834abff22b07dd98b979b235f0d7c7c79637c70eac405cfEach key is a 32-byte Ed25519 public key. The key id is the middle part of every signature
(ed25519:prod-2026-09-k1:…), which is how the SDK knows which key to check with. These values
are public — you can paste them into a config file or a repository without a second thought.
Rotation
Section titled “Rotation”When FortressFlag rotates a key, the new public key ships in an SDK release first, and we only start signing with it once the SDKs in the wild trust it. Rotation never requires you to release an app on a deadline: an SDK that trusts both the old and the new key keeps verifying throughout.
If you’re on the web
Section titled “If you’re on the web”The web SDK verifies with your browser’s built-in Web Crypto. That needs Ed25519 support —
Safari 17+, Chrome 137+, Firefox 130+ — and a secure context: https:// pages and
http://localhost. On a plain http:// page served from, say, a LAN address, browsers don’t
expose Web Crypto at all.
Where the browser can’t verify, the SDK doesn’t pretend it did and doesn’t throw a good cache away either. It reports the payload as unverifiable, keeps serving its last verified values or your code’s defaults, and logs the reason once. Serve your app over HTTPS and this never comes up.
Where to next
Section titled “Where to next”- How flags reach your app — the two planes and the fallback promise underneath both
- Keys — why
ffc_is public andffs_is a secret