Skip to content

Cross-App Integration

Glint supports a delegation model where other OAuth applications can access Glint's data on behalf of their users — without any separate "app registration" in Glint itself. The entire authorization chain flows through Prism.

How It Works

Glint is App A (the resource provider). Any other app is App B (the consumer). The flow relies on Prism's cross-app scope delegation:

app:<glint_client_id>:<inner_scope>

For example, if Glint's Prism client ID is prism_abc123, a read scope looks like:

app:prism_abc123:read_todos

Full Authorization Flow

Nothing is registered inside Glint. App B registers Glint's scope inside Prism's own dashboard, and users grant access via Prism's standard consent screen.


Prerequisites

  • A running Glint deployment with Prism OAuth configured
  • Your Glint deployment's Prism Client ID (shown in Settings → App Config)
  • Access to the Prism instance that Glint uses, with an App B OAuth client

Step 1 — Define Scopes in Prism (Glint Admin)

A Glint owner must first define the permission scopes that App B can request. This is done directly in Prism's dashboard — not in Glint's UI.

Log in to Prism, open Glint's app settings, go to Permissions, and add scope definitions. Only define the scopes your integrations actually need.

Available Scopes

Scope keySuggested titleWhat it allows
read_todosRead todos & commentsList sets, view todos, read comments, export set content
create_todosCreate todosCreate todos and sub-todos; import into existing sets
edit_todosEdit todo titlesRename todos (own and others', per role)
complete_todosToggle completionMark todos complete/incomplete (own and others', per role)
delete_todosDelete todosDelete todos (own and others', per role)
reorder_todosReorder todosChange todo sort order (drag-and-drop equivalent)
claim_todosClaim todosClaim or release a todo to/from yourself
manage_setsManage setsCreate, rename, delete, reorder, and configure sets (auto-renew, timezone, split-completed). Also covers bulk import-as-new-set and replace-mode imports.
commentPost commentsAdd comments to todos
delete_commentsDelete commentsDelete comments (own and others', per role)
read_settingsRead workspace settingsRead team settings (name, timezone, etc.)
manage_settingsManage workspace settingsUpdate team settings
write_todosCreate & edit todos (legacy)Legacy catch-all for create, edit, and complete — use specific scopes for new integrations

Minimum for a read-only integration: read_todos

Minimum for a todo-writing integration: read_todos, create_todos

Recommended for a full automation integration: read_todos, create_todos, edit_todos, complete_todos, delete_todos, reorder_todos, claim_todos

Importing the Scope Definitions into Prism

Instead of typing each definition by hand, paste the JSON block below into Prism's Import dialog (Glint app → Permissions → Import). Existing definitions with the same scope are updated in place.

json
[
  {
    "scope": "read_todos",
    "title": "Read todos",
    "description": "List sets, read todos, and read comments."
  },
  {
    "scope": "create_todos",
    "title": "Create todos",
    "description": "Add new todos and sub-todos."
  },
  {
    "scope": "edit_todos",
    "title": "Edit todos",
    "description": "Edit todo titles."
  },
  {
    "scope": "complete_todos",
    "title": "Complete todos",
    "description": "Toggle completion on todos."
  },
  {
    "scope": "delete_todos",
    "title": "Delete todos",
    "description": "Delete todos."
  },
  {
    "scope": "reorder_todos",
    "title": "Reorder todos",
    "description": "Change todo sort order (drag-and-drop equivalent)."
  },
  {
    "scope": "claim_todos",
    "title": "Claim todos",
    "description": "Assign or release a todo to/from yourself."
  },
  {
    "scope": "write_todos",
    "title": "Write todos (legacy)",
    "description": "Legacy catch-all covering create, edit, and complete. Prefer the more specific scopes."
  },
  {
    "scope": "manage_sets",
    "title": "Manage sets",
    "description": "Create, rename, delete, reorder, and configure todo sets (auto-renew, timezone, split-completed). Also covers bulk import/export of set contents."
  },
  {
    "scope": "comment",
    "title": "Post comments",
    "description": "Add comments to todos."
  },
  {
    "scope": "delete_comments",
    "title": "Delete comments",
    "description": "Delete own comments, or any comment with team permission."
  },
  {
    "scope": "read_settings",
    "title": "Read team settings",
    "description": "Read team branding and preferences."
  },
  {
    "scope": "manage_settings",
    "title": "Manage team settings",
    "description": "Edit team branding and preferences."
  }
]

One-click alternative

If your Glint app has a client secret and allow_self_manage_exported_permissions is enabled in Prism, a Glint owner can hit Settings → App Config → Register Permissions to push these same definitions to Prism without leaving Glint.

Access Rules

Optionally, set access rules to restrict which apps or users can register these scopes:

  • app_allow — only specific App B client_ids may request your scopes
  • owner_allow — only specific Prism user IDs may add your scopes to their app's allowed_scopes

Without any allow rules, all apps are permitted by default.


Step 2 — Register the Scope in App B (App B Developer)

In App B's Prism dashboard → Settings → App Permissions, enter Glint's client ID and select the inner scopes your app needs. For example, selecting read_todos adds:

app:prism_abc123:read_todos

to App B's allowed_scopes. This step is gated by any owner_allow rules Glint's admin may have set.

Build App B's Allowed Scopes

Paste your Glint client ID, tick the permissions App B needs, then click Copy and paste the result into Prism's Import scopes dialog (App B → Settings → Allowed Scopes → Import scopes). Existing scopes are kept; new ones are merged in. Don't forget to click Save afterwards.

Platform scopes
Glint cross-app scopes
Generated allowed_scopes5 scope(s)
[
  "openid",
  "profile",
  "teams:read",
  "offline_access",
  "app:prism_abc123:read_todos"
]

Request only the scopes your app genuinely needs — leave the rest unticked. Users see the scope titles on the Prism consent screen.

Alternatively, via API:

bash
curl -X PATCH https://prism.example.com/api/apps/<appB_id> \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "allowed_scopes": [
      "openid", "profile", "teams:read",
      "app:prism_abc123:read_todos",
      "app:prism_abc123:create_todos"
    ]
  }'

Step 3 — Request the Scope in the Authorization URL

When App B redirects a user to Prism for login, include Glint's scopes in the scope parameter:

https://prism.example.com/api/oauth/authorize
  ?client_id=<appB_client_id>
  &redirect_uri=https://appb.example.com/callback
  &response_type=code
  &scope=openid+profile+teams%3Aread+app%3Aprism_abc123%3Aread_todos+app%3Aprism_abc123%3Acreate_todos
  &code_challenge=...
  &code_challenge_method=S256

The user will see a consent card in Prism showing Glint's scope titles as defined in Step 1.


Step 4 — Exchange the Code and Call Glint

After the user approves and you complete the standard token exchange, use the resulting access token as a Bearer token when calling Glint:

ts
const response = await fetch(
  `https://glint.example.com/api/cross-app/teams/${teamId}/sets`,
  {
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
  }
);
const { sets } = await response.json();

Team Membership Resolution

Glint's cross-app endpoints are team-scoped. To serve a request, Glint must verify that the user (identified by the sub in the introspected token) is a member of the requested team.

Glint resolves team membership via two paths, in order:

  1. KV cache (fast path) — populated whenever the user logs in to Glint directly. If the user has logged in to Glint at least once, their team memberships are cached for 1 hour in KV.
  2. Live Prism fetch (fallback) — if the bearer token includes teams:read scope, Glint calls Prism's /api/oauth/me/teams in real time and caches the result.

If neither path succeeds, the request returns 403 with an explanatory message. Always include teams:read in App B's requested scopes to ensure this fallback works for users who have never logged in to Glint.

openid profile teams:read app:prism_abc123:read_todos

Permission Enforcement

Possessing a Prism scope is a necessary but not sufficient condition for most write operations. Glint also enforces its own role-based permission system on every request.

For example, a user with the member role and a complete_todos Prism scope will still receive 403 if the team's complete_any_todo permission is disabled for members. The two systems work in tandem:

CheckWhere enforcedFailure response
Token is active and scopedPrism introspection401 / 403
User is a team memberGlint KV / Prism403
User has the Glint permissionGlint D1 (per-set or global)403

Per-set permission overrides are respected. If a team has restricted view_todos for a specific set, cross-app requests to that set will be denied even if the global permission is open.


App Token Warning in Glint UI

When a user logs in to Glint and Glint detects that the underlying access token was issued to an external application (i.e., the token's client_id differs from Glint's own client_id), Glint displays a modal warning:

Access via App Token — This session was established using a token issued to an external application, not directly to you. If you did not expect this, sign out immediately.

The user can choose to continue or sign out. This is a security safeguard and should not appear under normal cross-app usage (where App B uses the token server-side and the user logs in to Glint separately through Glint's own flow).


Error Reference

StatusMeaning
401Missing or malformed Authorization header, or token is inactive/expired
403Token is missing the required scope
403User is not a member of the team
403Team membership unavailable (no KV cache and no teams:read in token scope)
403User's Glint role permission is denied for this operation or set
404Resource not found, or does not belong to the requested team

Security Notes

  • Glint always verifies tokens via Prism's introspection endpoint — it never trusts the token payload directly.
  • App B's client_secret is never involved; only the client_id identifies the scope namespace.
  • If you want to restrict which apps can use Glint's scopes, set app_allow rules in Prism before publishing the client_id to integrators.
  • Revoking a user's App B consent in Prism also removes their access to Glint's resources — no extra action needed.
  • Cross-app tokens respect per-set permission overrides, not just global role permissions.
  • Use the most specific scopes available. Requesting write_todos gives broader access than create_todos alone.

Full Example (TypeScript)

ts
const GLINT = "https://glint.example.com";

async function listSets(token: string, teamId: string) {
  const res = await fetch(`${GLINT}/api/cross-app/teams/${teamId}/sets`, {
    headers: { Authorization: `Bearer ${token}` },
  });
  if (!res.ok) throw new Error((await res.json()).error);
  return (await res.json()).sets as Array<{ id: string; name: string }>;
}

async function listTodos(token: string, teamId: string, setId: string) {
  const res = await fetch(
    `${GLINT}/api/cross-app/teams/${teamId}/sets/${setId}/todos`,
    { headers: { Authorization: `Bearer ${token}` } },
  );
  if (!res.ok) throw new Error((await res.json()).error);
  return (await res.json()).todos;
}

async function createTodo(
  token: string,
  teamId: string,
  setId: string,
  title: string,
  parentId?: string,
) {
  const res = await fetch(
    `${GLINT}/api/cross-app/teams/${teamId}/sets/${setId}/todos`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ title, parentId }),
    },
  );
  if (!res.ok) throw new Error((await res.json()).error);
  return (await res.json()).todo;
}

async function completeTodo(token: string, teamId: string, todoId: string) {
  const res = await fetch(
    `${GLINT}/api/cross-app/teams/${teamId}/todos/${todoId}`,
    {
      method: "PATCH",
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ completed: true }),
    },
  );
  if (!res.ok) throw new Error((await res.json()).error);
}

async function postComment(
  token: string,
  teamId: string,
  todoId: string,
  body: string,
) {
  const res = await fetch(
    `${GLINT}/api/cross-app/teams/${teamId}/todos/${todoId}/comments`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ body }),
    },
  );
  if (!res.ok) throw new Error((await res.json()).error);
  return (await res.json()).comment;
}

async function getSettings(token: string, teamId: string) {
  const res = await fetch(
    `${GLINT}/api/cross-app/teams/${teamId}/settings`,
    { headers: { Authorization: `Bearer ${token}` } },
  );
  if (!res.ok) throw new Error((await res.json()).error);
  return (await res.json()).settings;
}