This document describes the multi-schema GraphQL support in Saleor Dashboard, which allows the application to work with both production (main) and staging (staging) API schemas.
The dashboard supports dual GraphQL schemas:
- Production Schema (main): The default schema used in production environments
- Staging Schema (staging): Optional schema for testing upcoming API changes
Both schemas are generated at build time, and runtime selection is controlled by the FF_USE_STAGING_SCHEMA feature flag.
schema-main.graphql # Production schema (Saleor stable tags like 3.22, 3.23)
schema-staging.graphql # Staging schema (Saleor staging - main branch)
schema.graphql # Symlink to schema-main.graphql (for tooling compatibility)
Each schema generates its own set of TypeScript files:
src/graphql/
├── hooks.generated.ts # Hooks from main schema
├── types.generated.ts # Types from main schema
├── typePolicies.generated.ts # Type policies from main schema
├── fragmentTypes.generated.ts # Fragment types from main schema
├── hooksStaging.generated.ts # Hooks from staging schema
├── typesStaging.generated.ts # Types from staging schema
├── typePoliciesStaging.generated.ts # Type policies from staging schema
├── fragmentTypesStaging.generated.ts # Fragment types from staging schema
└── staging/
└── index.ts # Convenience export for all staging files
Add this variable to your .env file:
# Enable staging schema (default: false)
FF_USE_STAGING_SCHEMA=falseThe same API_URL is used for both schema versions. The FF_USE_STAGING_SCHEMA flag controls which schema types and hooks are used in the application.
By default, the application imports from the production schema:
// Default import uses production schema (main)
import { useProductListQuery } from "@dashboard/graphql";To explicitly use the staging schema, you have two options:
Option 1: Import from the staging directory (recommended for multiple imports)
// Import from staging directory - includes all staging types, hooks, and helpers
import { useProductListQuery, ProductListQuery, isStagingSchema } from "@dashboard/graphql/staging";Option 2: Import directly from generated files
// Import directly from specific generated files
import { useProductListQuery } from "@dashboard/graphql/hooksStaging.generated";
import type { ProductListQuery } from "@dashboard/graphql/typesStaging.generated";Use the schema version helpers to check which schema is active:
import { isStagingSchema, getSchemaVersion } from "@dashboard/graphql";
function MyComponent() {
// Check if staging schema is enabled
if (isStagingSchema()) {
// Use staging-specific features
}
// Or get the version string
const version = getSchemaVersion(); // "main" or "staging"
}For features that differ between schemas, you can conditionally use different hooks:
Example 1: Skip-based approach (recommended)
import { isMainSchema, isStagingSchema, useProductListQuery } from "@dashboard/graphql";
import { useProductListQuery as useProductListQueryStaging } from "@dashboard/graphql/staging";
function ProductList() {
// Execute only the relevant query based on schema version
const { data: dataMain } = useProductListQuery({
skip: isStagingSchema()
});
const { data: dataStaging } = useProductListQueryStaging({
skip: isMainSchema()
});
// Use whichever data is available
const data = dataStaging ?? dataMain;
return <div>{/* Use data */}</div>;
}Example 2: Dynamic hook selection
import { isStagingSchema } from "@dashboard/graphql";
import { useProductListQuery as useProductListQueryMain } from "@dashboard/graphql";
import { useProductListQuery as useProductListQueryStaging } from "@dashboard/graphql/staging";
function ProductList() {
// Choose the appropriate hook based on schema version
const useProductList = isStagingSchema() ? useProductListQueryStaging : useProductListQueryMain;
const { data } = useProductList({
variables: { /* ... */ }
});
return <div>{/* ... */}</div>;
}For queries that only exist in one schema version, you can organize them in separate files:
src/products/
├── queries.ts # Main schema queries
└── queries.staging.ts # Staging-specific queries
# Fetch both schemas
pnpm run fetch-schema
# Fetch individual schemas
pnpm run fetch-schema:main
pnpm run fetch-schema:staging# Generate types for both schemas
pnpm run generate
# Generate for individual schemas
pnpm run generate:main
pnpm run generate:stagingThe standard type checking commands work with the multi-schema setup:
# Type check with both schemas
pnpm run check-types- Both schemas are fetched from the Saleor repository
- GraphQL Codegen generates separate TypeScript files for each schema
- Production schema generates base types (no suffix)
- Staging schema generates Staging-suffixed document variables
FF_USE_STAGING_SCHEMAfeature flag determines which schema is active- Apollo Client loads the appropriate
fragmentTypesbased on the flag getApiUrl()returns the same API URL regardless of the flag- Application code imports hooks from the appropriate generated file
- Hook names are identical in both versions (e.g.,
useProductListQuery) - Type names are identical in both versions (e.g.,
ProductListQuery) - GraphQL document variables have Staging suffix in the staging version (e.g.,
ProductListStaging)
This means you cannot import both versions in the same file without aliasing:
// This works - import with aliases (recommended: use staging directory)
import { useProductListQuery as useProductListQueryMain } from "@dashboard/graphql";
import { useProductListQuery as useProductListQueryStaging } from "@dashboard/graphql/staging";
// This also works - import directly from generated files
import { useProductListQuery as useProductListQueryMain } from "@dashboard/graphql/hooks.generated";
import { useProductListQuery as useProductListQueryStaging } from "@dashboard/graphql/hooksStaging.generated";
// This doesn't work - naming conflict
import { useProductListQuery } from "@dashboard/graphql";
import { useProductListQuery } from "@dashboard/graphql/staging"; // ERROR!The src/graphql/staging/index.ts file provides a convenient way to import all staging-related exports:
// Instead of importing from multiple files...
import { useProductListQuery } from "@dashboard/graphql/hooksStaging.generated";
import type { ProductListQuery } from "@dashboard/graphql/typesStaging.generated";
import { isStagingSchema } from "@dashboard/graphql/schemaVersion";
// You can import everything from one place
import { useProductListQuery, ProductListQuery, isStagingSchema } from "@dashboard/graphql/staging";This export includes:
- All staging hooks (
hooksStaging.generated.ts) - All staging types (
typesStaging.generated.ts) - Type policies (
typePoliciesStaging.generated.ts) - Fragment types (
fragmentTypesStaging.generated.ts) - Schema version helpers (
schemaVersion.ts) - Extended types (
extendedTypes.ts)
The application uses a single Apollo Client instance that connects to the configured API_URL. The same API endpoint is used for both schema versions - only the client-side schema types and fragmentTypes differ based on the FF_USE_STAGING_SCHEMA flag.
When FF_USE_STAGING_SCHEMA=false (default):
- Uses production schema (main)
- Apollo Client uses main fragmentTypes
- All imports from
@dashboard/graphqluse production types
When FF_USE_STAGING_SCHEMA=true:
- Uses staging schema (staging)
- Apollo Client uses staging fragmentTypes
- Must explicitly import from Staging generated files for schema-specific features
- Connects to the same API_URL (you must know what schema version is provided by this endpoint)
- Add your query/mutation to the appropriate file (e.g.,
src/products/queries.ts) - Run
pnpm run generateto generate hooks for both schemas - Import from the default export for production schema:
import { useMyNewQuery } from "@dashboard/graphql";
- For staging-specific features, import from the staging directory:
import { useMyNewQuery } from "@dashboard/graphql/staging";
If a query uses fields that only exist in the staging schema:
- Create a separate file (e.g.,
src/products/queries.staging.ts) - Define your staging-specific query there
- Run
pnpm run generate:stagingto generate hooks - Import from the staging directory:
import { useMyNewQuery } from "@dashboard/graphql/staging";
This keeps schema-specific code organized and prevents type errors when generating main schema types.
- Set
FF_USE_STAGING_SCHEMA=falsein your.env - Start the dev server:
pnpm run dev - Test production schema (main) behavior
Then:
- Set
FF_USE_STAGING_SCHEMA=truein your.env - Restart the dev server
- Test staging schema (staging) behavior
Note: Your API must support both schema versions for this to work correctly.
- Both schemas must generate successfully for builds to pass
- Type checking validates both schema versions
- Consider running E2E tests against both schemas
- Differential Generation: Generate only changed types for Staging to reduce bundle size (this should not be a huge deal if we migrate from enums to type literals)
- Automatic Schema Switching: Detect API version from response headers
- Schema Compatibility Checker: Tool to validate backward compatibility
- Runtime Type Validation: Validate API responses match the active schema
As new schema versions are released:
- Update
fetch-schema:stagingscript to fetch the new version - Run
pnpm run generate - Fix any type errors in application code
- Test thoroughly with the new schema
- When ready, promote staging to main by updating
fetch-schema:mainto point to the new version