Loupe
Documentation

Technical

Activation envelope wire format.

The activation envelope is the wire-format contract between useloupe.tools and Loupe.app. The website signs it; the desktop app verifies the signature against an embedded public key, then unlocks the tier the envelope claims. No network calls from the app — the bytes carry their own trust.

Wire format

A single base64url string carrying the signature concatenated with the JSON payload.

wire = base64url( signature_64bytes || payload_json_bytes )

The website hands the wire blob to Loupe.app via a custom URL scheme:

loupe://activate?envelope=<wire>

macOS routes loupe://URLs to the registered Loupe.app, which handles the URL on launch and validates the envelope before showing any “activated” UI.

Payload schema

The signed payload is canonical JSON — keys in declaration order, UTF-8, no insignificant whitespace. The verifier signs the exact bytes it received, so reordering keys would invalidate the signature in transit.

{
  "v": 1,
  "tier": "single",
  "seatId": "",
  "orgId": "",
  "userId": "u_8a4c7f12b938ef38",
  "issuedAt": "2026-04-27T14:11:08Z",
  "expiresAt": null,
  "envelopeId": "9f1b4e7c-2a83-4c91-bd56-7e02af19c3d4",
  "productVersion": 1
}
v
1
Schema version. The verifier rejects any value other than 1.
tier
enum
"single" | "team" | "site" | "trial" | "upgrade-single" | "upgrade-team" | "upgrade-site". Determines which features Loupe.app unlocks after verification.
seatId
string
Per-seat identifier within an org (Team/Site tiers). Empty string for Single and Trial.
orgId
string
Org identifier for Team/Site tiers. Empty string for Single and Trial.
userId
string
OAuth-subject-derived stable user ID from useloupe.tools. Survives email changes.
issuedAt
ISO-8601 UTC
When useloupe.tools signed the envelope. Verifier rejects if older than 24h or more than 5 minutes in the future (clock-skew tolerance).
expiresAt
ISO-8601 UTC | null
When the activation expires. null = perpetual (paid tiers). Trial envelopes set this 14 days out.
envelopeId
UUID v4
Unique identifier per envelope. Loupe.app may use it to detect replay (rare; mostly relevant in audit forensics).
productVersion
1
Loupe major-version line this envelope unlocks. v1 envelopes do not unlock v2 — rebuying the major upgrade re-issues with productVersion=2.

Signature scheme

Algorithm
Ed25519 (RFC 8032)
Key size
32-byte private, 32-byte public
Signature size
64 bytes (fixed)
Hash
SHA-512 (built into Ed25519, not separate)
Encoding
base64url, no padding, RFC 4648 §5
Public key embed
Compiled into Loupe.app as a constant in EnvelopeVerifier.swift

The private key lives only in useloupe.tools' signing environment as the ED25519_SIGNING_KEY_B64 environment variable. Rotating it requires generating a new keypair, embedding the new public key in Loupe.app, and shipping a new notarized DMG — by design, a deliberate cycle, not a routine.

Verification flow (Loupe.app side)

Loupe.app receives the URL, extracts the envelope blob, and performs five checks in order. Any failing check rejects the envelope without partial state.

  1. 1Decode base64url→ bytes. Reject if total length > 8 KB.
  2. 2Split into signature[0..64] + payload[64..].
  3. 3Verify the Ed25519 signature against the embedded public key. Reject on signature failure.
  4. 4Parse the payload as JSON. Reject if v ≠ 1 or any required field is missing.
  5. 5Check freshness — reject if issuedAtis > 24h old or > 5 minutes in the future.

All five passing — Loupe.app stores the verified envelope in the user's Keychain (under studio.shyguy.loupe) and transitions to the activated state.

Reject cases

If you see Loupe.app refuse an activation, the failure is one of these five.

  • Signature invalid

    The 64-byte Ed25519 signature does not validate against the public key embedded in Loupe.app. Cause: forgery, transit corruption, or wrong-key envelope.

  • Schema version mismatch

    payload.v ≠ 1. Cause: malformed envelope, or a v2-line envelope offered to a v1 build.

  • Stale envelope

    issuedAt is more than 24h ago. Bound to limit the activation-link replay window — the issued blob is good only for first-handoff.

  • Future-dated envelope

    issuedAt is more than 5 minutes ahead of wall-clock. Tolerates legitimate clock skew while rejecting blatant tampering.

  • Oversize blob

    Total wire blob exceeds 8 KB. Defense-in-depth against pathological payloads. Real envelopes are ~600 bytes.

Verifying an envelope yourself

For security review or audit purposes, you can verify any envelope with a few lines of code. The public key is embedded in Loupe.app's EnvelopeVerifier.swift and is reproduced in the bundle's license tab so you can cross-reference.

# Node.js
import { ed25519 } from "@noble/curves/ed25519";

const wire = Buffer.from(blob_base64url, "base64url");
const sig = wire.subarray(0, 64);
const payload = wire.subarray(64);
const publicKey = Buffer.from(LOUPE_PUBLIC_KEY_BASE64, "base64");

const ok = ed25519.verify(sig, payload, publicKey);
console.log(ok ? "VALID" : "INVALID", JSON.parse(payload.toString("utf8")));