diff --git a/AGENTS.md b/AGENTS.md
index a5cfc121..e5ed666e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -21,10 +21,25 @@ APIs.
- Keep JSDoc strings short and useful
- while writing JSDoc follow it's standards, such as tags
+## References
+
+If you need to inspect source code of libraries or packages, prefer inspecting
+cloned source repositories when available, rather than library dist files.
+
+These directories already contain source checkouts for some packages:
+
+- `~/ref/fumadocs` - fumadocs framework / package.
+
## Behavior
- When asked opinion questions, answer only. Do not edit code unless explicitly asked.
- Never commit, push, or run database migrations unless explicitly asked.
+- Never create a partial commit from files that also contain unstaged changes.
+ If a requested commit overlaps dirty files, either commit the full intended
+ change or stop and ask. Do not rely on hooks stashing/hiding unstaged changes.
+- If a commit hook fails while hiding or restoring unstaged changes, stop
+ immediately and recover the user's unstaged work before retrying or committing
+ anything.
- When generating migrations, always provide a name.
- Never edit past migrations; create a new migration instead.
- Never run "deploy" scripts to test anything, only if explicitly asked
diff --git a/apps/web/content/docs/concepts/cli.mdx b/apps/web/content/docs/cli.mdx
similarity index 89%
rename from apps/web/content/docs/concepts/cli.mdx
rename to apps/web/content/docs/cli.mdx
index ce624a05..efe60c09 100644
--- a/apps/web/content/docs/concepts/cli.mdx
+++ b/apps/web/content/docs/cli.mdx
@@ -5,9 +5,11 @@ description: Project initialization and plan management from the command line.
PayKit includes a CLI tool for project setup, database migrations, and plan syncing. Install it once and use it throughout the project lifecycle.
-## `paykitjs init`
+## paykitjs init
-
+```bash
+npx paykitjs init
+```
An interactive setup wizard that scaffolds everything you need to get started. It configures Stripe, then generates:
@@ -17,9 +19,11 @@ An interactive setup wizard that scaffolds everything you need to get started. I
Run this once when starting a new project.
-## `paykitjs push`
+## paykitjs push
-
+```bash
+npx paykitjs push
+```
The command you'll run most often. It does two things:
@@ -42,9 +46,11 @@ paykitjs push -y && next build
This is similar to how you'd run database migrations on deploy. The `-y` flag skips the confirmation prompt.
-## `paykitjs status`
+## paykitjs status
-
+```bash
+npx paykitjs status
+```
Validates your entire PayKit setup without making any changes. Useful when debugging a broken environment. It checks:
@@ -56,7 +62,9 @@ Validates your entire PayKit setup without making any changes. Useful when debug
Pass `--throw` to exit with code 1 on failures, useful for CI pipelines:
-
+```bash
+npx paykitjs status --throw
+```
## Telemetry
diff --git a/apps/web/content/docs/concepts/client.mdx b/apps/web/content/docs/client.mdx
similarity index 88%
rename from apps/web/content/docs/concepts/client.mdx
rename to apps/web/content/docs/client.mdx
index adf332b7..2ac28a3a 100644
--- a/apps/web/content/docs/concepts/client.mdx
+++ b/apps/web/content/docs/client.mdx
@@ -16,13 +16,13 @@ import type { paykit } from "@/server/paykit";
export const paykitClient = createPayKitClient();
```
-The client resolves the current customer automatically on each request. You need `identify` configured on your server instance for this to work. See [customer identification](/docs/concepts/customers#customer-identification-client) for details.
+The client resolves the current customer automatically on each request. You need `identify` configured on your server instance for this to work. See [customer identification](/docs/customers#customer-identification-client) for details.
## Available methods
The client exposes `subscribe` and `customerPortal`. Neither requires a `customerId` since it's resolved from the incoming request via `identify`.
-## `subscribe`
+## subscribe
Works the same as the server-side `subscribe`, but without `customerId`. Returns `{ paymentUrl }`.
@@ -44,7 +44,7 @@ Works the same as the server-side `subscribe`, but without `customerId`. Returns
```
-## `customerPortal`
+## customerPortal
Opens the provider's customer portal. Returns `{ url }`.
@@ -72,4 +72,4 @@ You can also pass an absolute URL like `https://example.com/custom`.
## Type safety
-The client infers available plan IDs directly from your server instance type. If you pass an invalid `planId`, TypeScript catches it at compile time. See [TypeScript](/docs/concepts/typescript) for more on how type inference works across the stack.
+The client infers available plan IDs directly from your server instance type. If you pass an invalid `planId`, TypeScript catches it at compile time. See [TypeScript](/docs/typescript) for more on how type inference works across the stack.
diff --git a/apps/web/content/docs/concepts/meta.json b/apps/web/content/docs/concepts/meta.json
deleted file mode 100644
index b2e609e4..00000000
--- a/apps/web/content/docs/concepts/meta.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "title": "Concepts",
- "pages": [
- "plans-and-features",
- "customers",
- "subscriptions",
- "entitlements",
- "webhook-events",
- "database",
- "payment-providers",
- "plugins",
- "client",
- "cli",
- "typescript"
- ]
-}
diff --git a/apps/web/content/docs/concepts/payment-providers.mdx b/apps/web/content/docs/concepts/payment-providers.mdx
deleted file mode 100644
index cf61106f..00000000
--- a/apps/web/content/docs/concepts/payment-providers.mdx
+++ /dev/null
@@ -1,41 +0,0 @@
----
-title: Payment Providers
-description: How PayKit keeps Stripe-native IDs internal.
----
-
-PayKit uses Stripe for billing. Your app works with plans, customers, and subscriptions. PayKit translates those into Stripe-native operations behind the scenes.
-
-## How it works
-
-Pass your Stripe keys to `createPayKit({ stripe })`. PayKit handles Stripe API calls, webhook processing, and product syncing.
-
-```ts title="paykit.ts"
-export const paykit = createPayKit({
- // ...
- stripe: {
- secretKey: process.env.STRIPE_SECRET_KEY!,
- webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
- },
-});
-```
-
-## Stripe
-
-Stripe covers subscriptions, usage-based billing, webhooks, and product syncing out of the box.
-
-See the [Stripe provider page](/docs/providers/stripe) for full setup instructions, webhook configuration, and available options.
-
-## Stripe IDs stay internal
-
-Your app identifies customers and plans with its own IDs. PayKit maps them to Stripe customer IDs, product IDs, and price IDs internally. You never store or reference Stripe IDs in your app code.
-
-```ts
-// Your app always uses its own IDs
-await paykit.subscribe({ customerId: "user_123", planId: "pro" });
-
-// PayKit resolves the Stripe customer and price internally
-```
-
-
- This mapping keeps Stripe details out of your application code.
-
diff --git a/apps/web/content/docs/concepts/customers.mdx b/apps/web/content/docs/customers.mdx
similarity index 100%
rename from apps/web/content/docs/concepts/customers.mdx
rename to apps/web/content/docs/customers.mdx
diff --git a/apps/web/content/docs/plugins/dashboard.mdx b/apps/web/content/docs/dashboard.mdx
similarity index 100%
rename from apps/web/content/docs/plugins/dashboard.mdx
rename to apps/web/content/docs/dashboard.mdx
diff --git a/apps/web/content/docs/concepts/database.mdx b/apps/web/content/docs/database.mdx
similarity index 98%
rename from apps/web/content/docs/concepts/database.mdx
rename to apps/web/content/docs/database.mdx
index fa4bc1b9..639512bb 100644
--- a/apps/web/content/docs/concepts/database.mdx
+++ b/apps/web/content/docs/database.mdx
@@ -51,7 +51,9 @@ PayKit creates tables prefixed with `paykit_`. The key ones are:
`paykitjs push` applies any pending migrations. Run it on initial setup and whenever you update your plan configuration.
-
+```bash
+npx paykitjs push
+```
## What's synced
diff --git a/apps/web/content/docs/concepts/entitlements.mdx b/apps/web/content/docs/entitlements.mdx
similarity index 100%
rename from apps/web/content/docs/concepts/entitlements.mdx
rename to apps/web/content/docs/entitlements.mdx
diff --git a/apps/web/content/docs/flows/meta.json b/apps/web/content/docs/flows/meta.json
deleted file mode 100644
index 2fc1f59f..00000000
--- a/apps/web/content/docs/flows/meta.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "title": "Flows",
- "pages": ["subscription-billing", "metered-usage"]
-}
diff --git a/apps/web/content/docs/get-started/meta.json b/apps/web/content/docs/get-started/meta.json
deleted file mode 100644
index ef4c767e..00000000
--- a/apps/web/content/docs/get-started/meta.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "title": "Get Started",
- "pages": ["index", "installation", "quickstart"]
-}
diff --git a/apps/web/content/docs/guides/meta.json b/apps/web/content/docs/guides/meta.json
deleted file mode 100644
index d2cb1d9b..00000000
--- a/apps/web/content/docs/guides/meta.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "title": "Guides",
- "pages": ["skills"]
-}
diff --git a/apps/web/content/docs/get-started/installation.mdx b/apps/web/content/docs/installation.mdx
similarity index 89%
rename from apps/web/content/docs/get-started/installation.mdx
rename to apps/web/content/docs/installation.mdx
index 8fd9e721..d06038ef 100644
--- a/apps/web/content/docs/get-started/installation.mdx
+++ b/apps/web/content/docs/installation.mdx
@@ -1,28 +1,25 @@
---
title: Installation
-description: Install PayKit, configure your billing instance, and mount the route handler in your app.
+description: how to install and configure PayKit in your app
---
+## Steps
+
-## Install the package
+Install the package
Let's start by adding PayKit to your project:
-
-
-
-
-
- If you're using a separate client and server setup, make sure to install
- the package in both apps.
-
+```bash
+npm install paykitjs
+```
-## Create the PayKit instance
+Create the PayKit instance
Create a file named `paykit.ts` anywhere in your app.
@@ -42,7 +39,7 @@ export const paykit = createPayKit({
-## Configure Stripe
+Configure Stripe
PayKit uses Stripe for billing. Pass your Stripe keys directly to the PayKit instance.
@@ -59,7 +56,7 @@ export const paykit = createPayKit({
-## Configure database
+Configure database
PayKit needs a database to store billing state, such as subscriptions. You can create a separate database, or simply plug it into the app's own db.
@@ -77,13 +74,13 @@ export const paykit = createPayKit({
```
- It works by creating a few tables prefixed with `paykit_`. You can learn more [here](/docs/concepts/database).
+ It works by creating a few tables prefixed with `paykit_`. You can learn more [here](/docs/database).
-## Mount request handler
+Mount request handler
To handle webhooks and client API requests, you need to set up a request handler on your server.
@@ -135,7 +132,7 @@ Create a new file or route in your framework's designated catch-all route handle
-## Create client instance
+Create client instance
The client-side library helps you interact with the server. PayKit client sdk suitable for almost all modern frameworks, including React.
@@ -167,7 +164,7 @@ export const paykit = createPayKit({
-## Define your products
+Define your products
Optionally. PayKit provides a code-first way to create your plans, and a very useful usage billing with `track()` and `report()` out of the box.
@@ -221,21 +218,21 @@ export const paykit = createPayKit({
-## Push changes to DB
+Push changes to DB
PayKit includes a CLI tool to keep your database in sync with your configuration.
-
+```bash
+npx paykitjs push
+```
This applies database migrations and syncs your plan definitions to provider's
products.
Run it once on setup, and every time after you change your products configuration.
- For production deployments, see the [CLI reference](/docs/concepts/cli#production-usage).
+ For production deployments, see the [CLI reference](/docs/cli#production-usage).
- __You're now ready to use PayKit in your app!__ 🚀
-
diff --git a/apps/web/content/docs/get-started/index.mdx b/apps/web/content/docs/introduction.mdx
similarity index 95%
rename from apps/web/content/docs/get-started/index.mdx
rename to apps/web/content/docs/introduction.mdx
index 3ebffd07..336ff731 100644
--- a/apps/web/content/docs/get-started/index.mdx
+++ b/apps/web/content/docs/introduction.mdx
@@ -10,5 +10,3 @@ PayKit is an embedded Stripe billing framework for TypeScript apps. It provides
PayKit aims to be a complete billing framework. It provides a wide range of features out of the box and allows you to extend it with plugins.
-
-...and more to come!
diff --git a/apps/web/content/docs/meta.json b/apps/web/content/docs/meta.json
index 8bb3411d..78e7d1e5 100644
--- a/apps/web/content/docs/meta.json
+++ b/apps/web/content/docs/meta.json
@@ -2,16 +2,24 @@
"root": true,
"pages": [
"---Get Started---",
- "get-started",
+ "introduction",
+ "installation",
+ "quickstart",
"---Concepts---",
- "concepts",
+ "plans-and-features",
+ "customers",
+ "subscriptions",
+ "entitlements",
+ "webhook-events",
+ "database",
+ "plugins",
+ "client",
+ "cli",
+ "typescript",
"---Flows---",
- "flows",
- "---Providers---",
- "providers",
+ "subscription-billing",
+ "metered-usage",
"---Plugins---",
- "plugins",
- "---Guides---",
- "guides"
+ "dashboard"
]
}
diff --git a/apps/web/content/docs/flows/metered-usage.mdx b/apps/web/content/docs/metered-usage.mdx
similarity index 91%
rename from apps/web/content/docs/flows/metered-usage.mdx
rename to apps/web/content/docs/metered-usage.mdx
index 69f028a6..e4750405 100644
--- a/apps/web/content/docs/flows/metered-usage.mdx
+++ b/apps/web/content/docs/metered-usage.mdx
@@ -9,7 +9,7 @@ This guide walks through implementing usage-based billing: how to define metered
-## Define a metered feature
+Define a metered feature
Metered features track usage against a limit. Define one with `type: "metered"`.
@@ -23,7 +23,7 @@ const messages = feature({ id: "messages", type: "metered" });
-## Include in plans with different limits
+Include in plans with different limits
Pass the feature into each plan with a `limit` and `reset` interval.
@@ -55,7 +55,7 @@ Free customers get 100 messages per month; Pro customers get 2,000.
-## Check before consuming
+Check before consuming
Call `check` before performing the action. It returns `allowed` (whether the customer has remaining balance) and `balance` (how many units are left).
@@ -72,7 +72,7 @@ If `allowed` is `false`, the customer has hit their limit. Return early instead
-## Perform the action
+Perform the action
Only run the actual work if `allowed` is `true`.
@@ -88,7 +88,7 @@ const response = await generateChatResponse(input);
-## Report usage
+Report usage
After the action succeeds, call `report` to decrement the customer's balance.
@@ -106,7 +106,7 @@ Pass `amount` matching however many units the action consumed. For most cases th
-## Balance resets
+Balance resets
PayKit uses lazy resets. When the reset period passes, it doesn't reset balances proactively. Instead, the next `check` or `report` call detects that the period has expired and resets the balance automatically before returning.
diff --git a/apps/web/content/docs/concepts/plans-and-features.mdx b/apps/web/content/docs/plans-and-features.mdx
similarity index 100%
rename from apps/web/content/docs/concepts/plans-and-features.mdx
rename to apps/web/content/docs/plans-and-features.mdx
diff --git a/apps/web/content/docs/concepts/plugins.mdx b/apps/web/content/docs/plugins.mdx
similarity index 97%
rename from apps/web/content/docs/concepts/plugins.mdx
rename to apps/web/content/docs/plugins.mdx
index c83bc8e4..dae4b930 100644
--- a/apps/web/content/docs/concepts/plugins.mdx
+++ b/apps/web/content/docs/plugins.mdx
@@ -24,7 +24,9 @@ Each plugin mounts its own endpoints under the PayKit API base path automaticall
`@paykitjs/dash` adds an embedded billing dashboard your users can access directly from your app. It shows subscriptions, invoices, and lets customers manage their payment methods.
-
+```bash
+npm install @paykitjs/dash
+```
Pass an `authorize` function to control who can access it. Without `authorize`, the dashboard is open to any authenticated request.
diff --git a/apps/web/content/docs/plugins/meta.json b/apps/web/content/docs/plugins/meta.json
deleted file mode 100644
index d36d5294..00000000
--- a/apps/web/content/docs/plugins/meta.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "title": "Plugins",
- "pages": ["dashboard"]
-}
diff --git a/apps/web/content/docs/providers/meta.json b/apps/web/content/docs/providers/meta.json
deleted file mode 100644
index a058739d..00000000
--- a/apps/web/content/docs/providers/meta.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "title": "Providers",
- "pages": ["stripe"]
-}
diff --git a/apps/web/content/docs/providers/stripe.mdx b/apps/web/content/docs/providers/stripe.mdx
deleted file mode 100644
index a7e18b23..00000000
--- a/apps/web/content/docs/providers/stripe.mdx
+++ /dev/null
@@ -1,167 +0,0 @@
----
-title: Stripe
-description: Configure Stripe for PayKit, set up webhooks, sync products, and use the customer portal.
----
-
-Stripe is built into PayKit. PayKit handles Stripe API interactions, webhook processing, and product syncing.
-
-## Configuration
-
-Pass Stripe config to `createPayKit` with your secret key and webhook secret.
-
-```ts title="paykit.ts"
-import { createPayKit } from "paykitjs";
-
-export const paykit = createPayKit({
- // ...
- stripe: {
- secretKey: process.env.STRIPE_SECRET_KEY!,
- webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
- },
-});
-```
-
-## Environment variables
-
-Add these two variables to your `.env` file:
-
-```bash title=".env"
-STRIPE_SECRET_KEY=sk_live_...
-STRIPE_WEBHOOK_SECRET=whsec_...
-```
-
-- `STRIPE_SECRET_KEY`: find this in [Stripe Dashboard](https://dashboard.stripe.com) under **Developers > API keys**.
-- `STRIPE_WEBHOOK_SECRET`: generated when you create a webhook endpoint. See the section below.
-
-## Webhook setup
-
-In the Stripe Dashboard, go to **Developers > Webhooks** and create a new endpoint pointing to:
-
-```
-https://your-app.com/paykit/webhook
-```
-
-Select the following events when creating the endpoint:
-
-- `checkout.session.completed`
-- `customer.subscription.created`
-- `customer.subscription.updated`
-- `customer.subscription.deleted`
-- `invoice.created`
-- `invoice.finalized`
-- `invoice.paid`
-- `invoice.payment_failed`
-- `invoice.updated`
-- `payment_method.detached`
-
-
- Stripe discourages selecting all events as it may cause performance issues. Only the events listed above are required by PayKit.
-
-
-After saving, Stripe displays the signing secret. Copy it as your `STRIPE_WEBHOOK_SECRET`.
-
-## Local development
-
-Use the Stripe CLI to forward webhook events to your local server:
-
-```bash
-stripe listen --forward-to localhost:3000/paykit/webhook
-```
-
-The CLI prints a webhook signing secret at startup. Use that as `STRIPE_WEBHOOK_SECRET` in your local `.env`.
-
-
- Install the Stripe CLI from [stripe.com/docs/stripe-cli](https://stripe.com/docs/stripe-cli). You'll need to run `stripe login` once to authenticate.
-
-
-## Product syncing
-
-`paykitjs push` creates and updates Stripe products and prices to match your plan definitions. You don't need to touch the Stripe Dashboard for product management.
-
-
-
-
- Run this once on setup, and again every time you change your plans or pricing.
-
-
-## Customer portal
-
-PayKit can open Stripe's built-in customer portal so users can manage payment methods, invoices, and subscriptions.
-
-
-
- ```ts
- const { url } = await paykit.customerPortal({
- customerId: "user_123",
- returnUrl: "https://myapp.com/billing",
- });
-
- // redirect the user to `url`
- ```
-
-
- ```ts
- const { url } = await paykitClient.customerPortal({
- returnUrl: window.location.href,
- });
-
- window.location.href = url;
- ```
-
-
-
-The portal is hosted by Stripe, so no additional UI work is needed on your end.
-
-## Testing mode
-
-Enable `testing` on your PayKit instance to use Stripe test clocks during development. This lets you simulate time-based billing events like renewals and trial expirations.
-
-```ts title="paykit.ts"
-export const paykit = createPayKit({
- // ...
- testing: {
- enabled: true,
- },
-});
-```
-
-
- Use your Stripe test mode keys (`sk_test_...`) when testing. Test mode and live mode are completely isolated in Stripe.
-
-
-## API version
-
-PayKit pins the Stripe SDK to a known-good API version so upstream changes don't silently break your integration. Override it with `apiVersion` if you need a newer version, for example to opt into a preview feature.
-
-```ts title="paykit.ts"
-export const paykit = createPayKit({
- // ...
- stripe: {
- secretKey: process.env.STRIPE_SECRET_KEY!,
- webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
- apiVersion: "2026-03-04.preview",
- },
-});
-```
-
-The pinned default is exported as `PAYKIT_STRIPE_API_VERSION` if you want to reference it in code.
-
-## Managed Payments
-
-Stripe Managed Payments is a preview feature where Stripe takes over tax calculation, payment method selection, and billing address collection on subscription checkout sessions. Enable it with `managedPayments: true`, and set `apiVersion` to the preview version that supports it.
-
-```ts title="paykit.ts"
-export const paykit = createPayKit({
- // ...
- stripe: {
- secretKey: process.env.STRIPE_SECRET_KEY!,
- webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
- apiVersion: "2026-03-04.preview",
- managedPayments: true,
- },
-});
-```
-
-
- Managed Payments is a Stripe preview feature and the API version required may change. Review the [Stripe Managed Payments docs](https://docs.stripe.com/payments/managed-payments) before enabling in production. Note: digital products only, no shipping, and several Checkout parameters (`automatic_tax`, `payment_method_types`, Connect fields) are not supported in this mode.
-
diff --git a/apps/web/content/docs/get-started/quickstart.mdx b/apps/web/content/docs/quickstart.mdx
similarity index 88%
rename from apps/web/content/docs/get-started/quickstart.mdx
rename to apps/web/content/docs/quickstart.mdx
index d9d77b9b..23ad8094 100644
--- a/apps/web/content/docs/get-started/quickstart.mdx
+++ b/apps/web/content/docs/quickstart.mdx
@@ -3,7 +3,7 @@ title: Quickstart
description: Getting started with PayKit.
---
-This page assumes you have completed the [installation](/docs/get-started/installation) steps and have a running PayKit instance with plans defined.
+This page assumes you have completed the [installation](/docs/installation) steps and have a running PayKit instance with plans defined.
## Sync a customer
@@ -146,7 +146,6 @@ export const paykit = createPayKit({
## Next steps
-- [Plans & Features](/docs/concepts/plans-and-features) - plan groups, defaults, and feature types
-- [Subscriptions](/docs/concepts/subscriptions) - upgrade, downgrade, and cancellation behavior
-- [Entitlements](/docs/concepts/entitlements) - access checks and metered usage
-- [Stripe](/docs/providers/stripe) - provider configuration
+- [Plans & Features](/docs/plans-and-features) - plan groups, defaults, and feature types
+- [Subscriptions](/docs/subscriptions) - upgrade, downgrade, and cancellation behavior
+- [Entitlements](/docs/entitlements) - access checks and metered usage
diff --git a/apps/web/content/docs/guides/skills.mdx b/apps/web/content/docs/skills.mdx
similarity index 87%
rename from apps/web/content/docs/guides/skills.mdx
rename to apps/web/content/docs/skills.mdx
index 776ebf70..b02c6832 100644
--- a/apps/web/content/docs/guides/skills.mdx
+++ b/apps/web/content/docs/skills.mdx
@@ -3,9 +3,9 @@ title: Skills
description: Agent skills for coding assistants to set up and configure billing with PayKit.
---
-[Agent skills](https://agentskills.io) are portable instruction files (for example `SKILL.md`) that teach your coding agent project conventions, safe patterns, and where to look in the docs. The **PayKit** skill pack lives in the [`getpaykit/skills`](https://github.com/getpaykit/skills) repository.
+[Agent skills](https://agentskills.io) are portable instruction files (for example `SKILL.md`) that teach your coding agent project conventions, safe patterns, and where to look in the docs. The **PayKit** skill pack lives in the [getpaykit/skills](https://github.com/getpaykit/skills) repository.
-Install with the [`skills` CLI](https://www.npmjs.com/package/skills) (uses `npx` so nothing global is required):
+Install with the [skills CLI](https://www.npmjs.com/package/skills) (uses `npx` so nothing global is required):
```bash title="terminal"
npx skills add getpaykit/skills
diff --git a/apps/web/content/docs/flows/subscription-billing.mdx b/apps/web/content/docs/subscription-billing.mdx
similarity index 92%
rename from apps/web/content/docs/flows/subscription-billing.mdx
rename to apps/web/content/docs/subscription-billing.mdx
index cb39eb1e..c57832e2 100644
--- a/apps/web/content/docs/flows/subscription-billing.mdx
+++ b/apps/web/content/docs/subscription-billing.mdx
@@ -7,7 +7,7 @@ This guide walks through a complete subscription billing flow: defining plans, s
- ## Define plans
+ Define plans
Start by defining your plans. A typical setup has a free tier as the default and one or more paid tiers in the same group.
@@ -48,11 +48,11 @@ This guide walks through a complete subscription billing flow: defining plans, s
});
```
- For the full reference on plan groups, feature types, and pricing options, see [Plans & Features](/docs/concepts/plans-and-features).
+ For the full reference on plan groups, feature types, and pricing options, see [Plans & Features](/docs/plans-and-features).
- ## Create a customer
+ Create a customer
Before a customer can subscribe, they need to exist in PayKit.
@@ -93,7 +93,7 @@ This guide walks through a complete subscription billing flow: defining plans, s
- ## Subscribe to a paid plan
+ Subscribe to a paid plan
Call `subscribe()` with the target plan ID. For paid plans without a saved payment method, it returns a `paymentUrl` for checkout.
@@ -138,17 +138,17 @@ This guide walks through a complete subscription billing flow: defining plans, s
- ## Handle the webhook
+ Handle the webhook
After checkout, your payment provider sends a webhook to PayKit's endpoint. PayKit verifies the signature, syncs the subscription locally, and fires a `customer.updated` event. This is fully automatic.
You don't need to manually process Stripe events or update your database. By the time `customer.updated` fires, the customer's subscriptions and entitlements are already up to date.
- See [Webhook Events](/docs/concepts/webhook-events) for details on how PayKit processes and deduplicates incoming events.
+ See [Webhook Events](/docs/webhook-events) for details on how PayKit processes and deduplicates incoming events.
- ## Check entitlements
+ Check entitlements
Use `check()` to gate features based on the customer's active plan.
@@ -169,11 +169,11 @@ This guide walks through a complete subscription billing flow: defining plans, s
});
```
- For metered features, `check()` also returns a `balance` with `remaining`, `limit`, and `resetAt`. See [Entitlements](/docs/concepts/entitlements) for the full pattern including `report()`.
+ For metered features, `check()` also returns a `balance` with `remaining`, `limit`, and `resetAt`. See [Entitlements](/docs/entitlements) for the full pattern including `report()`.
- ## Upgrade
+ Upgrade
Upgrading moves the customer to a higher-priced plan in the same group. It takes effect immediately.
@@ -189,7 +189,7 @@ This guide walks through a complete subscription billing flow: defining plans, s
- ## Downgrade
+ Downgrade
Downgrading moves the customer to a lower-priced plan. The current plan stays active until the end of the billing period, then the target plan activates automatically.
@@ -205,7 +205,7 @@ This guide walks through a complete subscription billing flow: defining plans, s
- ## Cancel
+ Cancel
Cancellation works the same way as a downgrade: subscribe the customer to the default free plan.
@@ -228,7 +228,7 @@ This guide walks through a complete subscription billing flow: defining plans, s
- ## Listen to changes
+ Listen to changes
Add `on` handlers to your PayKit instance to react to any billing change. `customer.updated` fires after every subscription or entitlement update, including webhook-driven changes.
diff --git a/apps/web/content/docs/concepts/subscriptions.mdx b/apps/web/content/docs/subscriptions.mdx
similarity index 100%
rename from apps/web/content/docs/concepts/subscriptions.mdx
rename to apps/web/content/docs/subscriptions.mdx
diff --git a/apps/web/content/docs/concepts/typescript.mdx b/apps/web/content/docs/typescript.mdx
similarity index 99%
rename from apps/web/content/docs/concepts/typescript.mdx
rename to apps/web/content/docs/typescript.mdx
index 87b83078..5375733d 100644
--- a/apps/web/content/docs/concepts/typescript.mdx
+++ b/apps/web/content/docs/typescript.mdx
@@ -32,7 +32,7 @@ await paykit.subscribe({ customerId: "user_123", planId: "typo" });
Your editor's autocomplete also knows every valid plan and feature ID across the whole app.
-## The `$infer` helper
+## The $infer helper
`paykit.$infer` exposes the inferred union types so you can use them elsewhere in your app without re-declaring them.
diff --git a/apps/web/content/docs/concepts/webhook-events.mdx b/apps/web/content/docs/webhook-events.mdx
similarity index 94%
rename from apps/web/content/docs/concepts/webhook-events.mdx
rename to apps/web/content/docs/webhook-events.mdx
index 741546c2..7f427c7d 100644
--- a/apps/web/content/docs/concepts/webhook-events.mdx
+++ b/apps/web/content/docs/webhook-events.mdx
@@ -32,7 +32,7 @@ export const paykit = createPayKit({
});
```
-## `customer.updated`
+## customer.updated
Fires after any subscription or entitlement change. Use it to sync billing state into your own data layer, invalidate caches, or trigger downstream logic.
@@ -60,7 +60,7 @@ export const paykit = createPayKit({
## Webhook route
-The webhook endpoint is exposed by paykit handler at `/paykit/webhook` by default. See the [installation page](/docs/get-started/installation) for details on mounting it.
+The webhook endpoint is exposed by paykit handler at `/paykit/webhook` by default. See the [installation page](/docs/installation) for details on mounting it.
## Idempotency
diff --git a/apps/web/content/drafts/docs-index.mdx b/apps/web/content/drafts/docs-index.mdx
index 97f7e4a7..1b0f2c26 100644
--- a/apps/web/content/drafts/docs-index.mdx
+++ b/apps/web/content/drafts/docs-index.mdx
@@ -34,8 +34,8 @@ Subscriptions are intentionally **not** the core docs story right now.
- [Code](/docs/code/database)
The local billing-state model plus the MVP APIs for checkout, customers, payment methods,
charges, and webhooks.
-- [Providers](/docs/providers/stripe)
- Provider-specific setup notes and current integration status.
+- [Payment Providers](/docs/concepts/payment-providers)
+ Stripe setup notes and current integration status.
- [Code](/docs/code/sdks/server-sdk)
The planned server SDK, future React SDK direction, and framework handler examples.
diff --git a/apps/web/mdx-components.tsx b/apps/web/mdx-components.tsx
index 9bc2bcb1..fd718717 100644
--- a/apps/web/mdx-components.tsx
+++ b/apps/web/mdx-components.tsx
@@ -1,20 +1,10 @@
-import { Callout } from "fumadocs-ui/components/callout";
-import { Card, Cards } from "fumadocs-ui/components/card";
-import { Step, Steps } from "fumadocs-ui/components/steps";
-import { Tab, Tabs } from "fumadocs-ui/components/tabs";
-import defaultMdxComponents from "fumadocs-ui/mdx";
import type { MDXComponents } from "mdx/types";
+import { docsMdxComponents } from "@/components/docs/docs-mdx-components";
+
export function useMDXComponents(components?: MDXComponents): MDXComponents {
return {
- ...defaultMdxComponents,
+ ...docsMdxComponents,
...components,
- Callout,
- Card,
- Cards,
- Step,
- Steps,
- Tab,
- Tabs,
};
}
diff --git a/apps/web/next.config.js b/apps/web/next.config.js
index fa766063..a19c60c1 100644
--- a/apps/web/next.config.js
+++ b/apps/web/next.config.js
@@ -8,6 +8,36 @@ import "./src/env.js";
const withMDX = createMDX();
const currentDir = dirname(fileURLToPath(import.meta.url));
const repoRoot = join(currentDir, "../..");
+const remixLucideShim = join(currentDir, "src/lib/lucide-react-remix-shim.ts");
+
+const docsRedirects = [
+ { source: "/docs", destination: "/docs/introduction", permanent: true },
+ { source: "/docs/get-started", destination: "/docs/introduction", permanent: true },
+ { source: "/docs/get-started/installation", destination: "/docs/installation", permanent: true },
+ { source: "/docs/get-started/quickstart", destination: "/docs/quickstart", permanent: true },
+ {
+ source: "/docs/concepts/plans-and-features",
+ destination: "/docs/plans-and-features",
+ permanent: true,
+ },
+ { source: "/docs/concepts/customers", destination: "/docs/customers", permanent: true },
+ { source: "/docs/concepts/subscriptions", destination: "/docs/subscriptions", permanent: true },
+ { source: "/docs/concepts/entitlements", destination: "/docs/entitlements", permanent: true },
+ { source: "/docs/concepts/webhook-events", destination: "/docs/webhook-events", permanent: true },
+ { source: "/docs/concepts/database", destination: "/docs/database", permanent: true },
+ { source: "/docs/concepts/plugins", destination: "/docs/plugins", permanent: true },
+ { source: "/docs/concepts/client", destination: "/docs/client", permanent: true },
+ { source: "/docs/concepts/cli", destination: "/docs/cli", permanent: true },
+ { source: "/docs/concepts/typescript", destination: "/docs/typescript", permanent: true },
+ {
+ source: "/docs/flows/subscription-billing",
+ destination: "/docs/subscription-billing",
+ permanent: true,
+ },
+ { source: "/docs/flows/metered-usage", destination: "/docs/metered-usage", permanent: true },
+ { source: "/docs/plugins/dashboard", destination: "/docs/dashboard", permanent: true },
+ { source: "/docs/guides/skills", destination: "/docs/skills", permanent: true },
+];
/** @type {import("next").NextConfig} */
const config = {
@@ -17,6 +47,13 @@ const config = {
outputFileTracingRoot: repoRoot,
turbopack: {
root: repoRoot,
+ resolveAlias: {
+ "lucide-react": "./src/lib/lucide-react-remix-shim.ts",
+ },
+ },
+ webpack: (config) => {
+ config.resolve.alias["lucide-react"] = remixLucideShim;
+ return config;
},
experimental: {
optimizePackageImports: [
@@ -30,6 +67,7 @@ const config = {
],
},
redirects: async () => [
+ ...docsRedirects,
{ source: "/github", destination: "https://github.com/getpaykit/paykit", permanent: false },
{ source: "/discord", destination: "https://discord.gg/nzy9NPpFNU", permanent: false },
{ source: "/x", destination: "https://x.com/paykit_sh", permanent: false },
@@ -43,6 +81,8 @@ const config = {
destination: "https://github.com/orgs/getpaykit/projects/1",
permanent: false,
},
+ { source: "/donate", destination: "/sponsor", permanent: true },
+ { source: "/sponsors", destination: "/sponsor", permanent: true },
],
};
diff --git a/apps/web/package.json b/apps/web/package.json
index 4d970ae2..9739e58e 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -21,6 +21,7 @@
"@hugeicons/core-free-icons": "^3.3.0",
"@hugeicons/react": "^1.1.5",
"@t3-oss/env-nextjs": "^0.12.0",
+ "@tanstack/react-hotkeys": "^0.10.0",
"@types/mdx": "^2.0.13",
"@vercel/analytics": "^1.6.1",
"@vercel/speed-insights": "^2.0.0",
@@ -30,9 +31,9 @@
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
"framer-motion": "^12.34.3",
- "fumadocs-core": "^16.7.11",
- "fumadocs-mdx": "^14.2.11",
- "fumadocs-ui": "^16.7.11",
+ "fumadocs-core": "^16.9.3",
+ "fumadocs-mdx": "^15.0.10",
+ "fumadocs-ui": "^16.9.3",
"geist": "^1.3.1",
"input-otp": "^1.4.2",
"lucide-react": "^0.575.0",
diff --git a/apps/web/source.config.ts b/apps/web/source.config.ts
index 784fb0c1..3a6ea149 100644
--- a/apps/web/source.config.ts
+++ b/apps/web/source.config.ts
@@ -1,3 +1,4 @@
+import { rehypeCodeDefaultOptions } from "fumadocs-core/mdx-plugins/rehype-code";
import { defineConfig, defineDocs } from "fumadocs-mdx/config";
import { shikiThemes } from "./src/lib/shiki-themes";
@@ -15,6 +16,14 @@ export default defineConfig({
mdxOptions: {
rehypeCodeOptions: {
themes: shikiThemes,
+ transformers: [
+ ...(rehypeCodeDefaultOptions.transformers ?? []),
+ {
+ pre(node) {
+ node.properties["data-language"] = this.options.lang;
+ },
+ },
+ ],
},
},
});
diff --git a/apps/web/src/app/(marketing)/blog/page.tsx b/apps/web/src/app/(marketing)/blog/page.tsx
new file mode 100644
index 00000000..b817925a
--- /dev/null
+++ b/apps/web/src/app/(marketing)/blog/page.tsx
@@ -0,0 +1,42 @@
+import type { Metadata } from "next";
+import Link from "next/link";
+import { RiArrowLeftLine } from "react-icons/ri";
+
+import { Section, SectionContent } from "@/components/layout/section";
+import { Button } from "@/components/ui/button";
+
+export const metadata: Metadata = {
+ title: "Blog",
+ description: "PayKit blog is coming soon.",
+ alternates: {
+ canonical: "/blog",
+ },
+};
+
+export default function BlogPage() {
+ return (
+
+
+
+
+
+ Coming soon
+
+
+ Blog is not ready yet
+
+
+ Notes, guides, engineering deep dives, and product updates will land here soon.
+
+
+ } nativeButton={false}>
+
+ Go home
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/(marketing)/contact/contact-form.tsx b/apps/web/src/app/(marketing)/contact/contact-form.tsx
index 36dd61df..54a7aebb 100644
--- a/apps/web/src/app/(marketing)/contact/contact-form.tsx
+++ b/apps/web/src/app/(marketing)/contact/contact-form.tsx
@@ -1,8 +1,8 @@
"use client";
import { AnimatePresence, motion } from "framer-motion";
-import { Check, Loader2 } from "lucide-react";
import { useActionState } from "react";
+import { RiCheckLine, RiLoader4Line } from "react-icons/ri";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -30,7 +30,7 @@ export function ContactForm() {
className="flex flex-col items-center gap-3 py-12 text-center"
>
-
+
We received your message.
We will get back to you as soon as possible.
@@ -87,7 +87,7 @@ export function ContactForm() {
{state?.error &&
{state.error}
}
)}
diff --git a/apps/web/src/app/(marketing)/donate/page.tsx b/apps/web/src/app/(marketing)/donate/page.tsx
deleted file mode 100644
index f428400f..00000000
--- a/apps/web/src/app/(marketing)/donate/page.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import type { Metadata } from "next";
-import Link from "next/link";
-
-import { Section, SectionContent } from "@/components/layout/section";
-import { FooterSection } from "@/components/sections/footer-section";
-
-const donateUrl = "https://opencollective.com/maxktz";
-
-export const metadata: Metadata = {
- title: "Donate",
- description: "Support PayKit development through Open Collective.",
- alternates: {
- canonical: "/donate",
- },
-};
-
-export default function DonatePage() {
- return (
-
-
-
-
-
-
-
-
- Redirecting to Open Collective...
-
-
- Support ongoing PayKit development on Open Collective. If you are not redirected
- automatically, use the fallback link below.
-
-
-
-
-
- Continue to Open Collective
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(marketing)/layout.tsx b/apps/web/src/app/(marketing)/layout.tsx
index 520afabd..b54c4f19 100644
--- a/apps/web/src/app/(marketing)/layout.tsx
+++ b/apps/web/src/app/(marketing)/layout.tsx
@@ -1,18 +1,15 @@
import type { ReactNode } from "react";
-import { CommandMenuProvider } from "@/components/command-menu";
import { NavigationBar } from "@/components/layout/navigation-bar";
import { PageTransition } from "@/components/layout/page-transition";
export default function MarketingLayout({ children }: { children: ReactNode }) {
return (
-
-
-
-
- {children}
-
-
-
+
+
+
+ {children}
+
+
);
}
diff --git a/apps/web/src/app/(marketing)/page.tsx b/apps/web/src/app/(marketing)/page.tsx
index e648b2ee..bbb8d394 100644
--- a/apps/web/src/app/(marketing)/page.tsx
+++ b/apps/web/src/app/(marketing)/page.tsx
@@ -1,6 +1,6 @@
import { CTASection } from "@/components/sections/cta-section";
import { DemoSection } from "@/components/sections/demo";
-import { FeaturesSection } from "@/components/sections/features-section";
+import { FeedbackSection } from "@/components/sections/feedback-section";
import { FooterSection } from "@/components/sections/footer-section";
import { HeroSection } from "@/components/sections/hero-section";
import { demoSnippets } from "@/components/sections/readme-code-content";
@@ -17,7 +17,7 @@ export default function HomePage() {
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
))}
-
+
,
}}
/>
-
+
diff --git a/apps/web/src/app/(marketing)/sponsor/page.tsx b/apps/web/src/app/(marketing)/sponsor/page.tsx
new file mode 100644
index 00000000..9b555eb7
--- /dev/null
+++ b/apps/web/src/app/(marketing)/sponsor/page.tsx
@@ -0,0 +1,42 @@
+import type { Metadata } from "next";
+import Link from "next/link";
+import { RiArrowLeftLine } from "react-icons/ri";
+
+import { Section, SectionContent } from "@/components/layout/section";
+import { Button } from "@/components/ui/button";
+
+export const metadata: Metadata = {
+ title: "Sponsors",
+ description: "PayKit sponsors page is coming soon.",
+ alternates: {
+ canonical: "/sponsor",
+ },
+};
+
+export default function SponsorPage() {
+ return (
+
+
+
+
+
+ Coming soon
+
+
+ Sponsors page is not ready yet
+
+
+ Sponsor information and acknowledgements will be available soon.
+
+
+ } nativeButton={false}>
+
+ Go home
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/api/og/[[...slug]]/route.tsx b/apps/web/src/app/api/og/[[...slug]]/route.tsx
index af140200..3f7fe134 100644
--- a/apps/web/src/app/api/og/[[...slug]]/route.tsx
+++ b/apps/web/src/app/api/og/[[...slug]]/route.tsx
@@ -6,6 +6,8 @@ import { ImageResponse } from "next/og";
import { source } from "@/lib/source";
+export const revalidate = false;
+
export function generateStaticParams() {
return source.generateParams();
}
diff --git a/apps/web/src/app/docs/[[...slug]]/page.tsx b/apps/web/src/app/docs/[[...slug]]/page.tsx
index 09cc1f89..b2fbb6ad 100644
--- a/apps/web/src/app/docs/[[...slug]]/page.tsx
+++ b/apps/web/src/app/docs/[[...slug]]/page.tsx
@@ -1,36 +1,23 @@
-import { Callout as BaseCallout } from "fumadocs-ui/components/callout";
-import { Card, Cards } from "fumadocs-ui/components/card";
-import { Step, Steps } from "fumadocs-ui/components/steps";
-import { Tab, Tabs } from "fumadocs-ui/components/tabs";
-import { DocsBody, DocsDescription, DocsPage, DocsTitle } from "fumadocs-ui/layouts/docs/page";
-import defaultMdxComponents from "fumadocs-ui/mdx";
import type { Metadata } from "next";
-import { notFound, redirect } from "next/navigation";
-import type { ComponentPropsWithoutRef } from "react";
+import { notFound } from "next/navigation";
import { CopyMarkdownButton } from "@/components/docs/copy-markdown-button";
-import { Features } from "@/components/docs/features";
-import { PackageInstall, PackageRun } from "@/components/docs/package-command";
-import { TocFooter } from "@/components/docs/toc-footer";
+import { docsMdxComponents } from "@/components/docs/docs-mdx-components";
+import { DocsBody, DocsDescription, DocsPage, DocsTitle } from "@/components/docs/docs-page";
+import { PackageManagerProvider } from "@/components/docs/package-command";
+import { fallbackPackageManager } from "@/components/docs/package-manager-state";
import type { SourcePage } from "@/lib/source";
import { source } from "@/lib/source";
-import { cn } from "@/lib/utils";
interface DocsPageProps {
params: Promise<{ slug?: string[] }>;
}
-function Callout(props: ComponentPropsWithoutRef) {
- return ;
-}
+export const revalidate = false;
export default async function Page({ params }: DocsPageProps) {
const { slug } = await params;
- if (!slug || slug.length === 0) {
- redirect("/docs/get-started");
- }
-
const page = source.getPage(slug ?? []) as SourcePage | undefined;
if (!page) notFound();
@@ -45,38 +32,21 @@ export default async function Page({ params }: DocsPageProps) {
}}
toc={page.data.toc}
full={page.data.full}
- tableOfContent={{
- footer: ,
- style: "clerk",
- }}
- tableOfContentPopover={{
- style: "clerk",
- }}
>
- {page.data.title}
+
);
}
diff --git a/apps/web/src/components/docs/docs-code-surface.tsx b/apps/web/src/components/docs/docs-code-surface.tsx
new file mode 100644
index 00000000..8e7e4826
--- /dev/null
+++ b/apps/web/src/components/docs/docs-code-surface.tsx
@@ -0,0 +1,23 @@
+import type { ComponentProps } from "react";
+
+import { cn } from "@/lib/utils";
+
+/**
+ * Renders the styled `
` wrapper used by docs code blocks.
+ *
+ * @param props - Standard pre props, including `className` and `tabIndex`.
+ * Remaining props are spread onto the `
` element. Defaults `tabIndex` to
+ * `0` when undefined so code blocks are keyboard focusable.
+ */
+export function DocsCodeSurface({ className, tabIndex, ...props }: ComponentProps<"pre">) {
+ return (
+