Complete reference documentation for the GitStore GraphQL API.
- Overview
- GraphQL Endpoint
- Authentication
- Query Operations
- Mutation Operations
- Types
- Scalars
- Enums
- Filtering and Pagination
- Error Handling
- Examples
- Controller Watch Stream (Proposal)
- Versioning
GitStore provides a GraphQL API following the Relay specification for:
- Queries: Read operations for products, categories, collections
- Mutations: Write operations for managing catalog entities
- Connections: Cursor-based pagination for list queries
- Node interface: Global object identification
- URL:
http://localhost:4000/graphql - Playground:
http://localhost:4000/playground - Method: POST
- Content-Type:
application/json
Read operations are public unless a resolver documents otherwise. Protected mutations require a JWT bearer token in the Authorization header:
Authorization: Bearer <token>Obtain a token with the GraphQL login mutation:
mutation {
login(input: { username: "admin", password: "<password>" }) {
session {
token
expiresAt
user {
username
isAdmin
}
}
}
}Namespace create and delete mutations require authentication. Creating ENTERPRISE namespaces requires session.user.isAdmin == true.
All GraphQL types that implement Node expose opaque global IDs. The ID format is base64-encoded gid://GitStore/{NodeType}/{rawID}. For example, product raw ID 123 is returned as Z2lkOi8vR2l0U3RvcmUvUHJvZHVjdC8xMjM=.
Clients should treat these values as opaque and pass them back unchanged to node, nodes, lookup selectors such as product(by: {id: ...}), filters, and mutation fields typed as ID. Business identifiers such as product(by: {sku: ...}), category(by: {slug: ...}), collection(by: {slug: ...}), namespace(by: {identifier: ...}), and namespace parentEnterpriseIdentifier are not global IDs.
Fetch any object by its globally unique ID (Relay Node interface).
query {
node(id: "Z2lkOi8vR2l0U3RvcmUvUHJvZHVjdC8xMjM=") {
id
... on Product {
title
price
}
}
}Arguments:
id: ID!- Globally unique identifier
Returns: Node (can be cast to Product, Category, Collection, or Namespace)
Fetch multiple objects by their IDs.
query {
nodes(ids: ["Z2lkOi8vR2l0U3RvcmUvUHJvZHVjdC8xMjM=", "Z2lkOi8vR2l0U3RvcmUvQ2F0ZWdvcnkvMTIz"]) {
id
... on Product {
title
}
... on Category {
name
}
}
}Arguments:
ids: [ID!]!- Array of globally unique identifiers
Returns: [Node]!
Get a namespace by exactly one unique selector: id or identifier.
query {
namespace(by: {identifier: "acme-corp"}) {
id
identifier
displayName
tier
parentEnterpriseId
createdAt
createdBy
updatedAt
updatedBy
}
}Arguments:
by: NamespaceBy!- One ofid(global ID) oridentifier
Returns: Namespace (nullable)
Get a single product by exactly one unique selector: id or sku.
query {
product(by: {sku: "MBP-16-M3-2024"}) {
id
title
price
currency
}
}Arguments:
by: ProductBy!- One ofid(global ID) orsku
Returns: Product (nullable)
List products with filtering and cursor-based pagination.
query {
products(
first: 10
after: "cursor_abc"
filter: {
categoryId: "Z2lkOi8vR2l0U3RvcmUvQ2F0ZWdvcnkvMTIz"
priceMin: "100"
priceMax: "5000"
inventoryStatus: IN_STOCK
}
) {
edges {
cursor
node {
id
title
price
}
}
pageInfo {
hasNextPage
endCursor
}
totalCount
}
}Arguments:
first: Int- Number of items to return (forward pagination)after: String- Cursor to paginate afterlast: Int- Number of items to return (backward pagination)before: String- Cursor to paginate beforefilter: ProductFilter- Filter criteria
Returns: ProductConnection!
Get a category by exactly one unique selector: id or slug.
query {
category(by: {slug: "electronics"}) {
id
name
children {
name
}
}
}Arguments:
by: CategoryBy!- One ofid(global ID) orslug
Returns: Category (nullable)
Get categories in hierarchical structure with Relay cursor-based pagination.
query {
categories(first: 20) {
edges {
cursor
node {
id
name
displayOrder
parent {
name
}
children {
name
}
}
}
pageInfo {
hasNextPage
endCursor
}
totalCount
}
}Returns: CategoryConnection!
Get a collection by exactly one unique selector: id or slug.
query {
collection(by: {slug: "featured"}) {
id
name
products {
edges {
node {
title
}
}
}
}
}Arguments:
by: CollectionBy!- One ofid(global ID) orslug
Returns: Collection (nullable)
Get collections with Relay cursor-based pagination.
query {
collections(first: 20) {
edges {
cursor
node {
id
name
slug
displayOrder
}
}
pageInfo {
hasNextPage
endCursor
}
totalCount
}
}Returns: CollectionConnection!
Get the current catalog version (latest release tag).
query {
catalogVersion {
tag
commit
publishedAt
message
}
}Returns: CatalogVersion!
Authenticate and return a JWT session.
mutation {
login(input: { username: "admin", password: "<password>" }) {
session {
token
expiresAt
user {
username
isAdmin
}
}
}
}Input Fields:
username: String!- Configured admin usernamepassword: String!- Configured admin passwordclientMutationId: String- Client-side mutation tracking
Returns: LoginPayload!
Create a namespace. Requires authentication; ENTERPRISE requires an admin token.
mutation {
createNamespace(
input: {
clientMutationId: "create-acme-corp"
identifier: "acme-corp"
displayName: "Acme Corporation"
tier: USER
}
) {
clientMutationId
namespace {
id
identifier
displayName
tier
createdAt
createdBy
}
}
}Input Fields:
clientMutationId: String- Client-side mutation trackingidentifier: String!- Globally unique namespace identifierdisplayName: String- Optional human-friendly display nametier: NamespaceTier!-USER,ORGANISATION, orENTERPRISEparentEnterpriseIdentifier: String- Optional parent enterprise identifier forORGANISATION
Returns: CreateNamespacePayload!
Delete an empty namespace. Requires the namespace owner or an admin token.
mutation {
deleteNamespace(
input: {
clientMutationId: "delete-acme-corp"
identifier: "acme-corp"
}
) {
clientMutationId
deletedIdentifier
}
}Input Fields:
clientMutationId: String- Client-side mutation trackingidentifier: String!- Namespace identifier to delete
Returns: DeleteNamespacePayload!
Create a new product.
mutation {
createProduct(
input: {
title: "New Product"
sku: "PROD-001"
price: "99.99"
currency: "USD"
categoryId: "Z2lkOi8vR2l0U3RvcmUvQ2F0ZWdvcnkvMTIz"
inventoryStatus: IN_STOCK
inventoryQuantity: 50
clientMutationId: "create-product-1"
}
) {
clientMutationId
product {
id
title
}
}
}Input Fields:
title: String!- Product namesku: String!- Stock Keeping Unit (must be unique)price: Decimal!- Product pricecurrency: String!- ISO currency codecategoryId: ID!- Category global IDbody: String- Product description (markdown)collectionIds: [ID!]- Collection global IDs to add product toimages: [String!]- Array of image URLsinventoryStatus: InventoryStatus- Stock statusinventoryQuantity: Int- Available quantitymetadata: JSON- Custom attributesclientMutationId: String- Client-side mutation tracking
Returns: CreateProductPayload!
Update an existing product.
mutation {
updateProduct(
input: {
id: "Z2lkOi8vR2l0U3RvcmUvUHJvZHVjdC8xMjM="
title: "Updated Title"
price: "3599.00"
clientMutationId: "update-product-1"
}
) {
clientMutationId
product {
id
title
price
updatedAt
}
conflict {
field
localValue
remoteValue
}
}
}Input Fields:
id: ID!- Product global ID- All other fields optional (only provided fields are updated)
Returns: UpdateProductPayload! with optional conflict field for concurrent edit detection
Delete a product.
mutation {
deleteProduct(
input: {
id: "Z2lkOi8vR2l0U3RvcmUvUHJvZHVjdC8xMjM="
clientMutationId: "delete-product-1"
}
) {
clientMutationId
deletedProductId
}
}Input Fields:
id: ID!- Product global ID to deleteclientMutationId: String
Returns: DeleteProductPayload!
Create a new category.
mutation {
createCategory(
input: {
name: "Laptops"
slug: "laptops"
parentId: "Z2lkOi8vR2l0U3RvcmUvQ2F0ZWdvcnkvMTIz"
displayOrder: 1
clientMutationId: "create-category-1"
}
) {
clientMutationId
category {
id
name
parent {
name
}
}
}
}Input Fields:
name: String!- Category nameslug: String!- URL-friendly identifierbody: String- Description (markdown)parentId: ID- Parent category global ID for hierarchydisplayOrder: Int- Sort orderclientMutationId: String
Returns: CreateCategoryPayload!
Update an existing category.
mutation {
updateCategory(
input: {
id: "Z2lkOi8vR2l0U3RvcmUvQ2F0ZWdvcnkvMTIz"
name: "Electronics & Gadgets"
displayOrder: 0
}
) {
category {
id
name
displayOrder
}
}
}Returns: UpdateCategoryPayload!
Delete a category.
mutation {
deleteCategory(
input: {
id: "Z2lkOi8vR2l0U3RvcmUvQ2F0ZWdvcnkvMTIz"
}
) {
deletedCategoryId
}
}Returns: DeleteCategoryPayload!
Reorder categories by providing new display order.
mutation {
reorderCategories(
input: {
orderedIds: [
"Z2lkOi8vR2l0U3RvcmUvQ2F0ZWdvcnkvMTIz",
"Z2lkOi8vR2l0U3RvcmUvQ2F0ZWdvcnkvMTI0",
"Z2lkOi8vR2l0U3RvcmUvQ2F0ZWdvcnkvMTI1"
]
}
) {
categories {
id
displayOrder
}
}
}Returns: ReorderCategoriesPayload!
Create a new collection.
mutation {
createCollection(
input: {
name: "Summer Sale"
slug: "summer-sale"
displayOrder: 2
}
) {
collection {
id
name
}
}
}Returns: CreateCollectionPayload!
Update an existing collection.
mutation {
updateCollection(
input: {
id: "Z2lkOi8vR2l0U3RvcmUvQ29sbGVjdGlvbi8xMjM="
name: "Featured Items"
}
) {
collection {
id
name
updatedAt
}
}
}Returns: UpdateCollectionPayload!
Delete a collection.
mutation {
deleteCollection(
input: {
id: "Z2lkOi8vR2l0U3RvcmUvQ29sbGVjdGlvbi8xMjM="
}
) {
deletedCollectionId
}
}Returns: DeleteCollectionPayload!
Reorder collections.
mutation {
reorderCollections(
input: {
orderedIds: [
"Z2lkOi8vR2l0U3RvcmUvQ29sbGVjdGlvbi8xMjM=",
"Z2lkOi8vR2l0U3RvcmUvQ29sbGVjdGlvbi8xMjQ=",
"Z2lkOi8vR2l0U3RvcmUvQ29sbGVjdGlvbi8xMjU="
]
}
) {
collections {
id
displayOrder
}
}
}Returns: ReorderCollectionsPayload!
Commit changes and create a release tag.
mutation {
publishCatalog(
input: {
version: "v1.0.5"
message: "Add summer collection products"
}
) {
catalogVersion {
tag
commit
publishedAt
}
}
}Input Fields:
version: String!- Release tag (e.g., "v1.0.5")message: String!- Commit message
Returns: PublishCatalogPayload!
type Product implements Node {
id: ID!
sku: String!
title: String!
body: String
price: Decimal!
currency: String!
category: Category!
collections: [Collection!]!
images: [String!]!
inventoryStatus: InventoryStatus!
inventoryQuantity: Int
metadata: JSON
createdAt: DateTime!
updatedAt: DateTime!
}type Category implements Node {
id: ID!
name: String!
slug: String!
body: String
parent: Category
children: [Category!]!
displayOrder: Int!
products(
first: Int
after: String
last: Int
before: String
): ProductConnection!
createdAt: DateTime!
updatedAt: DateTime!
}type Collection implements Node {
id: ID!
name: String!
slug: String!
body: String
displayOrder: Int!
products(
first: Int
after: String
last: Int
before: String
): ProductConnection!
createdAt: DateTime!
updatedAt: DateTime!
}Relay-style connections for cursor-based pagination.
type ProductConnection {
edges: [ProductEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type ProductEdge {
cursor: String!
node: Product!
}
type CategoryConnection {
edges: [CategoryEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type CategoryEdge {
cursor: String!
node: Category!
}
type CollectionConnection {
edges: [CollectionEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type CollectionEdge {
cursor: String!
node: Collection!
}
type NamespaceConnection {
edges: [NamespaceEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type NamespaceEdge {
cursor: String!
node: Namespace!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}type CatalogVersion {
tag: String!
commit: String!
publishedAt: DateTime!
message: String
stats: CatalogStats
}
type CatalogStats {
totalProducts: Int!
totalCategories: Int!
totalCollections: Int!
}Used for optimistic locking in update mutations.
type ConflictInfo {
field: String!
localValue: String!
remoteValue: String!
timestamp: DateTime!
}String-based decimal type for precise price representation.
scalar DecimalExample: "99.99", "1299.00"
Why string? JavaScript's Number type loses precision for decimal values. Storing prices as strings preserves exact values.
ISO 8601 formatted date-time string.
scalar DateTimeExample: "2026-01-15T10:00:00Z"
Flexible JSON object for metadata.
scalar JSONExample:
{
"brand": "Apple",
"processor": "M3 Max",
"warranty_months": 12
}enum InventoryStatus {
IN_STOCK
OUT_OF_STOCK
PREORDER
DISCONTINUED
}input ProductFilter {
categoryId: ID
collectionId: ID
inventoryStatus: InventoryStatus
priceMin: Decimal
priceMax: Decimal
search: String
}Filter Examples:
By category:
filter: { categoryId: "Z2lkOi8vR2l0U3RvcmUvQ2F0ZWdvcnkvMTIz" }By collection:
filter: { collectionId: "Z2lkOi8vR2l0U3RvcmUvQ29sbGVjdGlvbi8xMjM=" }By price range:
filter: { priceMin: "100", priceMax: "500" }By inventory status:
filter: { inventoryStatus: IN_STOCK }Multiple filters (AND logic):
filter: {
categoryId: "Z2lkOi8vR2l0U3RvcmUvQ2F0ZWdvcnkvMTIz"
priceMax: "1000"
inventoryStatus: IN_STOCK
}Forward pagination (first N items):
products(first: 10) {
edges {
cursor
node { title }
}
pageInfo {
hasNextPage
endCursor
}
}Next page:
products(first: 10, after: "cursor_from_previous_query") {
# ...
}Backward pagination (last N items):
products(last: 10, before: "cursor") {
# ...
}GraphQL errors follow the standard format:
{
"errors": [
{
"message": "Product with SKU 'INVALID-SKU' not found",
"path": ["product"],
"extensions": {
"code": "NOT_FOUND"
}
}
],
"data": {
"product": null
}
}NOT_FOUND- Requested resource doesn't existVALIDATION_ERROR- Input validation failedCONFLICT- Concurrent modification detectedINTERNAL_ERROR- Server error
Queries that fetch single entities return null if not found:
query {
product(by: {sku: "NONEXISTENT"}) {
id # Returns null if product not found
}
}Check for null before accessing nested fields:
const result = await client.query({ query: GET_PRODUCT });
if (result.data.product) {
console.log(result.data.product.title);
} else {
console.log('Product not found');
}query GetProductDetails($sku: String!) {
product(by: {sku: $sku}) {
id
sku
title
body
price
currency
images
inventoryStatus
inventoryQuantity
metadata
category {
id
name
slug
parent {
name
}
}
collections {
id
name
slug
}
createdAt
updatedAt
}
}query ListProducts(
$first: Int!
$after: String
$categoryId: ID
$priceMin: Decimal
$priceMax: Decimal
) {
products(
first: $first
after: $after
filter: {
categoryId: $categoryId
priceMin: $priceMin
priceMax: $priceMax
inventoryStatus: IN_STOCK
}
) {
edges {
cursor
node {
id
sku
title
price
currency
images
category {
name
}
}
}
pageInfo {
hasNextPage
endCursor
}
totalCount
}
}query GetCategoryHierarchy {
categories(first: 50) {
edges {
node {
id
name
displayOrder
parent {
id
name
}
children {
id
name
displayOrder
}
products(first: 5) {
totalCount
edges {
node {
title
}
}
}
}
}
}
}mutation CreateProductComplete($input: CreateProductInput!) {
createProduct(input: $input) {
clientMutationId
product {
id
sku
title
price
category {
name
}
collections {
name
}
}
}
}
# Variables:
{
"input": {
"title": "Wireless Mouse",
"sku": "MOUSE-WIRELESS-001",
"price": "29.99",
"currency": "USD",
"categoryId": "Z2lkOi8vR2l0U3RvcmUvQ2F0ZWdvcnkvMTIz",
"collectionIds": ["Z2lkOi8vR2l0U3RvcmUvQ29sbGVjdGlvbi8xMjM=", "Z2lkOi8vR2l0U3RvcmUvQ29sbGVjdGlvbi8xMjQ="],
"inventoryStatus": "IN_STOCK",
"inventoryQuantity": 100,
"images": ["https://cdn.example.com/mouse.jpg"],
"metadata": {
"brand": "TechMouse",
"connectivity": "Bluetooth"
},
"clientMutationId": "create-mouse-1"
}
}mutation UpdateProductWithConflictCheck($input: UpdateProductInput!) {
updateProduct(input: $input) {
clientMutationId
product {
id
title
price
updatedAt
}
conflict {
field
localValue
remoteValue
timestamp
}
}
}mutation PublishCatalog {
publishCatalog(
input: {
version: "v1.0.5"
message: "Updated product prices for Q2 2026"
}
) {
catalogVersion {
tag
commit
publishedAt
message
}
}
}GitStore remains GraphQL-first, but controller loops for core resources and CRD kinds need Kubernetes-like watch semantics. The watch stream is exposed as GraphQL subscriptions over HTTP-compatible streaming transport (GraphQL-over-SSE).
- Event types follow
ADDED,MODIFIED, andDELETED. - Each event carries the full reconciled resource (
metadata,.spec,.status). metadata.resourceVersionis monotonic and used as a resume token.
subscription WatchProducts($after: String) {
watchProducts(afterResourceVersion: $after) {
type
resourceVersion
object {
metadata {
uid
resourceVersion
}
spec {
title
price
}
status {
inventory
lastReconciledAt
}
}
}
}Controllers should use a list-then-watch pattern:
- Query current state snapshot.
- Start subscription with
afterResourceVersionfrom the snapshot. - On disconnect, reconnect with the last applied resource version.
- If the server reports the resume point is too old, relist and restart the watch from a fresh snapshot.
- Controllers observe events from the stream.
- Controllers perform side effects out-of-band.
- Controllers write observed state via GraphQL status mutations.
- API persists the new status and emits the next watch event.
The API currently does not enforce rate limits. Future versions will implement rate limiting with the following headers:
X-RateLimit-Limit: Maximum requests per windowX-RateLimit-Remaining: Remaining requests in windowX-RateLimit-Reset: Window reset time (Unix timestamp)
The GraphQL API uses a single endpoint with schema evolution rather than versioned GraphQL paths.
For CRD-style kinds, the platform applies a hub-and-spoke conversion model:
- Each kind has one designated hub version (storage state), such as
gitstore.dev/v2. - Inbound manifests using older versions are converted to the hub version during the write pipeline.
- KV projections and core synthesised GraphQL types reflect hub-version shape.
When a kind introduces a breaking version, the owner provides WASI conversion hooks:
- Upgrade conversion (for example
v1 -> v2) - Downgrade conversion (for example
v2 -> v1)
Write-time flow:
- Client pushes a resource with non-hub
apiVersion. - Orchestrator invokes the conversion hook.
- Converted hub resource is validated and projected.
- Read models remain normalised on hub version.
Backward compatibility is maintained through field deprecation instead of endpoint versioning:
- Keep old fields available during migration windows.
- Mark legacy fields with
@deprecated(reason: "..."). - Resolve deprecated fields from hub state in resolver logic.
Example: if price is replaced by pricingMatrix, schema can expose both fields while clients migrate.
- User Guide - Getting started and usage examples
- GraphQL Playground - Interactive API explorer
- Relay Specification - Pagination and connection patterns