Involute exposes two HTTP surfaces:
- REST-like auth and health endpoints on the same server origin
- a GraphQL API at
/graphql
Default local endpoints:
http://localhost:4200/healthhttp://localhost:4200/auth/*http://localhost:4200/graphql
Production example:
https://involute.example.com/healthhttps://involute.example.com/auth/*https://involute.example.com/graphql
Supported auth modes:
- browser session cookie via Google OAuth
- trusted bearer token via
Authorization: Bearer <AUTH_TOKEN> - trusted viewer assertion via the configured viewer assertion header
Typical browser flow:
GET /auth/google/start- Google redirects back to
/auth/google/callback - server sets the session cookie
- browser calls
GET /auth/session - browser uses the session cookie for
/graphql
Returns plain text health status.
Response:
OK
Returns the current session state.
Response shape:
{
"authMode": "session",
"authenticated": true,
"googleOAuthConfigured": true,
"viewer": {
"email": "user@example.com",
"globalRole": "ADMIN",
"id": "uuid",
"name": "User Name"
}
}Unauthenticated example:
{
"authMode": "none",
"authenticated": false,
"googleOAuthConfigured": true,
"viewer": null
}Starts the Google OAuth login flow.
Behavior:
- returns
302 - sets the temporary OAuth state cookie
- redirects to Google authorization
OAuth callback endpoint.
Behavior:
- validates the OAuth state
- exchanges the authorization code
- upserts the user
- creates the session
- redirects back to
APP_ORIGIN
Failure behavior:
- redirects to
APP_ORIGIN?authError=<reason>
Clears the session cookie and deletes the backing session.
Response:
{
"success": true
}The GraphQL API uses a single endpoint.
Example:
curl https://involute.example.com/graphql \
-H 'content-type: application/json' \
-H 'authorization: Bearer YOUR_AUTH_TOKEN' \
--data '{"query":"query { teams { nodes { id key name visibility } } }"}'Returns the authenticated viewer or null.
query Viewer {
viewer {
id
name
email
globalRole
isMe
}
}Looks up an issue by UUID or business identifier.
query Issue($id: String!) {
issue(id: $id) {
id
identifier
title
description
createdAt
updatedAt
state { id name }
team { id key name visibility }
assignee { id name email }
labels { nodes { id name } }
parent { id identifier title }
children {
nodes { id identifier title }
}
comments(first: 50) {
nodes {
id
body
createdAt
user { id name email }
}
pageInfo {
hasNextPage
endCursor
}
}
}
}Returns the issue connection. The server clamps first to a safe limit.
Supported filters:
- team key
- workflow state name
assignee.isMe- label name via
some/every - nested
and
query Issues($first: Int!, $after: String, $filter: IssueFilter) {
issues(first: $first, after: $after, filter: $filter) {
nodes {
id
identifier
title
updatedAt
state { id name }
assignee { id name }
labels { nodes { id name } }
team { id key name }
}
pageInfo {
hasNextPage
endCursor
}
}
}Example variables:
{
"first": 100,
"filter": {
"team": {
"key": {
"eq": "SON"
}
}
}
}Returns visible teams.
query Teams {
teams {
nodes {
id
key
name
visibility
states {
nodes {
id
name
}
}
}
}
}Returns issue labels.
query Labels {
issueLabels {
nodes {
id
name
}
}
}Returns users visible to the current viewer.
query Users {
users {
nodes {
id
name
email
globalRole
isMe
}
}
}Creates an issue inside a team.
mutation IssueCreate($input: IssueCreateInput!) {
issueCreate(input: $input) {
success
issue {
id
identifier
title
state { id name }
team { id key name }
}
}
}Example variables:
{
"input": {
"teamId": "team-uuid",
"title": "Refine workspace shell spacing",
"description": "Tighten toolbar alignment and chip density.",
"stateId": "workflow-state-uuid"
}
}Updates any combination of:
stateIdlabelIdsparentIdtitledescriptionassigneeId
mutation IssueUpdate($id: String!, $input: IssueUpdateInput!) {
issueUpdate(id: $id, input: $input) {
success
issue {
id
identifier
title
updatedAt
state { id name }
assignee { id name }
labels { nodes { id name } }
}
}
}Deletes an issue.
mutation IssueDelete($id: String!) {
issueDelete(id: $id) {
success
issueId
}
}Creates a comment on an issue.
mutation CommentCreate($input: CommentCreateInput!) {
commentCreate(input: $input) {
success
comment {
id
body
createdAt
user { id name email }
}
}
}Deletes a comment.
mutation CommentDelete($id: String!) {
commentDelete(id: $id) {
success
commentId
}
}Changes team visibility.
mutation TeamUpdateAccess($input: TeamUpdateAccessInput!) {
teamUpdateAccess(input: $input) {
success
team {
id
key
name
visibility
}
}
}Creates or updates a membership by email.
mutation TeamMembershipUpsert($input: TeamMembershipUpsertInput!) {
teamMembershipUpsert(input: $input) {
success
membership {
id
role
user {
id
name
email
globalRole
}
}
}
}Removes a team membership.
mutation TeamMembershipRemove($input: TeamMembershipRemoveInput!) {
teamMembershipRemove(input: $input) {
success
membershipId
}
}PRIVATEPUBLIC
VIEWEREDITOROWNER
ADMINUSER
Issue and comment connections return:
type PageInfo {
hasNextPage: Boolean!
endCursor: String
}Use endCursor as the next after value.
ADMINcan read all teams- members can read their teams
- signed-in users can read
PUBLICteams PRIVATEteams stay hidden from non-members
ADMINcan manage all teamsOWNERcan manage team visibility and membershipsEDITORandOWNERcan modify issues and commentsVIEWERis read-only
The API exposes safe validation and permission errors as GraphQL errors.
Typical categories:
- validation errors
- not found errors
- forbidden errors
Mutation payloads still return success, but authorization failures are not silently downgraded into a fake success response.