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.
https://api.starfallwebdesign.ca
application/json
POST /graphql endpoint
Quick Start
Send your first request to verify the gateway is reachable:
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
Login to receive tokens
Call the login mutation with your credentials. You will receive an accessToken and a refreshToken.
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.
Attach the access token to every protected request
Authorization: Bearer <access_token>
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 |
|---|---|---|
| login | Mutation | No Auth |
| register | Mutation | No Auth |
| refreshToken | Mutation | No Auth |
| me | Query | Auth Required |
| user | Query | Auth Required |
| users | Query | Auth Required |
| logout | Mutation | Auth Required |
| organization | Query | Auth Required |
| organizations | Query | Auth Required |
| createUser | Mutation | Auth Required |
| updateUser | Mutation | Auth Required |
| deleteUser | Mutation | Auth Required |
| createOrganization | Mutation | Auth Required |
| role | Query | Auth Required |
| roles | Query | Auth Required |
| assignRole | Mutation | Auth Required |
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
Returns the operational status of the gateway and all registered downstream services. Use this endpoint for health checks, uptime monitoring, and deployment verification.
Request
curl https://api.starfallwebdesign.ca/health
Responses
{
"status": "ok",
"downstreams": {
"userService": {
"ok": true,
"status": 200
}
}
}
{
"status": "ok",
"downstreams": {
"userService": {
"ok": false,
"status": 503
}
}
}
200 for the health endpoint itself.
Inspect downstreams[*].ok to determine if individual services are reachable.
POST /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
Authenticate a user with email and password. Returns an access token, a refresh token, and the authenticated user object.
{
"query": "mutation Login($input: LoginInput!) { login(input: $input) { accessToken refreshToken user { id email name status } } }",
"variables": {
"input": {
"email": "user@example.com",
"password": "your-password"
}
}
}
{
"data": {
"login": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "usr_01abc",
"email": "user@example.com",
"name": "Jane Smith",
"status": "ACTIVE"
}
}
}
}
register
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
| Field | Type | Required | Description |
|---|---|---|---|
| String | Yes | The user's email address. | |
| password | String | Yes | The user's chosen password. |
| name | String | No | Display name. |
| timezone | String | No | IANA timezone string e.g. America/New_York. |
| language | String | No | BCP 47 language tag e.g. en. |
| autoVerify | Boolean | No | When true, sets emailVerifiedAt immediately. Defaults to false. |
{
"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
}
}
}
{
"data": {
"register": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "b4750146-3ae6-4a56-a75d-f3b2c86639a6",
"email": "user@example.com",
"name": "Jane Smith",
"emailVerifiedAt": null
}
}
}
}
refreshToken
Exchange a valid refresh token for a new token pair. The old refresh token is invalidated after use.
{
"query": "mutation RefreshToken($token: String!) { refreshToken(token: $token) { accessToken refreshToken user { id email } } }",
"variables": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
{
"data": {
"refreshToken": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "usr_01abc",
"email": "user@example.com"
}
}
}
}
logout
Invalidate the current session's tokens. Returns true on success. The access token should be discarded by the client immediately.
{
"query": "mutation { logout }"
}
{
"data": {
"logout": true
}
}
me
Returns the full profile of the currently authenticated user, including their assigned roles and organization memberships.
{
"query": "query Me { me { id email name status timezone language mfaEnabled lastLoginAt createdAt roles { role { id name } organization { id name } } organizations { id name status } } }"
}
{
"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
Fetch a single user by their unique ID.
{
"query": "query GetUser($id: ID!) { user(id: $id) { id email name status createdAt roles { role { id name } } } }",
"variables": {
"id": "usr_01abc"
}
}
{
"data": {
"user": {
"id": "usr_01abc",
"email": "user@example.com",
"name": "Jane Smith",
"status": "ACTIVE",
"createdAt": "2025-01-01T00:00:00.000Z",
"roles": []
}
}
}
users
List users with optional filtering and pagination. Supports searching by name or email and filtering by status.
{
"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"
}
}
}
{
"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
Fetch a single organization by ID, including its owner, locations, and current status.
{
"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"
}
}
{
"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
List organizations with optional filtering by status or search term.
{
"query": "query ListOrganizations($filter: OrganizationFilter) { organizations(filter: $filter) { id name legalName shortcode status createdAt } }",
"variables": {
"filter": {
"status": "ACTIVE",
"search": "acme"
}
}
}
{
"data": {
"organizations": [
{
"id": "org_01xyz",
"name": "Acme Corp",
"legalName": "Acme Corporation Inc.",
"shortcode": "acme",
"status": "ACTIVE",
"createdAt": "2025-01-01T00:00:00.000Z"
}
]
}
}
createUser
Create a new user account. Requires the users.manage permission. Returns the created user object.
{
"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"
}
}
}
{
"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
Update fields on an existing user by ID. Only the fields provided in input will be changed. Requires the users.write permission.
{
"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"
}
}
}
{
"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
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.
{
"query": "mutation DeleteUser($id: ID!) { deleteUser(id: $id) }",
"variables": {
"id": "usr_02xyz"
}
}
{
"data": {
"deleteUser": true
}
}
createOrganization
Create a new organization. The authenticated user is automatically set as the owner. Returns the created organization with owner details.
{
"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"
}
}
}
{
"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
Fetch a single role by ID, including its full list of permissions.
{
"query": "query GetRole($id: ID!) { role(id: $id) { id name description status systemRole isDefault permissions { id action resource } } }",
"variables": {
"id": "role-1"
}
}
{
"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
List all roles available in the system.
{
"query": "query ListRoles { roles { id name description status systemRole isDefault } }"
}
{
"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
Assign a role to a user within the context of a specific organization. Requires the roles.manage permission. Returns the created UserRole record.
{
"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"
}
}
}
{
"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.
List available roles — note the id of the role you want to assign
{
"query": "query ListRoles { roles { id name description status isDefault } }"
}
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"
}
}
}
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"
}
}
}
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
| Field | Type | Description |
|---|---|---|
| id | ID! | Unique user identifier |
| String! | Primary email address | |
| emailVerifiedAt | Time | When the email was verified; null if unverified |
| name | String! | Display name |
| avatar | String | URL to profile image |
| timezone | String | IANA timezone string (e.g. America/Toronto) |
| language | String | BCP-47 language code (e.g. en) |
| status | UserStatus! | Account status |
| mfaEnabled | Boolean! | Whether MFA is active |
| lastLoginAt | Time | Most recent successful login timestamp |
| createdAt | Time! | Account creation timestamp |
| updatedAt | Time! | 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.
| Field | Type | Description |
|---|---|---|
| id | ID! | Unique organization identifier |
| name | String! | Display name |
| legalName | String | Legal registered name |
| shortcode | String | Short identifier slug |
| status | OrganizationStatus! | Organization status |
| owner | User! | Owning user |
| locations | [Location!]! | Organization locations |
| createdAt | Time! | Creation timestamp |
| updatedAt | Time! | Last update timestamp |
Location
| Field | Type | Description |
|---|---|---|
| id | ID! | Unique location identifier |
| name | String! | Location name |
| brandName | String | Customer-facing brand name |
| organization | Organization | Parent organization |
| status | LocationStatus! | Location status |
| createdAt | Time! | Creation timestamp |
| updatedAt | Time! | Last update timestamp |
AuthPayload
| Field | Type | Description |
|---|---|---|
| accessToken | String! | Short-lived JWT for authenticating requests |
| refreshToken | String! | Long-lived token used to obtain new access tokens |
| user | User! | The authenticated user object |
UsersConnection
| Field | Type | Description |
|---|---|---|
| users | [User!]! | Array of users for the current page |
| total | Int! | Total number of matching records |
| page | Int! | Current page number (1-based) |
| pageSize | Int! | Number of results per page |
| totalPages | Int! | Total number of pages |
CreateUserInput
| Field | Type | Required | Description |
|---|---|---|---|
| String | Required | Email address for the new account | |
| password | String | Required | Initial plaintext password (hashed server-side) |
| name | String | Required | User's display name |
| timezone | String | Optional | IANA timezone, defaults to UTC |
| language | String | Optional | BCP-47 code, defaults to en |
| status | UserStatus | Optional | Defaults to PENDING |
RegisterInput
| Field | Type | Required | Description |
|---|---|---|---|
| String! | Required | Email address | |
| password | String! | Required | Password chosen by the user |
| name | String | Optional | Display name |
| timezone | String | Optional | IANA timezone string |
| language | String | Optional | ISO 639-1 language code |
| autoVerify | Boolean | Optional | Mark email verified immediately when allowed by the caller's flow |
UpdateUserInput
| Field | Type | Description |
|---|---|---|
| name | String | Update display name |
| status | UserStatus | Update account status |
| timezone | String | Update timezone |
| language | String | Update language preference |
UserFilter
| Field | Type | Description |
|---|---|---|
| status | UserStatus | Filter by account status |
| organizationId | ID | Filter users belonging to an organization |
| locationId | ID | Filter users assigned to a location |
| roleId | ID | Filter users holding a role |
| search | String | Search across name and email |
PaginationInput
| Field | Type | Description |
|---|---|---|
| page | Int | Page number, 1-based |
| pageSize | Int | Number of records per page |
| sortBy | String | Field name to sort by |
| sortOrder | SortOrder | ASC or DESC |
CreateOrganizationInput
| Field | Type | Required | Description |
|---|---|---|---|
| name | String! | Required | Organization display name |
| legalName | String | Optional | Full legal name |
| shortcode | String | Optional | Short identifier slug |
| status | OrganizationStatus | Optional | Initial status |
OrganizationFilter
| Field | Type | Description |
|---|---|---|
| status | OrganizationStatus | Filter by organization status |
| ownerId | ID | Filter by owner user ID |
| search | String | Free-text search on name |
AssignRoleInput
| Field | Type | Required | Description |
|---|---|---|---|
| userId | ID | Required | ID of the user to receive the role |
| roleId | ID | Required | ID of the role to assign |
| organizationId | ID | Required | Organization context for the role assignment |
Role
| Field | Type | Description |
|---|---|---|
| id | ID! | Unique role identifier |
| name | String! | Human-readable role name |
| description | String | Optional description of the role |
| status | String! | Role status (ACTIVE / INACTIVE) |
| systemRole | Boolean! | Whether the role is managed by the system |
| isDefault | Boolean! | Whether this role is auto-assigned to new users |
| permissions | [Permission!]! | List of permissions granted by this role |
UserRole
| Field | Type | Description |
|---|---|---|
| id | ID! | Unique assignment identifier |
| status | String! | Assignment status |
| grantedAt | Time! | When the role was granted |
| role | Role! | The assigned role |
| organization | Organization! | The organization context |
Permission
| Field | Type | Description |
|---|---|---|
| id | ID! | Unique permission identifier |
| name | String! | Human-readable permission name |
| action | String! | Canonical action clients should check, such as users.manage |
| resource | String! | Resource family, such as users |
| description | String | Optional description |
| product | Product | Associated product/module metadata |
Product
This is product/module metadata used by permissions, not a restaurant menu item type.
| Field | Type | Description |
|---|---|---|
| id | ID! | Unique product identifier |
| name | String! | Product/module name |
| description | String | Optional description |
| type | ProductType! | Product category |
| status | ProductStatus! | Lifecycle status |
| active | Boolean! | Whether the product is active |
| systemRole | Boolean! | Whether managed by the system |
Enums
| Value | Description |
|---|---|
| ACTIVE | Account is active and can authenticate |
| PENDING | Account created, awaiting verification |
| SUSPENDED | Access temporarily revoked |
| ARCHIVED | Soft-deleted, not recoverable via API |
| Value | Description |
|---|---|
| ACTIVE | Organization is operational |
| INACTIVE | Organization is dormant |
| SUSPENDED | Access temporarily restricted |
| ARCHIVED | Soft-deleted organization |
2026-03-19T10:00:00.000Z
Frontend Auth Flow
Use login -> me -> choose organization -> send scoped inputs as the browser-client bootstrap sequence.
login or register and store both tokens.me with Authorization: Bearer <access_token>.roles, role.permissions, and organizations.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.
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
| Code | HTTP Status | Description |
|---|---|---|
| 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.
x-request-id
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 } }"}'
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.