Developers › Apps & OAuth

Build on LeadLogiX identity

Developer Reference Build on LeadLogiX identity If your product serves LeadLogiX customers and nobody else, you do not need an accounts system. LeadLogiX already knows who the person is, which workspace they belong to and what they may do there — and re-derives all three on every request rather than trusting a snapshot. Consume that instead of maintaining a second copy that drifts.

What you can stop building

What you can stop building What you would normally build What replaces it A users table, password reset, MFA The authorization code flow. sub is your user key. An organisations table and a membership join workspace.id from UserInfo. It is your tenant key. A roles model and an admin screen to manage it role and role_rank . The customer manages membership in LeadLogiX. Invitations and seat management The customer invites in LeadLogiX; a new member can sign in to your product immediately. Deprovisioning when someone leaves Their next call fails — see Staying current below. What this does not remove You still own your own data, and it still needs a key. Store it against (workspace.id, sub) — never against an email address, which the owner can change, and never against the workspace NAME. The flow itself is on Sign in with LeadLogiX ; this page is what to build around it.

Sessions

Sessions Two designs, and the trade-off between them. Derive per request Short local session How it works Call UserInfo (or the MCP get_workspace tool) on every request that needs authority. After the code exchange, mint your own cookie holding sub, workspace id, role and a short expiry. Freshness Exact. A demotion takes effect on the next request. Up to the cookie's lifetime stale. Keep it to minutes, not days. Cost A network call per request. None after sign-in. Use it when The decision is consequential — money, deletion, cross-workspace reads. The decision is ordinary page rendering. Most products end up with both: a short cookie for rendering, and a live re-derivation before anything that matters. What neither should do is store a role at sign-up and trust it a year later — that is the copy that drifts. Your session must not outlive the grant A local session is derived state. If the customer disconnects your application, LeadLogiX stops honouring the token but your cookie knows nothing about it. Keep the grant's refresh token with the session and destroy the session the moment a call returns grant_revoked .

Authorization decisions

Authorization decisions Map the role once, at your boundary. // Your capabilities, expressed once, in terms of LeadLogiX's roles. const CAN = { "view_dashboard": ["admin", "procurement", "agent"], "export_data": ["admin", "procurement"], "change_settings": ["admin"], }; // An unlisted role gets nothing. That is the correct default for a value you do // not recognise — deny, do not infer from role_rank. export const can = (claims, capability) => CAN[capability].includes(claims.role); // ⚠ Compare the ROLE, not the rank, when the distinction matters. // procurement and agent are both rank 10 and neither manages the other, so // `role_rank >= 10` admits both — fine when that is what you meant, and a // silent privilege grant when you meant procurement. # Your capabilities, expressed once, in terms of LeadLogiX's roles. CAN = { "view_dashboard": {"admin", "procurement", "agent"}, "export_data": {"admin", "procurement"}, "change_settings": {"admin"}, } def can(claims: dict, capability: str) -> bool: # An unlisted role gets nothing. That is the correct default for a value you # do not recognise — deny, do not infer from role_rank. return claims.get("role") in CAN[capability] # ⚠ Compare the ROLE, not the rank, when the distinction matters: # procurement and agent are both rank 10 and neither manages the other. Do not invent a second role model on top. A per-user override table in your product is a permission LeadLogiX's administrator cannot see, cannot revoke, and will not remove when they remove the person. Treat any role you do not recognise as your least privileged tier.

Staying current

Staying current Three signals, and none of them is a poll. What happened How you find out What to do The customer disconnected your application Any call → grant_revoked Destroy the session. Re-authorize only if the person asks. Their role changed, they were suspended, deleted, their password was reset, or the workspace was suspended A back-channel logout token POSTed to your registered backchannel_logout_uri End the local session for that sub. The next sign-in picks up the new authority. The workspace is suspended or being deleted Calls → tenant_inoperable Stop calling. Do not re-authorize — it will not help. Register a backchannel_logout_uri (https, accepting an unauthenticated POST from LeadLogiX) to receive the second row. Without it you learn about a role change at your next token refresh, which is long enough to matter for a demotion. The logout token is a JWT signed with the same keys as the ID token: check iss , aud , the events claim, and that it carries sub or sid — then end only that session, never the whole workspace.

Provisioning on first sign-in

Provisioning on first sign-in The callback carries the workspace, the subject and the role. const { me } = await handleCallback(session, query); // The workspace you have never seen before IS your signup event. There is no // separate onboarding API to call, and no webhook to wait for. let org = await db.orgs.findByWorkspaceId(me.workspace.id); if (!org) { org = await db.orgs.create({ workspaceId: me.workspace.id, // the key. immutable, server-generated. displayName: me.workspace.name, // a label. see the warning below. createdBy: me.sub, }); } // The first person from a workspace is not necessarily its administrator — // read the role, do not assume it. await db.members.upsert({ orgId: org.id, sub: me.sub, role: me.role }); me = handle_callback(session, query)["me"] # The workspace you have never seen before IS your signup event. There is no # separate onboarding API to call, and no webhook to wait for. org = db.orgs.find_by_workspace_id(me["workspace"]["id"]) if org is None: org = db.orgs.create( workspace_id=me["workspace"]["id"], # the key. immutable, server-generated. display_name=me["workspace"]["name"], # a label. see the warning below. created_by=me["sub"], ) # The first person from a workspace is not necessarily its administrator — # read the role, do not assume it. db.members.upsert(org_id=org.id, sub=me["sub"], role=me["role"]) UserInfo { "sub": "6nQ2…", "email": "dana@northwind.example", "email_verified": true, "name": "Dana Fry", "workspace": { "id": "t_78d6b253d55b6c32", "name": "Northwind Roofing", "name_verified": false, "is_sub_workspace": false, "parent_id": null }, "role": "procurement", "role_rank": 10 } A workspace name is not an identity workspace.name is typed by whoever registered the workspace and is checked by nobody unless name_verified is true. Display it if you like; never use it to decide who someone is, to match against your own customer records, or to grant anything. The id is the identity. is_sub_workspace and parent_id are always present. If you bill or report per organisation, decide early whether a sub-workspace is its own customer or part of its parent — parent_id is the field to key that decision on.

What you still have to get right

What you still have to get right Concern Why it is still yours Storing the refresh token A long-lived credential for one person in one workspace. Encrypt it at rest and scope access to the service that refreshes. Your own audit trail LeadLogiX records what happened in LeadLogiX. What happened in your product is yours to record, and sub is the actor id to record it under. Rate limits and retries Your traffic is your own — see OAuth & MCP errors → Rate limits . Data you derive Anything you compute from a customer's leads is yours to delete when they disconnect. Nothing revokes it for you.