zuplo 6.69.6 → 6.69.9

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.
@@ -12,9 +12,9 @@ announced.
12
12
 
13
13
  :::
14
14
 
15
- Zuplo uses Stripe to handle payment processing, subscription billing, and
16
- invoicing. Zuplo handles metering, quota enforcement, and subscription state
17
- Stripe handles money.
15
+ Zuplo uses Stripe to collect payments. Zuplo is the system of record for plans,
16
+ subscriptions, features, entitlements, and metered usage; Stripe is the system
17
+ of record for **money** — customers, invoices, and payment collection.
18
18
 
19
19
  ## How it works
20
20
 
@@ -22,19 +22,21 @@ The integration flow:
22
22
 
23
23
  1. You define plans, features, and meters in Zuplo
24
24
  2. You connect your Stripe account via the Zuplo Portal
25
- 3. When you publish plans, corresponding Stripe Products and Prices are created
26
- automatically
25
+ 3. Plans, features, and rate cards stay in Zuplo's catalog Stripe is used only
26
+ at billing time
27
27
  4. Customers subscribe through your Developer Portal via Stripe Checkout
28
- 5. Stripe processes the payment and creates the subscription
29
- 6. A Zuplo subscription is created with an API key scoped to the plan's
30
- entitlements
31
- 7. As the customer uses the API, the monetization policy meters usage in real
28
+ 5. Stripe collects the payment method and Zuplo creates the subscription with an
29
+ API key scoped to the plan's entitlements
30
+ 6. As the customer uses the API, the monetization policy meters usage in real
32
31
  time
33
- 8. For usage-based billing, usage is tracked continuously and billed through
34
- Stripe automatically
32
+ 7. At the end of each billing period, Zuplo issues a Stripe Invoice for fixed
33
+ fees and usage-based charges
35
34
 
36
- Throughout this flow, Zuplo is the source of truth for access control and
37
- metering. Stripe is the source of truth for payment state.
35
+ Throughout this flow, Zuplo is the source of truth for access control,
36
+ plans/rate cards, and metered usage. Stripe is the source of truth for payment
37
+ state and tax. **Stripe Subscriptions, Products, Prices, and Billing Meters are
38
+ not used** — Zuplo manages those concepts internally and only materializes them
39
+ in Stripe at the moment a charge is needed (as invoice line items).
38
40
 
39
41
  ## Connecting your Stripe account
40
42
 
@@ -45,8 +47,19 @@ metering. Stripe is the source of truth for payment state.
45
47
  3. Enter a **Name** and paste your **Stripe API Key**
46
48
  4. Click **Save**
47
49
 
48
- The connection authorizes Zuplo to manage Stripe objects on your behalf,
49
- including products, prices, customers, and subscriptions.
50
+ :::tip
51
+
52
+ To script the same flow (CI, infrastructure-as-code, etc.) use the
53
+ [Stripe setup API](./api-access.mdx#stripe-setup-and-billing-readiness) — it
54
+ exposes `POST /setup/stripe`, `GET /billing-readiness`, and key-rotation
55
+ endpoints with the same prefix validation as the UI.
56
+
57
+ :::
58
+
59
+ The connection authorizes Zuplo to manage Stripe objects on your behalf —
60
+ specifically Customers, Checkout Sessions, Customer Portal Sessions, Invoices,
61
+ and Tax Calculations. See
62
+ [What Zuplo creates in Stripe](#what-zuplo-creates-in-stripe) for the full list.
50
63
 
51
64
  ### Test mode vs. live mode
52
65
 
@@ -143,20 +156,23 @@ new key must:
143
156
 
144
157
  ## What Zuplo creates in Stripe
145
158
 
146
- When you publish a plan, corresponding objects are created in Stripe
147
- automatically:
159
+ Zuplo's catalog plans, features, rate cards, and entitlements — is stored in
160
+ Zuplo. Stripe is used only at the points where money or payment state is
161
+ involved.
162
+
163
+ The objects that Zuplo creates or manages in your Stripe account:
148
164
 
149
- | Zuplo concept | Stripe object created |
150
- | -------------------- | -------------------------------- |
151
- | Plan | Product |
152
- | Rate card (flat fee) | Price (recurring, fixed amount) |
153
- | Rate card (per-unit) | Price (recurring, metered usage) |
154
- | Rate card (tiered) | Price (recurring, tiered) |
155
- | Feature entitlement | Metadata on the Product |
165
+ | Object | When it's created |
166
+ | ------------------------------- | -------------------------------------------------------------------------- |
167
+ | Stripe Customer | When a developer first subscribes — one Stripe Customer per Zuplo customer |
168
+ | Stripe Checkout Session | When a developer subscribes to a plan that requires a payment method |
169
+ | Stripe Customer Portal Session | When a developer opens **Manage Billing** in the Developer Portal |
170
+ | Stripe Invoice and Invoice Item | At the end of each billing period for fixed and usage-based charges |
171
+ | Stripe Tax Calculation | At invoice time when [tax collection](./tax-collection.md) is enabled |
172
+ | Stripe Webhook Endpoint | Once on connection, so Zuplo can react to payment events |
156
173
 
157
- You can see these in your Stripe Dashboard under **Products**. These objects are
158
- managed automatically — don't edit them directly in Stripe, as your changes may
159
- be overwritten on the next plan publish.
174
+ To see what Zuplo has created, look under **Customers** and **Invoices** in your
175
+ Stripe dashboard.
160
176
 
161
177
  ## Subscription flow
162
178
 
@@ -164,7 +180,8 @@ be overwritten on the next plan publish.
164
180
 
165
181
  When a customer clicks "Subscribe" in your Developer Portal:
166
182
 
167
- 1. A Stripe Checkout Session is created with the selected plan's prices
183
+ 1. A Stripe Checkout Session is created so the customer can enter a payment
184
+ method
168
185
  2. The customer is redirected to Stripe Checkout to enter payment details
169
186
  3. On successful payment, the subscription is created
170
187
  4. An API key is generated scoped to the subscription's plan entitlements
@@ -175,22 +192,38 @@ When a customer clicks "Subscribe" in your Developer Portal:
175
192
 
176
193
  When a customer changes their plan through the Developer Portal:
177
194
 
178
- 1. The Stripe Subscription is updated with the new plan's prices
179
- 2. Charges are prorated automatically
195
+ 1. Zuplo records the plan change and recalculates the customer's entitlements
196
+ 2. Any prorated amount is reflected on the customer's next Stripe Invoice
180
197
  3. The customer's entitlements update immediately
181
198
  4. The API key remains the same; its associated quota changes in real time
182
199
 
183
- Upgrades take effect immediately. Downgrades take effect at the next billing
184
- cycle.
200
+ Zuplo uses **max_consumption_based proration** so customers can't game
201
+ mid-period upgrades and downgrades — see
202
+ [Subscription Lifecycle → Proration behavior](./subscription-lifecycle.md#proration-behavior)
203
+ for the detailed model and examples.
185
204
 
186
205
  ### Cancellation
187
206
 
188
- When a customer cancels:
189
-
190
- 1. The subscription is set to cancel at the end of the current billing period
191
- (by default)
192
- 2. The customer retains access until their current billing period ends
193
- 3. At period end, access is revoked and the API key stops working
207
+ When a customer cancels through the Developer Portal, the timing of the
208
+ cancellation depends on whether the current phase has billable items:
209
+
210
+ - **Paid phases** — the portal sends `timing: "next_billing_cycle"`. The
211
+ subscription is scheduled to cancel at the end of the current billing
212
+ period, the customer retains access until then, and the API key stops
213
+ working at period end.
214
+ - **Free phases** — the portal sends `timing: "immediate"`. With nothing to
215
+ invoice at period end, there's no billing period to wait out, so the
216
+ subscription cancels and access is revoked right away. Two situations fall
217
+ into this branch:
218
+ - The customer is on a **free trial phase** (the first phase of a plan
219
+ with a later paid phase) and cancels before the trial converts.
220
+ - The customer is on a **free plan** — a plan whose only phase has no
221
+ billable rate cards (every rate card's `price` is `null`).
222
+
223
+ For programmatic cancellation, see
224
+ [Cancellation](./subscription-lifecycle.md#cancellation) in the Subscription
225
+ Lifecycle guide — the API endpoint accepts a `timing` parameter to control
226
+ this same behavior explicitly.
194
227
 
195
228
  ## Proration
196
229
 
@@ -203,10 +236,13 @@ invoice.
203
236
 
204
237
  For plans with usage-based pricing (per-unit, tiered, pay-as-you-go), usage is
205
238
  tracked in real time by the `MonetizationInboundPolicy`. Each API request
206
- increments the meter immediately. At the end of the billing period, usage is
207
- billed through Stripe automatically.
239
+ increments the meter immediately. At the end of the billing period, Zuplo
240
+ generates a Stripe Invoice with line items for the period's fixed fees and
241
+ metered usage, and Stripe collects payment.
208
242
 
209
- You don't need to implement usage reporting or run any batch jobs.
243
+ You don't need to implement usage reporting or run any batch jobs — and Zuplo
244
+ does **not** call Stripe Billing Meters; metered usage is materialized as
245
+ invoice line items directly.
210
246
 
211
247
  ## Handling failed payments
212
248
 
@@ -222,8 +258,11 @@ Stripe retries the payment.
222
258
  | `failed` | Access blocked after grace period (configurable) |
223
259
  | `uncollectible` | Access blocked |
224
260
 
225
- The grace period is configurable via `zuplo_max_payment_overdue_days` metadata
226
- on the plan or customer (default: 3 days).
261
+ The grace period is configurable, with customer metadata overriding plan
262
+ metadata, which overrides the bucket-level `maxPaymentOverdueDays`. Default is 3
263
+ days. See
264
+ [Subscription and payment validation](./monetization-policy.md#subscription-and-payment-validation)
265
+ for the full resolution order.
227
266
 
228
267
  ## Customer portal
229
268
 
@@ -260,13 +299,19 @@ After connecting Stripe and publishing plans:
260
299
  1. Open your Developer Portal
261
300
  2. Subscribe to a plan using test card `4242 4242 4242 4242`
262
301
  3. Verify in Stripe Dashboard:
263
- - Customer created
264
- - Subscription active
265
- - Product and Price match your plan
302
+ - **Customers** — a customer was created with correct metadata and test card
303
+ attached
304
+ - **Developers Webhooks** the Zuplo-managed webhook endpoint is
305
+ registered and recent events show `200` responses
266
306
  4. Make API requests and verify:
267
307
  - Requests succeed within quota
268
- - `403 Forbidden` returned when quota exceeded (hard limit plans)
308
+ - `403 Forbidden` returned when quota exceeded (hard-limit plans)
269
309
  - Usage visible in the Developer Portal dashboard
270
- 5. Cancel the subscription and verify:
271
- - Access revoked after billing period
272
- - Stripe subscription shows canceled
310
+ 5. Wait for the next billing cycle (or trigger a manual invoice in test mode)
311
+ and verify:
312
+ - **Invoices** — a Stripe Invoice was created with line items matching your
313
+ plan's fixed fees and metered usage
314
+ 6. Cancel the subscription in the Developer Portal and verify:
315
+ - Access is revoked after the billing period ends
316
+ - The Zuplo subscription shows `canceled` in the API and Developer Portal
317
+ (Stripe doesn't track this — there is no Stripe Subscription to look at)
@@ -27,8 +27,9 @@ trials, upgrades, downgrades, cancellation, and reactivation.
27
27
  :::note
28
28
 
29
29
  Access is also governed by payment status. If a payment is overdue beyond the
30
- grace period (default 3 days, configurable via `zuplo_max_payment_overdue_days`
31
- metadata), access is blocked even for active subscriptions.
30
+ grace period (default 3 days), access is blocked even for active subscriptions.
31
+ The grace period is configurable per-customer, per-plan, or per-bucket — see
32
+ [Subscription and payment validation](./monetization-policy.md#subscription-and-payment-validation).
32
33
 
33
34
  :::
34
35
 
@@ -56,14 +57,17 @@ curl -X POST https://dev.zuplo.com/v3/metering/{bucketId}/subscriptions \
56
57
  -H "Authorization: Bearer {API_KEY}" \
57
58
  -H "Content-Type: application/json" \
58
59
  -d '{
59
- "customerId": "cus_abc123",
60
- "planKey": "pro",
61
- "stripeCustomerId": "cus_stripe_xyz"
60
+ "plan": { "key": "pro" },
61
+ "customerId": "01J9ZX2A8R0K8H6VG2C1A0K3WP"
62
62
  }'
63
63
  ```
64
64
 
65
- The `customerId` is the user's identifier in your auth system (Auth0 user ID,
66
- etc.). The `stripeCustomerId` is the customer's ID in Stripe.
65
+ `plan` references the target plan by its `key` (and optionally `version`).
66
+ Provide either `customerId` (the OpenMeter customer ULID, format
67
+ `^[0-7][0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{25}$`) or `customerKey` (your own
68
+ identifier). Optional fields include `timing` (`"immediate"` by default,
69
+ `"next_billing_cycle"`, or an RFC 3339 timestamp), `startingPhase`, `name`,
70
+ `description`, `metadata`, `alignment`, and `billingAnchor`.
67
71
 
68
72
  ## Free trials
69
73
 
@@ -185,15 +189,24 @@ Customers can change plans from the Subscriptions page in the Developer Portal:
185
189
 
186
190
  ### Programmatic (API)
187
191
 
192
+ Plan changes go through the dedicated `change` endpoint, which closes the
193
+ current subscription and starts a new one on the target plan:
194
+
188
195
  ```bash
189
- curl -X PATCH https://dev.zuplo.com/v3/metering/{bucketId}/subscriptions/{subscriptionId} \
196
+ curl -X POST https://dev.zuplo.com/v3/metering/{bucketId}/subscriptions/{subscriptionId}/change \
190
197
  -H "Authorization: Bearer {API_KEY}" \
191
198
  -H "Content-Type: application/json" \
192
199
  -d '{
193
- "planKey": "enterprise"
200
+ "timing": "immediate",
201
+ "plan": { "key": "enterprise" }
194
202
  }'
195
203
  ```
196
204
 
205
+ `timing` accepts `"immediate"`, `"next_billing_cycle"`, or an RFC 3339
206
+ timestamp. To preview the proration credit before committing, call
207
+ `POST /v3/metering/{bucketId}/subscriptions/{subscriptionId}/change/estimate-credit`
208
+ with the same body.
209
+
197
210
  ### Proration behavior
198
211
 
199
212
  When a customer changes plans mid-billing-period, Zuplo uses
@@ -255,9 +268,21 @@ Customers can cancel from the Developer Portal subscriptions page:
255
268
 
256
269
  ```bash
257
270
  curl -X POST https://dev.zuplo.com/v3/metering/{bucketId}/subscriptions/{subscriptionId}/cancel \
258
- -H "Authorization: Bearer {API_KEY}"
271
+ -H "Authorization: Bearer {API_KEY}" \
272
+ -H "Content-Type: application/json" \
273
+ -d '{
274
+ "timing": "next_billing_cycle"
275
+ }'
259
276
  ```
260
277
 
278
+ `timing` controls when the cancellation takes effect:
279
+
280
+ - Omitted (or `"immediate"`) — the subscription is canceled immediately and
281
+ access stops right away. Use this for refund-style flows.
282
+ - `"next_billing_cycle"` — access continues until the end of the current billing
283
+ period, then is revoked. This matches the Developer Portal default.
284
+ - An RFC 3339 timestamp — schedule cancellation at a specific time.
285
+
261
286
  ### Cancellation behavior
262
287
 
263
288
  | Scenario | Default behavior |
@@ -276,14 +301,9 @@ curl -X POST https://dev.zuplo.com/v3/metering/{bucketId}/subscriptions/{subscri
276
301
  -H "Authorization: Bearer {API_KEY}"
277
302
  ```
278
303
 
279
- This removes the pending cancellation. The subscription continues as normal.
280
-
281
- A fully canceled subscription (past the period end) can be restored:
282
-
283
- ```bash
284
- curl -X POST https://dev.zuplo.com/v3/metering/{bucketId}/subscriptions/{subscriptionId}/restore \
285
- -H "Authorization: Bearer {API_KEY}"
286
- ```
304
+ This removes the pending cancellation. The subscription continues as normal. For
305
+ a subscription whose period has already ended, create a new subscription on the
306
+ same plan instead.
287
307
 
288
308
  ## Multiple subscriptions
289
309
 
@@ -30,8 +30,11 @@ announced.
30
30
  -H "Authorization: Bearer {API_KEY}"
31
31
  ```
32
32
 
33
- Fix: Either resolve the payment issue in Stripe, or adjust the grace period
34
- via `zuplo_max_payment_overdue_days` metadata on the plan or customer.
33
+ Fix: Either resolve the payment issue in Stripe, or adjust the grace period.
34
+ The window resolves customer metadata plan metadata → bucket configuration
35
+ → 3-day default. See
36
+ [Subscription and payment validation](./monetization-policy.md#subscription-and-payment-validation)
37
+ for details.
35
38
 
36
39
  2. **Customer is using the wrong API key.** Each subscription generates its own
37
40
  key. If the customer has multiple subscriptions or regenerated their key,
@@ -154,6 +154,28 @@ When the prompt parameter is omitted (empty string), Auth0 will:
154
154
  - Silently authenticate the user if they have a valid session
155
155
  - Redirect to the login page if no valid session exists
156
156
 
157
+ ### Customizing Sign-up
158
+
159
+ By default Auth0 already adds `screen_hint=signup` when a user clicks Register. To send users to a
160
+ different page (or hide Register entirely):
161
+
162
+ ```typescript
163
+ authentication: {
164
+ type: "auth0",
165
+ domain: "your-domain.us.auth0.com",
166
+ clientId: "<your-auth0-client-id>",
167
+
168
+ // Send Register to a separate URL (absolute → external, relative → in-app)
169
+ signUp: { url: "/register" },
170
+
171
+ // Or pass extra params to the Auth0 authorize URL on sign-up
172
+ signUp: { authorizationParams: { connection: "your-signup-connection" } },
173
+
174
+ // Hide Register UI entirely. Visual only — configure Auth0 to actually block sign-ups.
175
+ disableSignUp: true,
176
+ }
177
+ ```
178
+
157
179
  ## Troubleshooting
158
180
 
159
181
  ### Common Issues
@@ -178,6 +178,24 @@ To allow external partners access:
178
178
  3. Invite guest users to your directory
179
179
  4. Grant appropriate permissions to your application
180
180
 
181
+ ### Customizing Sign-up
182
+
183
+ Azure AD B2C usually handles sign-up via a separate user flow. To send users there, point Register
184
+ at the URL of that flow (or any other page):
185
+
186
+ ```typescript
187
+ authentication: {
188
+ type: "azureb2c",
189
+ // ...
190
+
191
+ // Absolute URL → external redirect, relative path → in-app navigate
192
+ signUp: { url: "https://your-tenant.b2clogin.com/your-tenant.onmicrosoft.com/B2C_1_SignUp/oauth2/v2.0/authorize?..." },
193
+
194
+ // Hide Register entirely. Visual only — sign-ups are still controlled by your B2C policy.
195
+ disableSignUp: true,
196
+ }
197
+ ```
198
+
181
199
  ## User Data
182
200
 
183
201
  Azure AD provides rich user profile data through OpenID Connect:
@@ -84,6 +84,23 @@ that provides 10,000 monthly active users.
84
84
 
85
85
  You should also ensure your site's domain is added as an allowed origin in the Clerk dashboard.
86
86
 
87
+ 5. **Customizing Sign-up (Optional)**
88
+
89
+ To send Register to a different page, or hide it entirely:
90
+
91
+ ```typescript
92
+ authentication: {
93
+ type: "clerk",
94
+ clerkPubKey: "<your-clerk-publishable-key>",
95
+
96
+ // Absolute URL → external redirect, relative path → in-app navigate
97
+ signUp: { url: "/register" },
98
+
99
+ // Hide Register UI. Visual only — configure Clerk to actually block sign-ups.
100
+ disableSignUp: true,
101
+ },
102
+ ```
103
+
87
104
  </Stepper>
88
105
 
89
106
  ## Troubleshooting
@@ -52,6 +52,13 @@ export default {
52
52
  // "microsoft", "apple", "yahoo", "password", "emailLink"
53
53
  // If not specified, all enabled providers in Firebase will be available
54
54
  providers: ["google", "github", "password"],
55
+ // Optional: disable the sign-up UI for invite-only setups. Defaults to false.
56
+ // When true, the Register button and "Sign up" link are hidden, and /signup shows a message.
57
+ // Visual only — also disable sign-ups in your Firebase project for real enforcement.
58
+ disableSignUp: true,
59
+ // Optional: send Register to a separate URL instead of /signup
60
+ // (absolute URL → external redirect, relative path → in-app navigate)
61
+ signUp: { url: "/register" },
55
62
  // Optional: configure redirect URLs after authentication
56
63
  redirectToAfterSignIn: "/docs",
57
64
  redirectToAfterSignUp: "/getting-started",
@@ -92,6 +92,33 @@ You can confirm your issuer URL is correct by opening `<issuer>/.well-known/open
92
92
  a browser. It should return a JSON document listing `authorization_endpoint`, `token_endpoint`,
93
93
  `userinfo_endpoint`, and `jwks_uri`.
94
94
 
95
+ ## Customizing Sign-up
96
+
97
+ By default Register and Sign in both call the OIDC authorize endpoint, so users land on the same
98
+ login page. Two options change that:
99
+
100
+ ```typescript title="zudoku.config.ts"
101
+ {
102
+ authentication: {
103
+ type: "openid",
104
+ clientId: "<your-client-id>",
105
+ issuer: "<the-issuer-url>",
106
+
107
+ // Send Register to a separate URL (absolute → external redirect, relative → in-app navigate)
108
+ signUp: { url: "/register" },
109
+
110
+ // Or pass extra params to the authorize URL on sign-up only (e.g. Keycloak)
111
+ signUp: { authorizationParams: { kc_action: "register" } },
112
+
113
+ // Hide Register UI entirely. Visual only — still configure your IdP to block sign-ups.
114
+ disableSignUp: true,
115
+ },
116
+ }
117
+ ```
118
+
119
+ When `disableSignUp` is `true`, the Register button on the protected-route login dialog is hidden
120
+ and `/signup` shows an "Invitation required" page.
121
+
95
122
  ## User Profile
96
123
 
97
124
  After sign-in Dev Portal calls the provider's
@@ -247,6 +247,16 @@ authentication: {
247
247
  // Optional: When true, sign-in and sign-up pages show only OAuth buttons (no email/password)
248
248
  onlyThirdPartyProviders: true,
249
249
 
250
+ // Optional: When true, the sign-up UI is disabled (for invite-only setups).
251
+ // Must also be disabled in the Supabase dashboard under
252
+ // Authentication → Configuration → Sign In / Providers → Allow new users to sign up.
253
+ // Defaults to false.
254
+ disableSignUp: true,
255
+
256
+ // Optional: send Register to a separate URL instead of /signup
257
+ // (absolute URL → external redirect, relative path → in-app navigate)
258
+ signUp: { url: "/register" },
259
+
250
260
  // Optional: Redirect URLs after authentication events
251
261
  redirectToAfterSignUp: "/welcome",
252
262
  redirectToAfterSignIn: "/dashboard",
@@ -311,6 +321,31 @@ CREATE TRIGGER on_auth_user_created
311
321
  FOR EACH ROW EXECUTE PROCEDURE public.handle_new_user();
312
322
  ```
313
323
 
324
+ ## Invite-only sign-ups
325
+
326
+ To launch an invite-only portal where new users can only be created by an admin, set
327
+ `disableSignUp: true` in your Dev Portal config **and** disable new sign-ups in the Supabase dashboard
328
+ (**Authentication** → **Configuration** → **Sign In / Providers** → clear **Allow new users to sign
329
+ up**). When `disableSignUp` is `true`:
330
+
331
+ - The "Don't have an account? Sign up." link is hidden on the sign-in page.
332
+ - The Register button is hidden on the protected-route login dialog.
333
+ - Navigating to `/signup` shows an "Invitation required" message instead of a form.
334
+
335
+ ```ts title="zudoku.config.ts"
336
+ export default {
337
+ authentication: {
338
+ type: "supabase",
339
+ supabaseUrl: "https://your-project.supabase.co",
340
+ supabaseKey: "<your-publishable-key>",
341
+ disableSignUp: true,
342
+ },
343
+ };
344
+ ```
345
+
346
+ Create users directly from the Supabase dashboard (**Authentication** → **Users** → **Add user**) or
347
+ via the Supabase admin API.
348
+
314
349
  ## Troubleshooting
315
350
 
316
351
  ### Common Issues
@@ -183,6 +183,22 @@ The `url` should be a template where the file path will be appended. For example
183
183
  in a `docs/pages/` directory, the URL might be
184
184
  `https://github.com/your-org/your-repo/edit/main/docs/pages`.
185
185
 
186
+ #### `fullWidth`
187
+
188
+ **Type:** `boolean` **Default:** `false`
189
+
190
+ Whether pages should use the full available width (hiding the table of contents sidebar) by default.
191
+ When enabled, the table of contents is accessible via an "On this page" toggle in the page header.
192
+ Combine with `toc: false` to hide the table of contents entirely.
193
+
194
+ ```tsx title="zudoku.config.tsx"
195
+ docs: {
196
+ defaultOptions: {
197
+ fullWidth: true, // Use full-width layout for all pages by default
198
+ }
199
+ }
200
+ ```
201
+
186
202
  #### `copyPage`
187
203
 
188
204
  **Type:** `boolean` **Default:** `undefined`