Skip to Content
We are live but in Staging 🎉
RecipesSign-in for Ignite Apps

Sign-in for Ignite apps

If your app runs on Ignite , this is the integration to use. Each person on your customer’s team gets their own account in a pool and signs in as themselves. The Ignite gateway handles the whole login (PKCE, the session cookie, refresh, JWT verification) and passes your app an identity it has already verified. Your app contains no login code and no JWT library.

This is not the Dodil connection you use to build. dodil auth login, the CLI, and the MCP server  sign you, the builder, into your Dodil org. Your app’s users never touch any of that. They live in an AppID pool, a separate identity plane (see Core Concepts). A pool user can’t reach your buckets or apps directly, and nobody shares a login.

1. Create the pool and its people

dodil appid pool create acme --display-name "Acme" --with-local # role -> permission catalog (roles set REPLACES the catalog: get, merge, set) dodil appid roles set acme rep=deals:read,deals:write manager=deals:read,deals:write,deals:approve # a business system: no self-signup, invite each person dodil appid settings set acme --allow-signup=false dodil appid user create acme [email protected] --roles rep # no --password: they get an invite/reset email

Want Sign in with Google (or Microsoft, or another OpenID Connect provider) as well? Add the connection; see CLI → connection. The hosted page then shows a “Continue with …” button.

With allow_signup off, nobody can create their own account. Invite at least one person, or the login screen will work and still let nobody in.

2. Attach the pool to the app

In .dodil/deploy.yaml:

apps: - name: acme-app user_pool: acme # "pool" (same org) or "org/pool"; "" detaches; absent = no change public_invoke: false # false = sign-in required (applied only when the app is first created)

public_invoke in deploy.yaml takes effect only when the app is created. To change it later, use dodil ignite app update acme-app --allow-unauthenticated=<true|false>. The pool must already exist.

Or on an existing app:

dodil ignite app update acme-app --user-pool <org>/acme

user_pool and public_invoke (--allow-unauthenticated) together decide who gets in:

user_poolpublic_invokeWho can call the app
setfalseSign-in required. Anonymous requests get 401 plus a login_url. Platform principals (your service accounts, CI, dodil ignite invoke) still work.
settrueOptional sign-in. Anonymous requests pass. Identity is injected when a session or pool token is present, and the app decides. Required for an app with an /mcp endpoint (see below); the app’s own gate then refuses identity-less requests except /healthz and its discovery metadata.
unsetfalsePlatform principals only.
unsettrueFully public, no identity.

The pool adds sign-in. It never removes platform access.

3. Allowlist the callback

The gateway signs people in with the redirect URI https://<public_url>/.dodil/auth/callback. A pool with no redirect_uris signs nobody in.

dodil ignite app get acme-app -o json # copy the host from public_urls; never build it by hand dodil appid settings get acme -o json # settings set REPLACES the object: read first dodil appid settings set acme --allow-signup=false \ --redirect-uris https://<public_url>/.dodil/auth/callback

Registering the callback is a manual step today. Several apps on one domain can share one entry with a wildcard label, e.g. https://*.apps.example.com/.dodil/auth/callback (one label only; see Redirect URIs).

Copy the host from public_urls, because it escapes some characters (an org named acme-co appears as acme--co). If the URI is missing, the person signing in sees “redirect_uri is not in the pool’s allowlist”.

4. Read the identity in your handler

On every request that passes the check, the gateway strips any inbound copy of these headers and then sets them itself:

HeaderContents
X-Dodil-UserJSON: sub, email, connection, app_roles
X-Dodil-User-JwtThe pool access token the gateway verified. The expanded permissions claim (and tenant, when pinned) is only here. Decode it, or verify it again against the pool JWKS if you prefer.
X-Dodil-Auth-Sourcepool (a person from the pool) or platform (a service account or other platform principal)
import base64, json from fastapi import Request, HTTPException def claims(request: Request) -> dict: """The signed-in person. Already verified by the gateway; do not verify again.""" if request.headers.get("X-Dodil-Auth-Source") != "pool": raise HTTPException(401, "sign-in required") body = request.headers["X-Dodil-User-Jwt"].split(".")[1] body += "=" * (-len(body) % 4) return json.loads(base64.urlsafe_b64decode(body)) # sub, email, permissions, tenants, tenant… def require(request: Request, perm: str) -> dict: c = claims(request) if perm not in c.get("permissions", []): raise HTTPException(403, f"missing {perm}") return c

Because the gateway removes forged copies, a header sent by a client never reaches your app. A local test server does not strip them, so a test suite that sets X-Dodil-User by hand passes even when production would refuse the request. Check once on the deployed URL (see Verify).

The browser only ever holds the gateway’s HttpOnly, host-only session cookie, so no token is exposed to page JavaScript. Serve the UI and the API from one app (one host). If you split them across two hosts, the cookie stops reaching the API.

5. Verify with a real person

dodil ignite invoke and service-client tokens both bypass the browser login, so they can’t show that sign-in works. Before you call the app done:

  1. Open https://<public_url>/ in a private window. You should be sent to the pool’s sign-in page.
  2. Sign in as an invited user. You should land back in the app as that person.
  3. Sign in as a user with a different role and confirm they see only what that role allows.

If you can’t hand someone a URL they can sign in to, the app is not verified.

Other callers

  • Another app calling this one: use a pool service client, with a role that holds only what that call needs, and send its client_credentials token as Authorization: Bearer: dodil appid service-client create acme --client-id billing-sync --role sync
  • A person connecting Claude.ai or ChatGPT to the app’s /mcp: see the next section.
  • Apps not hosted on Ignite: there is no gateway in front of them, so verify the pool token yourself. See Verify tokens in your backend.

A person connecting Claude.ai or ChatGPT

Each person can connect their own assistant to the app’s /mcp and sign in as themselves (authorization code + PKCE against the pool), so their agent sees exactly what they see. Nobody shares a token.

WhoDoes what
The appServes GET /.well-known/oauth-protected-resource/mcp (RFC 9728 ) naming the pool issuer https://appid.dodil.io/<org>/<pool> as its authorization server. Answers an unauthenticated /mcp with 401 and WWW-Authenticate: Bearer resource_metadata="<base>/.well-known/oauth-protected-resource/mcp". Gates every other route on X-Dodil-User.
The Ignite gatewayVerifies the pool bearer token the assistant presents and injects X-Dodil-User / X-Dodil-User-Jwt, exactly as for a browser session. It does not serve the discovery metadata.
AppIDRuns the sign-in. Two ways in. Dynamic registration (the default): the assistant registers itself and starts out pending; until you approve it, the person trying to connect gets an error page. Approve with dodil appid client list acme, then dodil appid client approve acme <client-id>. Client metadata URL (CIMD): turn on cimd_enabled in the pool settings (off by default; see settings) and assistants identify themselves by a metadata URL and skip the approval step. Claude picks this automatically; ChatGPT uses it with private_key_jwt. Each person approves the assistant once on a consent screen and can withdraw it at {issuer}/user/consents. client suspend cuts an assistant off and revokes those consents.

The app must be user_pool + public_invoke: true. With public_invoke: false, the gateway refuses the assistant’s anonymous discovery requests before the app can answer them.

The token carries that person’s roles and permissions, and is bound to your MCP server when the assistant asks for it by resource. Verified with Claude.ai against the reference CRM (AppID 0.15.1).

You don’t have to write this from scratch. The ERP platform brick (erp-platform, bricks.py: the protected-resource metadata and the sign-in gate) implements it, and it runs in the reference CRM.

Next

The Ignite side of this (the flags, the reserved /.dodil/auth/* paths, the API fields) is in Ignite → Access & sign-in .

Sign-in tells you who the person is. Making sure they only see their data is the next step, covered in Keep each customer’s data separate.