zuplo 6.70.68 → 6.70.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/docs/ai-gateway/getting-started.mdx +12 -8
  2. package/docs/ai-gateway/introduction.mdx +11 -9
  3. package/docs/articles/api-key-buckets.mdx +4 -2
  4. package/docs/articles/archiving-requests-to-storage.mdx +4 -4
  5. package/docs/articles/branch-based-deployments.mdx +10 -8
  6. package/docs/articles/ci-cd-github/cleanup-on-branch-delete.mdx +52 -31
  7. package/docs/articles/ci-cd-github/pr-preview-environments.mdx +17 -6
  8. package/docs/articles/custom-ci-cd-azure.mdx +1 -1
  9. package/docs/articles/custom-ci-cd-bitbucket.mdx +1 -1
  10. package/docs/articles/custom-ci-cd-circleci.mdx +1 -1
  11. package/docs/articles/custom-ci-cd-github.mdx +1 -1
  12. package/docs/articles/custom-ci-cd-gitlab.mdx +1 -1
  13. package/docs/articles/graphql.mdx +276 -0
  14. package/docs/articles/monorepo-deployment.mdx +17 -3
  15. package/docs/articles/opentelemetry.mdx +5 -2
  16. package/docs/articles/per-user-rate-limits-using-db.mdx +5 -6
  17. package/docs/articles/securing-the-gateway-with-client-mtls.mdx +68 -43
  18. package/docs/articles/step-1-setup-basic-gateway.mdx +1 -3
  19. package/docs/articles/step-2-add-rate-limiting.mdx +1 -1
  20. package/docs/articles/testing.mdx +1 -1
  21. package/docs/articles/troubleshooting.md +7 -3
  22. package/docs/articles/waf-ddos-akamai.md +35 -16
  23. package/docs/articles/waf-ddos-aws-waf-shield.mdx +35 -16
  24. package/docs/articles/waf-ddos-fastly.mdx +10 -7
  25. package/docs/cli/deploy.mdx +13 -10
  26. package/docs/cli/deploy.partial.mdx +13 -10
  27. package/docs/dev-portal/zudoku/components/sidecar-box.mdx +131 -0
  28. package/docs/dev-portal/zudoku/configuration/api-catalog.md +62 -42
  29. package/docs/dev-portal/zudoku/configuration/api-reference.md +5 -4
  30. package/docs/dev-portal/zudoku/configuration/navigation.mdx +70 -7
  31. package/docs/guides/canary-routing-for-employees.mdx +103 -39
  32. package/docs/guides/modify-openapi-paths.mdx +3 -3
  33. package/docs/handlers/legacy-dev-portal-handler.mdx +1 -1
  34. package/docs/handlers/mcp-server.mdx +13 -11
  35. package/docs/handlers/url-forward.mdx +5 -1
  36. package/docs/handlers/url-rewrite.mdx +7 -2
  37. package/docs/handlers/websocket-handler.mdx +5 -1
  38. package/docs/mcp-gateway/observability/logging.mdx +19 -12
  39. package/docs/mcp-server/resources.mdx +27 -15
  40. package/docs/mcp-server/testing.mdx +0 -2
  41. package/docs/policies/archive-request-azure-storage-inbound/doc.md +1 -1
  42. package/docs/policies/archive-response-azure-storage-outbound/doc.md +1 -1
  43. package/docs/policies/ip-restriction-inbound/policy.ts +1 -1
  44. package/docs/programmable-api/http-problems.mdx +0 -18
  45. package/docs/programmable-api/jwt-service-plugin.mdx +131 -109
  46. package/docs/programmable-api/runtime-behaviors.mdx +4 -2
  47. package/docs/programmable-api/streaming-zone-cache.mdx +4 -6
  48. package/docs/programmable-api/web-crypto-apis.mdx +10 -6
  49. package/package.json +4 -4
  50. package/docs/errors/get-head-body-error.mdx +0 -41
@@ -9,7 +9,7 @@ initial configuration to making your first LLM request through Zuplo.
9
9
  ## Prerequisites
10
10
 
11
11
  - A Zuplo account (sign up free at [zuplo.com](https://zuplo.com))
12
- - API keys for at least one LLM provider (OpenAI, Anthropic, Google Gemini,
12
+ - API keys for at least one LLM provider (OpenAI, Anthropic, Google, Mistral,
13
13
  etc.)
14
14
  - An application that needs to call LLM APIs
15
15
 
@@ -27,9 +27,9 @@ includes Apps, Teams, and a setup guide to help you get started.
27
27
 
28
28
  ## Step 2: Configure Providers
29
29
 
30
- Providers are the LLM services (like OpenAI or Google Gemini) that your
31
- applications will use. You'll configure these once as an administrator, and your
32
- team members can use them without needing direct access to provider API keys.
30
+ Providers are the LLM services (like OpenAI or Anthropic) that your applications
31
+ will use. You'll configure these once as an administrator, and your team members
32
+ can use them without needing direct access to provider API keys.
33
33
 
34
34
  ### Adding Your First Provider
35
35
 
@@ -43,13 +43,17 @@ team members can use them without needing direct access to provider API keys.
43
43
  ### Adding Additional Providers
44
44
 
45
45
  Repeat the process above to add more providers. This allows your teams to switch
46
- between providers (OpenAI, Gemini, etc.) without changing application code.
46
+ between providers (OpenAI, Anthropic, etc.) without changing application code.
47
47
 
48
48
  **Example providers you might add:**
49
49
 
50
50
  - OpenAI (for GPT models)
51
- - Google Gemini (for Gemini models)
52
- - Additional providers as they become available
51
+ - Anthropic (for Claude models)
52
+ - Google (for Gemini models)
53
+ - Mistral (for Mistral models)
54
+
55
+ See [AI Providers](./providers.mdx) for the full list of supported providers and
56
+ capabilities, including OpenAI-compatible custom providers.
53
57
 
54
58
  ## Step 3: Create a Team
55
59
 
@@ -202,7 +206,7 @@ Now that your AI Gateway is running, explore additional features:
202
206
  ### Switch Providers Without Code Changes
203
207
 
204
208
  1. Go to your app settings
205
- 2. Change the **Provider** dropdown (for example, from OpenAI to Gemini)
209
+ 2. Change the **Provider** dropdown (for example, from OpenAI to Anthropic)
206
210
  3. Select a new model
207
211
  4. Click **Save Changes**
208
212
 
@@ -4,16 +4,17 @@ sidebar_label: Introduction
4
4
  ---
5
5
 
6
6
  Zuplo's AI Gateway acts as an intelligent proxy layer that sits between your
7
- engineering team's applications and LLM providers like OpenAI, Google Gemini,
8
- and others. Instead of your applications communicating directly with these
9
- providers, all requests flow through the Zuplo AI Gateway, which streams
7
+ engineering team's applications and LLM providers like OpenAI, Anthropic,
8
+ Google, and Mistral. Instead of your applications communicating directly with
9
+ these providers, all requests flow through the Zuplo AI Gateway, which streams
10
10
  responses while applying policies, controls, and monitoring.
11
11
 
12
12
  ## Key Benefits
13
13
 
14
- **Provider Independence**: Switch between LLM providers (OpenAI, Google Gemini,
15
- etc.) dynamically without modifying application code. Configure your provider
16
- choice through the gateway rather than hard coding it into your applications.
14
+ **Provider Independence**: Switch between LLM providers (OpenAI, Anthropic,
15
+ Google, Mistral, and more) dynamically without modifying application code.
16
+ Configure your provider choice through the gateway rather than hard coding it
17
+ into your applications.
17
18
 
18
19
  **Cost Control**: Set spending limits at organization, team, and application
19
20
  levels with hierarchical budgets that cascade down through your structure.
@@ -49,9 +50,10 @@ credentials.
49
50
  ### Multi-Provider Support
50
51
 
51
52
  Configure multiple LLM providers within a single gateway project. Supported
52
- providers include OpenAI (GPT-4, GPT-4.5, and other models) and Google Gemini
53
- (all model variants). Select which models are available to your teams when
54
- configuring each provider.
53
+ providers include OpenAI, Anthropic, Google, Mistral, and OpenAI-compatible
54
+ custom providers. See [AI Providers](./providers.mdx) for the full list of
55
+ providers and supported capabilities. Select which models are available to your
56
+ teams when configuring each provider.
55
57
 
56
58
  ### Team Hierarchy & Budgets
57
59
 
@@ -11,10 +11,12 @@ credentials across different environments. Learn more in the
11
11
 
12
12
  Zuplo automatically creates three buckets for each project:
13
13
 
14
- - **Working copy**: Stores API keys for the working-copy environment
15
14
  - **Production**: Stores API keys for the production environment (your default
16
15
  Git branch)
17
- - **Shared**: Stores API keys shared across all other environments
16
+ - **Preview**: Stores API keys shared across all preview environments
17
+ (non-default Git branches)
18
+ - **Development**: Stores API keys for the development (working copy)
19
+ environment
18
20
 
19
21
  For more information on how environments relate to Git branches, see
20
22
  [Branch-Based Deployments](./branch-based-deployments.mdx).
@@ -30,7 +30,7 @@ can generate one of these on the `Shared access tokens` tab.
30
30
  Note, you should minimize the permissions - and select only the `Create`
31
31
  permission. Choose a sensible start and expiration time for your token. Note, we
32
32
  don't recommend restricting IP addresses because Zuplo runs at the edge in over
33
- 200 data-centers world-wide.
33
+ 300 data centers worldwide.
34
34
 
35
35
  ![shared access tokens](../../public/media/guides/archiving-requests-to-storage/Untitled_1.png)
36
36
 
@@ -46,7 +46,7 @@ You'll need another environment variable called `BLOB_CONTAINER_PATH`.
46
46
  We'll write a policy called `request-archive-policy` that can be used on all
47
47
  routes.
48
48
 
49
- ```ts title="file-archive-policy.ts"
49
+ ```ts title="modules/request-archive-policy.ts"
50
50
  import { ZuploRequest, ZuploContext } from "@zuplo/runtime";
51
51
 
52
52
  export type RequestArchivePolicyOptions = {
@@ -102,7 +102,7 @@ example below:
102
102
  "policyType": "code-policy",
103
103
  "handler": {
104
104
  "export": "default",
105
- "module": "$import(./modules/archive-request-policy)",
105
+ "module": "$import(./modules/request-archive-policy)",
106
106
  "options": {
107
107
  "blobCreateSas": "$env(BLOB_CREATE_SAS)",
108
108
  "blobContainerPath": "$env(BLOB_CONTAINER_PATH)"
@@ -111,7 +111,7 @@ example below:
111
111
  }
112
112
  ```
113
113
 
114
- Don't forget to reference the `file-archive-policy` in the policies.inbound
114
+ Don't forget to reference the `request-archive-policy` in the policies.inbound
115
115
  property of your routes.
116
116
 
117
117
  Here's the policy in action:
@@ -34,17 +34,19 @@ bugfix/123 ─────────────────► Preview (b
34
34
  When Zuplo deploys an environment from a branch, the environment name matches
35
35
  the branch name. For example:
36
36
 
37
- | Branch Name | Environment Name | Example URL |
38
- | -------------- | ---------------- | --------------------------------------------------- |
39
- | `main` | main | `https://my-project-main-abc1234.zuplo.app` |
40
- | `staging` | staging | `https://my-project-staging-def5678.zuplo.app` |
41
- | `feature/auth` | feature/auth | `https://my-project-feature-auth-ghi9012.zuplo.app` |
37
+ | Branch Name | Environment Name | Example URL |
38
+ | -------------- | ---------------- | ------------------------------------------------- |
39
+ | `main` | main | `https://my-project-main-abc1234.zuplo.app` |
40
+ | `staging` | staging | `https://my-project-staging-def5678.zuplo.app` |
41
+ | `feature/auth` | feature/auth | `https://my-project-feature-au-ghi9012.zuplo.app` |
42
42
 
43
43
  :::note
44
44
 
45
- The environment URL includes a unique identifier to ensure each deployment has a
46
- distinct address. Configure [custom domains](./custom-domains.mdx) for your
47
- environments.
45
+ Zuplo normalizes the branch name in the deployment URL lowercasing it,
46
+ replacing special characters with hyphens, and truncating it to 10 characters —
47
+ which is why `feature/auth` appears as `feature-au`. The URL also includes a
48
+ unique identifier to ensure each deployment has a distinct address. Configure
49
+ [custom domains](./custom-domains.mdx) for your environments.
48
50
 
49
51
  :::
50
52
 
@@ -34,24 +34,29 @@ jobs:
34
34
  run: |
35
35
  # The deleted branch name
36
36
  BRANCH_NAME="${{ github.event.ref }}"
37
- # Convert slashes to hyphens
38
- ENV_NAME="${BRANCH_NAME//\//-}"
39
-
40
- echo "Deleting environment: $ENV_NAME"
41
-
42
- # Ignore errors if env doesn't exist
43
- npx zuplo delete \
44
- --environment "$ENV_NAME" \
45
- --api-key "$ZUPLO_API_KEY" \
46
- --wait || true
37
+ # Deployment names use the format {project}-{branch}-{hash}, where
38
+ # the branch segment is normalized (lowercase, special characters
39
+ # replaced by hyphens) and truncated to 10 characters
40
+ ENV_NAME=$(echo "$BRANCH_NAME" | tr '/_' '--' |
41
+ tr '[:upper:]' '[:lower:]' | cut -c1-10 | sed 's/-*$//')
42
+
43
+ # Find the deployment for the deleted branch and delete it by URL
44
+ npx zuplo list --api-key "$ZUPLO_API_KEY" \
45
+ --output json --show-details |
46
+ jq -r --arg env "$ENV_NAME" \
47
+ '.[] | select(.name | test("-" + $env + "-[a-z0-9]+$")) | .url' |
48
+ while read -r URL; do
49
+ echo "Deleting environment: $URL"
50
+ npx zuplo delete --url "$URL" --api-key "$ZUPLO_API_KEY" --wait || true
51
+ done
47
52
  ```
48
53
 
49
54
  This workflow:
50
55
 
51
56
  1. Triggers when any branch is deleted
52
- 2. Converts the branch name to the environment name format
53
- 3. Deletes the corresponding Zuplo environment
54
- 4. Continues without error if the environment doesn't exist
57
+ 2. Converts the branch name to the deployment-name branch segment format
58
+ 3. Looks up the matching deployment and deletes it by URL
59
+ 4. Continues without error if no matching environment exists
55
60
 
56
61
  ## Combining with PR Cleanup
57
62
 
@@ -95,24 +100,40 @@ jobs:
95
100
 
96
101
  - name: Cleanup stale environments
97
102
  run: |
98
- # Get all remote branches
99
- BRANCHES=$(git branch -r | sed 's|origin/||' | tr '/' '-' | tr -d ' ')
100
-
101
- # List Zuplo environments and delete stale ones
102
- npx zuplo list --api-key "$ZUPLO_API_KEY" --json > environments.json
103
-
104
- cat environments.json | jq -r '.[] | .name' | while read ENV; do
105
- # Skip protected environments
106
- if [[ "$ENV" == "main" || "$ENV" == "production" || "$ENV" == "staging" ]]; then
107
- continue
108
- fi
109
-
110
- # Delete if no matching branch exists
111
- if ! echo "$BRANCHES" | grep -q "^$ENV$"; then
112
- echo "Deleting stale environment: $ENV"
113
- npx zuplo delete --environment "$ENV" --api-key "$ZUPLO_API_KEY" --wait || true
114
- fi
115
- done
103
+ # Get all remote branches, converted to the deployment-name branch
104
+ # segment format: lowercase, special characters replaced by hyphens,
105
+ # truncated to 10 characters
106
+ BRANCHES=$(git branch -r | sed 's|origin/||' | tr -d ' ' |
107
+ tr '/_' '--' | tr '[:upper:]' '[:lower:]' |
108
+ cut -c1-10 | sed 's/-*$//')
109
+
110
+ # List deployed environments as JSON. Each entry has the shape
111
+ # {"projectName": "...", "name": "...", "url": "..."}
112
+ npx zuplo list --api-key "$ZUPLO_API_KEY" \
113
+ --output json --show-details > environments.json
114
+
115
+ # Deployment names follow the format {project}-{branch}-{hash}
116
+ jq -r '.[] | "\(.name) \(.url)"' environments.json |
117
+ while read -r NAME URL; do
118
+ # Skip protected environments
119
+ case "$NAME" in
120
+ *-main-* | *-production-* | *-staging-*) continue ;;
121
+ esac
122
+
123
+ # Delete only if no branch matches the deployment name
124
+ STALE=true
125
+ for BRANCH in $BRANCHES; do
126
+ if [[ "$NAME" == *"-$BRANCH-"* ]]; then
127
+ STALE=false
128
+ break
129
+ fi
130
+ done
131
+
132
+ if [[ "$STALE" == "true" ]]; then
133
+ echo "Deleting stale environment: $NAME"
134
+ npx zuplo delete --url "$URL" --api-key "$ZUPLO_API_KEY" --wait || true
135
+ fi
136
+ done
116
137
  ```
117
138
 
118
139
  ## Next Steps
@@ -71,15 +71,26 @@ jobs:
71
71
  - name: Install dependencies
72
72
  run: npm install
73
73
 
74
+ - name: Get deployment URL
75
+ id: get-url
76
+ uses: actions/github-script@v7
77
+ with:
78
+ script: |
79
+ const comments = await github.rest.issues.listComments({
80
+ issue_number: context.issue.number,
81
+ owner: context.repo.owner,
82
+ repo: context.repo.repo,
83
+ });
84
+ const match = comments.data
85
+ .map((c) => c.body.match(/Deployed to: (https:\/\/\S+)/))
86
+ .find(Boolean);
87
+ core.setOutput("url", match ? match[1] : "");
88
+
74
89
  - name: Delete environment
90
+ if: steps.get-url.outputs.url != ''
75
91
  run: |
76
- # Environment name is based on branch name
77
- BRANCH_NAME="${{ github.head_ref }}"
78
- # Convert slashes to hyphens (Zuplo convention)
79
- ENV_NAME="${BRANCH_NAME//\//-}"
80
-
81
92
  npx zuplo delete \
82
- --environment "$ENV_NAME" \
93
+ --url "${{ steps.get-url.outputs.url }}" \
83
94
  --api-key "$ZUPLO_API_KEY" \
84
95
  --wait
85
96
  ```
@@ -38,7 +38,7 @@ npx zuplo deploy --api-key "$ZUPLO_API_KEY"
38
38
  npx zuplo test --endpoint https://your-env.zuplo.dev
39
39
 
40
40
  # Clean up environments you no longer need
41
- npx zuplo delete --environment pr-123 --api-key "$ZUPLO_API_KEY"
41
+ npx zuplo delete --url "https://my-api-pr-123-d3vp01.zuplo.app" --api-key "$ZUPLO_API_KEY"
42
42
 
43
43
  # Test locally before deploying
44
44
  npx zuplo dev # starts local server on port 9000
@@ -37,7 +37,7 @@ npx zuplo deploy --api-key "$ZUPLO_API_KEY"
37
37
  npx zuplo test --endpoint https://your-env.zuplo.dev
38
38
 
39
39
  # Clean up environments you no longer need
40
- npx zuplo delete --environment pr-123 --api-key "$ZUPLO_API_KEY"
40
+ npx zuplo delete --url "https://my-api-pr-123-d3vp01.zuplo.app" --api-key "$ZUPLO_API_KEY"
41
41
 
42
42
  # Test locally before deploying
43
43
  npx zuplo dev # starts local server on port 9000
@@ -36,7 +36,7 @@ npx zuplo deploy --api-key "$ZUPLO_API_KEY"
36
36
  npx zuplo test --endpoint https://your-env.zuplo.dev
37
37
 
38
38
  # Clean up environments you no longer need
39
- npx zuplo delete --environment pr-123 --api-key "$ZUPLO_API_KEY"
39
+ npx zuplo delete --url "https://my-api-pr-123-d3vp01.zuplo.app" --api-key "$ZUPLO_API_KEY"
40
40
 
41
41
  # Test locally before deploying
42
42
  npx zuplo dev # starts local server on port 9000
@@ -54,7 +54,7 @@ npx zuplo deploy --api-key "$ZUPLO_API_KEY"
54
54
  npx zuplo test --endpoint https://your-env.zuplo.dev
55
55
 
56
56
  # Clean up environments you no longer need
57
- npx zuplo delete --environment pr-123 --api-key "$ZUPLO_API_KEY"
57
+ npx zuplo delete --url "https://my-api-pr-123-d3vp01.zuplo.app" --api-key "$ZUPLO_API_KEY"
58
58
 
59
59
  # Test locally before deploying
60
60
  npx zuplo dev # starts local server on port 9000
@@ -36,7 +36,7 @@ npx zuplo deploy --api-key "$ZUPLO_API_KEY"
36
36
  npx zuplo test --endpoint https://your-env.zuplo.dev
37
37
 
38
38
  # Clean up environments you no longer need
39
- npx zuplo delete --environment pr-123 --api-key "$ZUPLO_API_KEY"
39
+ npx zuplo delete --url "https://my-api-pr-123-d3vp01.zuplo.app" --api-key "$ZUPLO_API_KEY"
40
40
 
41
41
  # Test locally before deploying
42
42
  npx zuplo dev # starts local server on port 9000
@@ -0,0 +1,276 @@
1
+ ---
2
+ title: GraphQL on Zuplo
3
+ sidebar_label: GraphQL
4
+ description:
5
+ Proxy a GraphQL API through your Zuplo gateway and publish a browsable schema
6
+ reference with a playground in your Dev Portal.
7
+ tags:
8
+ - backends
9
+ ---
10
+
11
+ Zuplo fronts GraphQL APIs the same way it fronts REST: requests pass through a
12
+ gateway route — with whatever authentication, rate limiting, and
13
+ GraphQL-specific policies you attach — and your Dev Portal documents the schema.
14
+ This guide covers both halves: adding a GraphQL endpoint to your gateway routes,
15
+ then rendering a schema reference and playground in the Dev Portal with
16
+ `@zudoku/plugin-graphql`.
17
+
18
+ ## Prerequisites
19
+
20
+ - A Zuplo project
21
+ - An upstream GraphQL API to proxy
22
+ - For the Dev Portal section: your project's
23
+ [Dev Portal](../dev-portal/introduction.mdx) lives in its `docs/` folder. The
24
+ plugin requires the `zudoku` package version `0.80.1` or newer — check the
25
+ version in `docs/package.json` and
26
+ [update the Dev Portal](../dev-portal/updating.mdx) if yours is older.
27
+
28
+ ## Add a GraphQL endpoint to your gateway
29
+
30
+ A GraphQL API serves every query and mutation from a single endpoint, so the
31
+ gateway route uses the [URL Rewrite handler](../handlers/url-rewrite.mdx) —
32
+ every request goes to exactly the upstream URL — rather than URL Forward, which
33
+ appends the incoming path.
34
+
35
+ ### Add a new GraphQL route
36
+
37
+ <Stepper>
38
+
39
+ 1. **Open the Route Designer**
40
+
41
+ In the Zuplo Portal, open the **Code** tab and click **routes.oas.json**.
42
+ This opens the Route Designer, which lists your project's existing routes and
43
+ an **Add** menu.
44
+
45
+ 2. **Add the endpoint**
46
+
47
+ Click **Add** and select **GraphQL Endpoint**. This creates a `POST /graphql`
48
+ route preconfigured with the URL Rewrite handler and a demo upstream. If
49
+ `/graphql` is already taken, the Portal appends a short suffix (for example
50
+ `/graphql-b891`) — edit the path to whatever you prefer. GraphQL routes show
51
+ a **GraphQL** badge in the route list instead of an HTTP method.
52
+
53
+ 3. **Point it at your upstream**
54
+
55
+ Expand the new route. Its handler is preset to **URL Rewrite** in the
56
+ **Request Handler** drop-down; in the URL text box below it, replace the demo
57
+ URL with your GraphQL API's endpoint, for example
58
+ `https://api.example.com/graphql`. To use a different backend per
59
+ environment, reference an [environment variable](./environment-variables.mdx)
60
+ such as `${env.GRAPHQL_API_URL}`.
61
+
62
+ 4. **Save**
63
+
64
+ Save your changes. Your gateway now proxies GraphQL requests at `/graphql`.
65
+
66
+ </Stepper>
67
+
68
+ ### Mark an existing route as GraphQL
69
+
70
+ Already proxying a GraphQL API through a regular route? Open the route in the
71
+ Route Designer, click the **⋯** menu at the end of the route's options row (next
72
+ to the CORS and docs toggles), and check **Mark as GraphQL**. This tags the
73
+ route as a GraphQL endpoint without changing its handler or policies.
74
+
75
+ ### The resulting route configuration
76
+
77
+ Both paths produce a route in `routes.oas.json` with the `x-graphql` extension
78
+ set:
79
+
80
+ ```json title="config/routes.oas.json"
81
+ {
82
+ "paths": {
83
+ "/graphql": {
84
+ "post": {
85
+ "summary": "GraphQL Endpoint",
86
+ "x-graphql": true,
87
+ "x-zuplo-route": {
88
+ "corsPolicy": "none",
89
+ "handler": {
90
+ "export": "urlRewriteHandler",
91
+ "module": "$import(@zuplo/runtime)",
92
+ "options": {
93
+ "rewritePattern": "https://api.example.com/graphql"
94
+ }
95
+ },
96
+ "policies": {
97
+ "inbound": []
98
+ }
99
+ },
100
+ "operationId": "graphql-1a2bc3d4"
101
+ }
102
+ }
103
+ }
104
+ }
105
+ ```
106
+
107
+ :::tip{title="Secure the endpoint"}
108
+
109
+ GraphQL has its own attack surface — deeply nested queries, expensive
110
+ operations, and schema introspection. Zuplo ships policies for all three. See
111
+ [Secure your GraphQL API](./graphql-security.mdx) to add complexity limits and
112
+ disable introspection in production.
113
+
114
+ :::
115
+
116
+ ## Document the API in your Dev Portal
117
+
118
+ The `@zudoku/plugin-graphql` package renders GraphQL APIs in your Dev Portal
119
+ from a schema. Each plugin instance produces a browsable type reference
120
+ (queries, mutations, objects, enums, and so on) plus a playground, generated
121
+ straight from your schema.
122
+
123
+ :::caution{title="Beta"}
124
+
125
+ The GraphQL plugin is in beta. During the beta it isn't available in your
126
+ project's working copy — it only works in projects
127
+ [connected to source control](./source-control.mdx).
128
+
129
+ :::
130
+
131
+ <Stepper>
132
+
133
+ 1. **Install the plugin**
134
+
135
+ In your project's `docs/` folder (where `zudoku.config.tsx` lives), install
136
+ the plugin:
137
+
138
+ ```bash
139
+ npm install @zudoku/plugin-graphql
140
+ ```
141
+
142
+ The plugin requires `zudoku` `0.80.1` or newer.
143
+
144
+ 2. **Add a schema**
145
+
146
+ Drop a GraphQL schema definition language (SDL) file next to your config:
147
+
148
+ ```graphql title="schema.graphql"
149
+ type Query {
150
+ product(id: ID!): Product
151
+ }
152
+
153
+ type Product {
154
+ id: ID!
155
+ name: String!
156
+ price: Float!
157
+ }
158
+ ```
159
+
160
+ Don't have an SDL file handy? Skip this step and introspect a live endpoint
161
+ at build time with `type: "url"` in the next step — the schema is fetched for
162
+ you when the portal builds.
163
+
164
+ 3. **Register the plugin**
165
+
166
+ Import `graphqlPlugin` and add an instance per API. The `path` is where the
167
+ docs mount, and `input` points at your schema:
168
+
169
+ ```tsx title="zudoku.config.tsx"
170
+ import { graphqlPlugin } from "@zudoku/plugin-graphql";
171
+
172
+ const config = {
173
+ plugins: [
174
+ graphqlPlugin({
175
+ type: "file",
176
+ input: "./schema.graphql",
177
+ path: "/graphql/ecommerce",
178
+ options: {
179
+ title: "E-Commerce GraphQL API",
180
+ description: "Products, orders, and customers.",
181
+ playground: {
182
+ endpoint: "https://my-gateway.example.com/graphql",
183
+ },
184
+ },
185
+ }),
186
+ ],
187
+ };
188
+
189
+ export default config;
190
+ ```
191
+
192
+ Set `playground.endpoint` to the gateway route from the first half of this
193
+ guide so readers run real queries through your gateway. With a file-based
194
+ schema the plugin doesn't know where your API lives — leave the endpoint
195
+ unset and the reference pages still render, but the playground asks you to
196
+ configure `options.playground.endpoint` before it can run operations.
197
+
198
+ With `type: "url"`, set `input` to your GraphQL endpoint's URL instead: the
199
+ schema is fetched via introspection when the portal builds, and the
200
+ playground defaults to that same URL. You can register the plugin more than
201
+ once to document several schemas, each with its own `path`.
202
+
203
+ 4. **Link it in the navigation**
204
+
205
+ Point a navigation link at the instance's `path`. Set `stack: true` so the
206
+ API's own pages render as a stacked sub-navigation instead of expanding
207
+ inline:
208
+
209
+ ```tsx title="zudoku.config.tsx"
210
+ navigation: [
211
+ {
212
+ type: "category",
213
+ label: "Documentation",
214
+ items: [
215
+ {
216
+ type: "link",
217
+ label: "E-Commerce API",
218
+ to: "/graphql/ecommerce",
219
+ stack: true,
220
+ },
221
+ ],
222
+ },
223
+ ];
224
+ ```
225
+
226
+ Prefer the API as its own top-level section instead? Drop the link at the top
227
+ level of `navigation` and leave off `stack`. See the
228
+ [navigation reference](../dev-portal/zudoku/configuration/navigation.mdx) for
229
+ all link, category, and stacking options.
230
+
231
+ </Stepper>
232
+
233
+ ### Plugin options
234
+
235
+ All options live under the instance's `options` key:
236
+
237
+ | Option | Type | Description |
238
+ | --------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
239
+ | `title` | `string` | Display title for the API's overview page. |
240
+ | `description` | `string` | Short description shown on the overview page. |
241
+ | `showDeprecated` | `boolean` | Include deprecated fields and operations in the reference. |
242
+ | `playground.enabled` | `boolean` | Show the playground. Defaults to `true`. |
243
+ | `playground.endpoint` | `string` | URL the playground sends operations to. Defaults to `input` for `type: "url"`. File schemas have no default; the playground prompts for one until set. |
244
+ | `playground.headers` | `object` | Default headers the playground sends with each operation. |
245
+
246
+ ## Verify it worked
247
+
248
+ Open the
249
+ [**Environments**](https://portal.zuplo.com/+/account/project/environments) tab
250
+ in the Zuplo Portal — your build is listed there along with the URLs for both
251
+ your gateway and your Dev Portal. Send a query through the gateway URL:
252
+
253
+ ```bash
254
+ curl https://my-gateway.example.com/graphql \
255
+ -H "Content-Type: application/json" \
256
+ -d '{"query":"{ __typename }"}'
257
+ ```
258
+
259
+ A working route returns a response from your upstream, such as
260
+ `{"data":{"__typename":"Query"}}`.
261
+
262
+ For the Dev Portal, open its URL from the **Environments** tab (or run
263
+ `npm run dev` in your `docs/` folder) and go to the plugin's `path` (for example
264
+ `/graphql/ecommerce`). The overview page lists the schema's types, and the
265
+ playground runs operations against your configured endpoint.
266
+
267
+ ## Next steps
268
+
269
+ - [Secure your GraphQL API](./graphql-security.mdx) — complexity limits and
270
+ introspection policies
271
+ - [Testing GraphQL queries](./testing-graphql.mdx) — test the endpoint from the
272
+ Zuplo Portal or external tools
273
+ - [URL Rewrite handler](../handlers/url-rewrite.mdx) — full reference for the
274
+ handler GraphQL routes use
275
+ - [MCP Server GraphQL endpoints](../mcp-server/graphql.mdx) — expose GraphQL
276
+ queries as MCP tools for AI agents
@@ -206,12 +206,26 @@ jobs:
206
206
  - name: Install dependencies
207
207
  run: npm install
208
208
 
209
+ - name: Get deployment URL
210
+ id: get-url
211
+ uses: actions/github-script@v7
212
+ with:
213
+ script: |
214
+ const comments = await github.rest.issues.listComments({
215
+ issue_number: context.issue.number,
216
+ owner: context.repo.owner,
217
+ repo: context.repo.repo,
218
+ });
219
+ const match = comments.data
220
+ .map((c) => c.body.match(/Deployed to: (https:\/\/\S+)/))
221
+ .find(Boolean);
222
+ core.setOutput("url", match ? match[1] : "");
223
+
209
224
  - name: Delete environment
225
+ if: steps.get-url.outputs.url != ''
210
226
  run: |
211
- BRANCH_NAME="${{ github.head_ref }}"
212
- ENV_NAME="${BRANCH_NAME//\//-}"
213
227
  npx zuplo delete \
214
- --environment "$ENV_NAME" \
228
+ --url "${{ steps.get-url.outputs.url }}" \
215
229
  --api-key "$ZUPLO_API_KEY" \
216
230
  --wait
217
231
  ```