Starfall API Gateway

The Starfall API Gateway is the single entry point for all Starfall platform services. It handles authentication, routing, and request tracing across downstream microservices.

Base URL https://api.starfallwebdesign.ca
Protocol HTTPS only — all plaintext HTTP connections are rejected
Format All requests and responses are application/json
GraphQL All operations use a single POST /graphql endpoint
Restaurant modules: This gateway currently exposes User Service operations only. Reservations, menu/product CRUD, and orders are not public GraphQL operations in this schema yet.

Quick Start

Send your first request to verify the gateway is reachable:

Shell
curl https://api.starfallwebdesign.ca/health

Authentication

The gateway uses a short-lived access token paired with a long-lived refresh token. Token validation is delegated to the User Service — the gateway itself does not inspect token contents beyond forwarding.

Token Flow

1

Login to receive tokens

Call the login mutation with your credentials. You will receive an accessToken and a refreshToken.

2

Load roles and organizations from me

After storing both tokens, call me with the access token and build client auth state from roles, role.permissions, and organizations.

3

Attach the access token to every protected request

Authorization: Bearer <access_token>
4

Refresh when the access token expires

Call refreshToken with your refreshToken to obtain a new token pair without re-authenticating.

Public vs. Protected Operations

Operation Type Auth Required
loginMutationNo Auth
registerMutationNo Auth
refreshTokenMutationNo Auth
meQueryAuth Required
userQueryAuth Required
usersQueryAuth Required
logoutMutationAuth Required
organizationQueryAuth Required
organizationsQueryAuth Required
createUserMutationAuth Required
updateUserMutationAuth Required
deleteUserMutationAuth Required
createOrganizationMutationAuth Required
roleQueryAuth Required
rolesQueryAuth Required
assignRoleMutationAuth Required
Note: Token validation is performed by the User Service. The gateway forwards the Authorization header to downstream services unchanged.

Permission Model

Permissions are exposed as structured Permission objects on Role.permissions. Clients should check the canonical action string, such as users.manage or users.write, and may use resource for grouping or display.

Permission actions are distinct. Do not assume users.manage includes users.write unless both actions are returned by me or role.


Endpoints

GET /health

GET https://api.starfallwebdesign.ca/health No Auth

Returns the operational status of the gateway and all registered downstream services. Use this endpoint for health checks, uptime monitoring, and deployment verification.

Request

Shell
curl https://api.starfallwebdesign.ca/health

Responses

200 — Healthy
{
  "status": "ok",
  "downstreams": {
    "userService": {
      "ok": true,
      "status": 200
    }
  }
}
200 — Degraded
{
  "status": "ok",
  "downstreams": {
    "userService": {
      "ok": false,
      "status": 503
    }
  }
}
The gateway always returns HTTP 200 for the health endpoint itself. Inspect downstreams[*].ok to determine if individual services are reachable.

POST /graphql

POST https://api.starfallwebdesign.ca/graphql

All GraphQL operations are sent as HTTP POST requests to this single endpoint. The request body must be application/json and contain a query string and optionally a variables object. Protected operations additionally require an Authorization: Bearer <token> header.


Operations

login

Mutation No Auth

Authenticate a user with email and password. Returns an access token, a refresh token, and the authenticated user object.

Request
{
  "query": "mutation Login($input: LoginInput!) { login(input: $input) { accessToken refreshToken user { id email name status } } }",
  "variables": {
    "input": {
      "email": "user@example.com",
      "password": "your-password"
    }
  }
}
Response
{
  "data": {
    "login": {
      "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
      "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
      "user": {
        "id": "usr_01abc",
        "email": "user@example.com",
        "name": "Jane Smith",
        "status": "ACTIVE"
      }
    }
  }
}

register

Mutation No Auth

Public self-service sign-up. No authentication required. Creates a new user account and returns an access token, refresh token, and the created user — so the caller is signed in immediately after registration.

By default the account is created with emailVerifiedAt: null and the user must complete the email verification flow before the account becomes fully active. Pass autoVerify: true to skip this and mark the account as verified immediately — useful for services that handle verification out-of-band or do not require it.

For admin-created accounts (where initial status or email verification policy needs to be controlled) use createUser instead.

Input fields

FieldTypeRequiredDescription
emailStringYesThe user's email address.
passwordStringYesThe user's chosen password.
nameStringNoDisplay name.
timezoneStringNoIANA timezone string e.g. America/New_York.
languageStringNoBCP 47 language tag e.g. en.
autoVerifyBooleanNoWhen true, sets emailVerifiedAt immediately. Defaults to false.
Request
{
  "query": "mutation Register($input: RegisterInput!) { register(input: $input) { accessToken refreshToken user { id email name emailVerifiedAt } } }",
  "variables": {
    "input": {
      "email": "user@example.com",
      "password": "Secret123!",
      "name": "Jane Smith",
      "autoVerify": false
    }
  }
}
Response
{
  "data": {
    "register": {
      "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
      "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
      "user": {
        "id": "b4750146-3ae6-4a56-a75d-f3b2c86639a6",
        "email": "user@example.com",
        "name": "Jane Smith",
        "emailVerifiedAt": null
      }
    }
  }
}

refreshToken

Mutation No Auth

Exchange a valid refresh token for a new token pair. The old refresh token is invalidated after use.

Request
{
  "query": "mutation RefreshToken($token: String!) { refreshToken(token: $token) { accessToken refreshToken user { id email } } }",
  "variables": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
  }
}
Response
{
  "data": {
    "refreshToken": {
      "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
      "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
      "user": {
        "id": "usr_01abc",
        "email": "user@example.com"
      }
    }
  }
}

logout

Mutation Auth Required

Invalidate the current session's tokens. Returns true on success. The access token should be discarded by the client immediately.

Request
{
  "query": "mutation { logout }"
}
Response
{
  "data": {
    "logout": true
  }
}

me

Query Auth Required

Returns the full profile of the currently authenticated user, including their assigned roles and organization memberships.

Request
{
  "query": "query Me { me { id email name status timezone language mfaEnabled lastLoginAt createdAt roles { role { id name } organization { id name } } organizations { id name status } } }"
}
Response
{
  "data": {
    "me": {
      "id": "usr_01abc",
      "email": "user@example.com",
      "name": "Jane Smith",
      "status": "ACTIVE",
      "timezone": "America/Toronto",
      "language": "en",
      "mfaEnabled": false,
      "lastLoginAt": "2026-03-19T10:00:00.000Z",
      "createdAt": "2025-01-01T00:00:00.000Z",
      "roles": [],
      "organizations": []
    }
  }
}

user

Query Auth Required

Fetch a single user by their unique ID.

Request
{
  "query": "query GetUser($id: ID!) { user(id: $id) { id email name status createdAt roles { role { id name } } } }",
  "variables": {
    "id": "usr_01abc"
  }
}
Response
{
  "data": {
    "user": {
      "id": "usr_01abc",
      "email": "user@example.com",
      "name": "Jane Smith",
      "status": "ACTIVE",
      "createdAt": "2025-01-01T00:00:00.000Z",
      "roles": []
    }
  }
}

users

Query Auth Required

List users with optional filtering and pagination. Supports searching by name or email and filtering by status.

Request
{
  "query": "query ListUsers($filter: UserFilter, $pagination: PaginationInput) { users(filter: $filter, pagination: $pagination) { total page pageSize totalPages users { id email name status createdAt } } }",
  "variables": {
    "filter": {
      "status": "ACTIVE",
      "search": "jane"
    },
    "pagination": {
      "page": 1,
      "pageSize": 20,
      "sortOrder": "DESC"
    }
  }
}
Response
{
  "data": {
    "users": {
      "total": 42,
      "page": 1,
      "pageSize": 20,
      "totalPages": 3,
      "users": [
        {
          "id": "usr_01abc",
          "email": "user@example.com",
          "name": "Jane Smith",
          "status": "ACTIVE",
          "createdAt": "2025-01-01T00:00:00.000Z"
        }
      ]
    }
  }
}

organization

Query Auth Required

Fetch a single organization by ID, including its owner, locations, and current status.

Request
{
  "query": "query GetOrganization($id: ID!) { organization(id: $id) { id name legalName shortcode status owner { id email name } locations { id name status } createdAt } }",
  "variables": {
    "id": "org_01xyz"
  }
}
Response
{
  "data": {
    "organization": {
      "id": "org_01xyz",
      "name": "Acme Corp",
      "legalName": "Acme Corporation Inc.",
      "shortcode": "acme",
      "status": "ACTIVE",
      "owner": {
        "id": "usr_01abc",
        "email": "owner@acme.com",
        "name": "Jane Smith"
      },
      "locations": [],
      "createdAt": "2025-01-01T00:00:00.000Z"
    }
  }
}

organizations

Query Auth Required

List organizations with optional filtering by status or search term.

Request
{
  "query": "query ListOrganizations($filter: OrganizationFilter) { organizations(filter: $filter) { id name legalName shortcode status createdAt } }",
  "variables": {
    "filter": {
      "status": "ACTIVE",
      "search": "acme"
    }
  }
}
Response
{
  "data": {
    "organizations": [
      {
        "id": "org_01xyz",
        "name": "Acme Corp",
        "legalName": "Acme Corporation Inc.",
        "shortcode": "acme",
        "status": "ACTIVE",
        "createdAt": "2025-01-01T00:00:00.000Z"
      }
    ]
  }
}

createUser

Mutation Auth Required

Create a new user account. Requires the users.manage permission. Returns the created user object.

Request
{
  "query": "mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { id email name status timezone language createdAt } }",
  "variables": {
    "input": {
      "email": "newuser@example.com",
      "password": "SecurePassword123!",
      "name": "John Doe",
      "timezone": "America/Toronto",
      "language": "en",
      "status": "ACTIVE"
    }
  }
}
Response
{
  "data": {
    "createUser": {
      "id": "usr_02xyz",
      "email": "newuser@example.com",
      "name": "John Doe",
      "status": "ACTIVE",
      "timezone": "America/Toronto",
      "language": "en",
      "createdAt": "2026-03-19T10:00:00.000Z"
    }
  }
}

updateUser

Mutation Auth Required

Update fields on an existing user by ID. Only the fields provided in input will be changed. Requires the users.write permission.

Request
{
  "query": "mutation UpdateUser($id: ID!, $input: UpdateUserInput!) { updateUser(id: $id, input: $input) { id email name status timezone language updatedAt } }",
  "variables": {
    "id": "usr_02xyz",
    "input": {
      "name": "John D.",
      "status": "ACTIVE",
      "timezone": "America/Vancouver"
    }
  }
}
Response
{
  "data": {
    "updateUser": {
      "id": "usr_02xyz",
      "email": "newuser@example.com",
      "name": "John D.",
      "status": "ACTIVE",
      "timezone": "America/Vancouver",
      "language": "en",
      "updatedAt": "2026-03-19T11:00:00.000Z"
    }
  }
}

deleteUser

Mutation Auth Required

Soft-delete a user by ID. Sets deleted_at and transitions the account status to ARCHIVED. The record is retained and can be restored. Requires the users.manage permission.

Request
{
  "query": "mutation DeleteUser($id: ID!) { deleteUser(id: $id) }",
  "variables": {
    "id": "usr_02xyz"
  }
}
Response
{
  "data": {
    "deleteUser": true
  }
}

createOrganization

Mutation Auth Required

Create a new organization. The authenticated user is automatically set as the owner. Returns the created organization with owner details.

Request
{
  "query": "mutation CreateOrganization($input: CreateOrganizationInput!) { createOrganization(input: $input) { id name legalName shortcode status owner { id email } createdAt } }",
  "variables": {
    "input": {
      "name": "Acme Corp",
      "legalName": "Acme Corporation Inc.",
      "shortcode": "acme",
      "status": "ACTIVE"
    }
  }
}
Response
{
  "data": {
    "createOrganization": {
      "id": "org_02abc",
      "name": "Acme Corp",
      "legalName": "Acme Corporation Inc.",
      "shortcode": "acme",
      "status": "ACTIVE",
      "owner": {
        "id": "usr_01abc",
        "email": "owner@example.com"
      },
      "createdAt": "2026-03-19T10:00:00.000Z"
    }
  }
}

role

Query Auth Required

Fetch a single role by ID, including its full list of permissions.

Request
{
  "query": "query GetRole($id: ID!) { role(id: $id) { id name description status systemRole isDefault permissions { id action resource } } }",
  "variables": {
    "id": "role-1"
  }
}
Response
{
  "data": {
    "role": {
      "id": "role-1",
      "name": "Admin",
      "description": "Full administrative access",
      "status": "ACTIVE",
      "systemRole": false,
      "isDefault": false,
      "permissions": [
        {
          "id": "perm-1",
          "action": "users.manage",
          "resource": "users"
        }
      ]
    }
  }
}

roles

Query Auth Required

List all roles available in the system.

Request
{
  "query": "query ListRoles { roles { id name description status systemRole isDefault } }"
}
Response
{
  "data": {
    "roles": [
      {
        "id": "role-1",
        "name": "Admin",
        "description": "Full administrative access",
        "status": "ACTIVE",
        "systemRole": false,
        "isDefault": false
      },
      {
        "id": "role-2",
        "name": "Member",
        "description": "Standard member access",
        "status": "ACTIVE",
        "systemRole": false,
        "isDefault": true
      }
    ]
  }
}

assignRole

Mutation Auth Required

Assign a role to a user within the context of a specific organization. Requires the roles.manage permission. Returns the created UserRole record.

Request
{
  "query": "mutation AssignRole($input: AssignRoleInput!) { assignRole(input: $input) { id status grantedAt role { id name } organization { id name } } }",
  "variables": {
    "input": {
      "userId": "usr_02xyz",
      "roleId": "role-1",
      "organizationId": "org_01xyz"
    }
  }
}
Response
{
  "data": {
    "assignRole": {
      "id": "ur-1",
      "status": "ACTIVE",
      "grantedAt": "2026-03-19T10:00:00.000Z",
      "role": {
        "id": "role-1",
        "name": "Admin"
      },
      "organization": {
        "id": "org_01xyz",
        "name": "Acme Corp"
      }
    }
  }
}

Flows

Create User with Role

This guide walks through the end-to-end process of creating a new user and assigning them a role within an organization. All three requests require an Authorization: Bearer <token> header. The caller must have both users.manage and roles.manage permissions.

1

List available roles — note the id of the role you want to assign

{
  "query": "query ListRoles { roles { id name description status isDefault } }"
}
2

Create the user — save the returned id

{
  "query": "mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { id email name status createdAt } }",
  "variables": {
    "input": {
      "email": "newuser@example.com",
      "password": "SecurePassword123!",
      "name": "John Doe",
      "timezone": "America/Toronto",
      "language": "en",
      "status": "ACTIVE"
    }
  }
}
3

Assign the role using userId + roleId + organizationId

{
  "query": "mutation AssignRole($input: AssignRoleInput!) { assignRole(input: $input) { id status grantedAt role { id name } organization { id name } } }",
  "variables": {
    "input": {
      "userId": "usr_02xyz",
      "roleId": "role-1",
      "organizationId": "org_01xyz"
    }
  }
}
Permissions required: The authenticated caller must hold users.manage (for step 2) and roles.manage (for step 3) within the target organization.

Types

Full type definitions for all objects returned or accepted by the API.

User

FieldTypeDescription
idID!Unique user identifier
emailString!Primary email address
emailVerifiedAtTimeWhen the email was verified; null if unverified
nameString!Display name
avatarStringURL to profile image
timezoneStringIANA timezone string (e.g. America/Toronto)
languageStringBCP-47 language code (e.g. en)
statusUserStatus!Account status
mfaEnabledBoolean!Whether MFA is active
lastLoginAtTimeMost recent successful login timestamp
createdAtTime!Account creation timestamp
updatedAtTime!Last update timestamp
roles[UserRole!]!Roles assigned to this user
organizations[Organization!]!Organizations this user belongs to

Organization

The current schema does not expose an organization-level timezone field. Use User.timezone until that field is added.

FieldTypeDescription
idID!Unique organization identifier
nameString!Display name
legalNameStringLegal registered name
shortcodeStringShort identifier slug
statusOrganizationStatus!Organization status
ownerUser!Owning user
locations[Location!]!Organization locations
createdAtTime!Creation timestamp
updatedAtTime!Last update timestamp

Location

FieldTypeDescription
idID!Unique location identifier
nameString!Location name
brandNameStringCustomer-facing brand name
organizationOrganizationParent organization
statusLocationStatus!Location status
createdAtTime!Creation timestamp
updatedAtTime!Last update timestamp

AuthPayload

FieldTypeDescription
accessTokenString!Short-lived JWT for authenticating requests
refreshTokenString!Long-lived token used to obtain new access tokens
userUser!The authenticated user object

UsersConnection

FieldTypeDescription
users[User!]!Array of users for the current page
totalInt!Total number of matching records
pageInt!Current page number (1-based)
pageSizeInt!Number of results per page
totalPagesInt!Total number of pages

CreateUserInput

FieldTypeRequiredDescription
emailStringRequiredEmail address for the new account
passwordStringRequiredInitial plaintext password (hashed server-side)
nameStringRequiredUser's display name
timezoneStringOptionalIANA timezone, defaults to UTC
languageStringOptionalBCP-47 code, defaults to en
statusUserStatusOptionalDefaults to PENDING

RegisterInput

FieldTypeRequiredDescription
emailString!RequiredEmail address
passwordString!RequiredPassword chosen by the user
nameStringOptionalDisplay name
timezoneStringOptionalIANA timezone string
languageStringOptionalISO 639-1 language code
autoVerifyBooleanOptionalMark email verified immediately when allowed by the caller's flow

UpdateUserInput

FieldTypeDescription
nameStringUpdate display name
statusUserStatusUpdate account status
timezoneStringUpdate timezone
languageStringUpdate language preference

UserFilter

FieldTypeDescription
statusUserStatusFilter by account status
organizationIdIDFilter users belonging to an organization
locationIdIDFilter users assigned to a location
roleIdIDFilter users holding a role
searchStringSearch across name and email

PaginationInput

FieldTypeDescription
pageIntPage number, 1-based
pageSizeIntNumber of records per page
sortByStringField name to sort by
sortOrderSortOrderASC or DESC

CreateOrganizationInput

FieldTypeRequiredDescription
nameString!RequiredOrganization display name
legalNameStringOptionalFull legal name
shortcodeStringOptionalShort identifier slug
statusOrganizationStatusOptionalInitial status

OrganizationFilter

FieldTypeDescription
statusOrganizationStatusFilter by organization status
ownerIdIDFilter by owner user ID
searchStringFree-text search on name

AssignRoleInput

FieldTypeRequiredDescription
userIdIDRequiredID of the user to receive the role
roleIdIDRequiredID of the role to assign
organizationIdIDRequiredOrganization context for the role assignment

Role

FieldTypeDescription
idID!Unique role identifier
nameString!Human-readable role name
descriptionStringOptional description of the role
statusString!Role status (ACTIVE / INACTIVE)
systemRoleBoolean!Whether the role is managed by the system
isDefaultBoolean!Whether this role is auto-assigned to new users
permissions[Permission!]!List of permissions granted by this role

UserRole

FieldTypeDescription
idID!Unique assignment identifier
statusString!Assignment status
grantedAtTime!When the role was granted
roleRole!The assigned role
organizationOrganization!The organization context

Permission

FieldTypeDescription
idID!Unique permission identifier
nameString!Human-readable permission name
actionString!Canonical action clients should check, such as users.manage
resourceString!Resource family, such as users
descriptionStringOptional description
productProductAssociated product/module metadata

Product

This is product/module metadata used by permissions, not a restaurant menu item type.

FieldTypeDescription
idID!Unique product identifier
nameString!Product/module name
descriptionStringOptional description
typeProductType!Product category
statusProductStatus!Lifecycle status
activeBoolean!Whether the product is active
systemRoleBoolean!Whether managed by the system

Enums

UserStatus
ValueDescription
ACTIVEAccount is active and can authenticate
PENDINGAccount created, awaiting verification
SUSPENDEDAccess temporarily revoked
ARCHIVEDSoft-deleted, not recoverable via API
OrganizationStatus
ValueDescription
ACTIVEOrganization is operational
INACTIVEOrganization is dormant
SUSPENDEDAccess temporarily restricted
ARCHIVEDSoft-deleted organization
Time scalar: All timestamp fields use the ISO 8601 UTC format: 2026-03-19T10:00:00.000Z

Frontend Auth Flow

Use login -> me -> choose organization -> send scoped inputs as the browser-client bootstrap sequence.

Step 1Call login or register and store both tokens.
Step 2Call me with Authorization: Bearer <access_token>.
Step 3Build auth state from roles, role.permissions, and organizations.
Step 4Let the user choose an organization when multiple memberships are returned.
Do not read User.role or User.permissions. The supported shape is User.roles[] with nested role, organization, and assignment metadata.

Organization-Scoped Requests

Current User Service operations pass organization scope through GraphQL arguments or input fields: organization(id:), users(filter: { organizationId }), and assignRole(input: { organizationId }).

The gateway forwards Authorization and x-request-id. It does not read or enforce an X-Organization-ID header, and single-organization users should not rely on omitted headers to imply scope.

Restaurant/runtime services are not exposed yet. When they are added, each operation must document whether tenant scope comes from input, header, JWT claim, or another mechanism.

Schema Publication

The served SDL snapshot is available at /schema.graphql. Prefer code generation and contract checks from this SDL instead of copying fields from examples by hand.

GraphQL introspection may be disabled in production depending on deployment settings. Treat the SDL snapshot as the canonical downloadable schema artifact for the documented gateway contract.


Errors

GraphQL errors are returned in the standard errors array alongside any partial data. Each error object contains a human-readable message, an error code in extensions, and optionally an upstream block for downstream failures.

Standard Error Format

{
  "errors": [
    {
      "message": "You must be authenticated to perform this operation.",
      "locations": [{ "line": 1, "column": 1 }],
      "path": ["me"],
      "extensions": {
        "code": "UNAUTHORIZED"
      }
    }
  ],
  "data": null
}

Error Codes

CodeHTTP StatusDescription
UNAUTHORIZED 401 A protected operation was called without a valid Authorization header
UPSTREAM_ERROR 502 A downstream service returned an error or was unreachable
BAD_USER_INPUT 400 The request input failed validation (missing required fields, invalid format, etc.)
INTERNAL_SERVER_ERROR 500 An unexpected error occurred within the gateway itself

Upstream Error Example

When the User Service returns an error, it is surfaced as an UPSTREAM_ERROR with additional context in extensions.upstream:

{
  "errors": [
    {
      "message": "Upstream service error: userService returned 503",
      "extensions": {
        "code": "UPSTREAM_ERROR",
        "upstream": {
          "service": "userService",
          "status": 503,
          "message": "Service temporarily unavailable"
        }
      }
    }
  ],
  "data": null
}

Request Tracing

The gateway supports end-to-end request tracing via the x-request-id header. Attaching a unique identifier to each request makes it significantly easier to trace a single request across the gateway logs, downstream service logs, and error reports.

Header name x-request-id
If not provided The gateway generates a UUID v4 automatically
Forwarding Forwarded to all downstream service calls
Response Echoed back in the response as x-request-id

Example — Providing a request ID

curl -X POST https://api.starfallwebdesign.ca/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <access_token>" \
  -H "x-request-id: 550e8400-e29b-41d4-a716-446655440000" \
  -d '{"query":"query Me { me { id email name } }"}'
Recommendation: Always include an x-request-id in production requests. Use a UUID v4 generated per-request. When filing a bug report or support ticket, include the request ID to enable fast log correlation.