GUIDES
Identify users
Two modes: open identity for personalization, signed identity for anything private. Signed identity is what gates uploads, the vault, and per-user tasks.
Every request the widget makes carries a user context (who is asking). Agentifys can either trust it as-is, or require it to be cryptographically signed by your server. The difference decides whether a user can be personalized, or actually trusted with private data.
Open identity
In open mode you set window.ERAConfig.user in the browser and Agentifys trusts it as written. It is the fastest way to personalize replies: the agent knows the name, role, and anydata you attach. But because it lives in the browser, a determined user could edit it, so open mode never unlocks anything sensitive.
<script>
window.ERAConfig = {
user: {
id: "user_123",
name: "Sara",
role: "admin",
data: { plan: "pro" }
}
}
</script>Use open identity when the stakes are cosmetic: greeting the user by name, tailoring tone, or feeding read-only context into policies.
Signed identity
Signed mode makes the identity tamper-proof. Your server signs the user context with a secret only it knows, and Agentifys verifies that signature on every request. This is what gates the parts of the platform where trust matters:
- Private knowledge uploads (a user reading only their own documents)
- The per-user credential vault
- Per-user MCP OAuth connections
- The My-tasks panel and scheduled tasks
VITE_ variable, not in a NEXT_PUBLIC_ variable, not in any bundle a user can read.The signing recipe
Sign on your server, then hand the signed context to the browser. The signature is an HMAC-SHA256 over a canonical (stably stringified) JSON of the identity plus a fresh Unix timestamp _ts. Sorting keys matters: Agentifys re-stringifies the same way to verify, so the field order must be deterministic. The server canonicalizes with Python's json.dumps (default ensure_ascii=True), so the recipe below escapes every non-ASCII character to its \uXXXX form; without that, an Arabic or other Unicode name hashes differently and the request fails with IDENTITY_SIGNATURE_INVALID.
import { createHmac } from "crypto";
// Canonical JSON, byte-for-byte what Agentifys verifies against:
// json.dumps(ctx, sort_keys=True, separators=(",", ":")) # ensure_ascii=True by default
// Three things diverge if you hand-roll this with JSON.stringify:
// 1. every non-ASCII char must become \uXXXX (an Arabic name signs wrong otherwise),
// 2. keys sort by Unicode CODE POINT, not UTF-16 code unit (differs above the BMP),
// 3. only integers are safe (Python renders 1e-7 as 1e-07).
function enc(v) {
return JSON.stringify(v).replace(/[^ -~]/g,
c => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0"));
}
const codePoints = (s) => Array.from(s, (c) => c.codePointAt(0));
function byCodePoint(a, b) {
const A = codePoints(a), B = codePoints(b);
for (let i = 0; i < Math.min(A.length, B.length); i++) if (A[i] !== B[i]) return A[i] - B[i];
return A.length - B.length;
}
function canonicalJson(v) {
if (Array.isArray(v)) return "[" + v.map(canonicalJson).join(",") + "]";
if (v && typeof v === "object") {
return "{" + Object.keys(v).sort(byCodePoint)
.map((k) => enc(k) + ":" + canonicalJson(v[k])).join(",") + "}";
}
if (typeof v === "number" && !Number.isInteger(v)) {
throw new Error("Sign integers only: " + v + " formats differently in Python. Send it as a string.");
}
return enc(v);
}
// Add _ts (unix seconds), sign the context WITHOUT _sig, then send { ...identity, _ts, _sig }.
// Valid for 5 minutes, and re-checked on every /v1/chat call.
function signUserContext(identity, secret) {
const ctx = { ...identity, _ts: Math.floor(Date.now() / 1000) };
ctx._sig = createHmac("sha256", secret).update(canonicalJson(ctx)).digest("hex");
return ctx;
}Return that object to your page and drop it straight into ERAConfig.user. The two extra fields, _ts and _sig, are what Agentifys checks.
// Server-side (e.g. an endpoint your page calls on load)
const signed = signUserContext(
{ id: "user_123", name: "Sara", role: "admin", data: { plan: "pro" } },
process.env.ERA_SIGNING_SECRET
)
res.json(signed)
// Browser: use the signed context verbatim
window.ERAConfig = { user: signed }The 5-minute window
A signature is only valid for 300 seconds after its _ts. This keeps a captured context from being replayed forever. For a long-lived session, re-sign periodically (a fresh _ts and _sig) rather than reusing one signature all day. A short-lived endpoint that signs on each page load is usually enough.
IDENTITY_SIGNATURE_EXPIRED. A missing or wrong signature returns IDENTITY_SIGNATURE_REQUIRED or IDENTITY_SIGNATURE_INVALID. Check the clock skew on your server first, then confirm you are stringifying keys in sorted order.IDENTITY_NOT_CONFIGURED means signed identity is switched on but no signing secret has been generated yet, in Settings, then Identity. SIGNED_IDENTITY_REQUIRED means the opposite: you called a signed-only endpoint (the vault, private uploads, per-user OAuth) while the project is still in open mode.