{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "stripe-billing",
  "type": "registry:block",
  "title": "Stripe Billing",
  "description": "Stripe checkout, billing portal, subscription sync, webhook dedupe, and entitlement mapping.",
  "dependencies": [
    "stripe"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://stackfoundry.dev/r/drizzle-postgres.json"
  ],
  "files": [
    {
      "path": "packages/db/src/schema/billing.ts",
      "type": "registry:file",
      "target": "packages/db/src/schema/billing.ts",
      "content": "import { pgTable, text, timestamp, uuid } from \"drizzle-orm/pg-core\";\n\nexport const billingCustomers = pgTable(\"billing_customers\", {\n  id: uuid(\"id\").primaryKey().defaultRandom(),\n  ownerId: text(\"owner_id\").notNull(),\n  provider: text(\"provider\").notNull().default(\"stripe\"),\n  providerCustomerId: text(\"provider_customer_id\").notNull(),\n  createdAt: timestamp(\"created_at\", { withTimezone: true }).defaultNow().notNull(),\n});\n\nexport const subscriptions = pgTable(\"subscriptions\", {\n  id: uuid(\"id\").primaryKey().defaultRandom(),\n  ownerId: text(\"owner_id\").notNull(),\n  provider: text(\"provider\").notNull().default(\"stripe\"),\n  providerSubscriptionId: text(\"provider_subscription_id\").notNull(),\n  status: text(\"status\").notNull(),\n  currentPeriodEndsAt: timestamp(\"current_period_ends_at\", { withTimezone: true }),\n  createdAt: timestamp(\"created_at\", { withTimezone: true }).defaultNow().notNull(),\n});\n\nexport const webhookEvents = pgTable(\"webhook_events\", {\n  id: uuid(\"id\").primaryKey().defaultRandom(),\n  provider: text(\"provider\").notNull(),\n  providerEventId: text(\"provider_event_id\").notNull(),\n  eventType: text(\"event_type\").notNull(),\n  processedAt: timestamp(\"processed_at\", { withTimezone: true }),\n  createdAt: timestamp(\"created_at\", { withTimezone: true }).defaultNow().notNull(),\n});\n"
    },
    {
      "path": "apps/web/src/lib/stripe/client.ts",
      "type": "registry:file",
      "target": "apps/web/src/lib/stripe/client.ts",
      "content": "import \"server-only\";\n\nimport Stripe from \"stripe\";\n\nexport function getStripe() {\n  const secretKey = process.env.STRIPE_SECRET_KEY;\n  if (!secretKey) {\n    throw new Error(\"STRIPE_SECRET_KEY is not set.\");\n  }\n\n  return new Stripe(secretKey);\n}\n"
    },
    {
      "path": "apps/web/src/app/api/webhooks/stripe/route.ts",
      "type": "registry:file",
      "target": "apps/web/src/app/api/webhooks/stripe/route.ts",
      "content": "import { headers } from \"next/headers\";\n\nimport { getStripe } from \"@/lib/stripe/client\";\n\nexport async function POST(request: Request) {\n  const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;\n  if (!webhookSecret) {\n    return Response.json({ error: \"STRIPE_WEBHOOK_SECRET is not set.\" }, { status: 500 });\n  }\n\n  const signature = (await headers()).get(\"stripe-signature\");\n  if (!signature) {\n    return Response.json({ error: \"Missing stripe-signature header.\" }, { status: 400 });\n  }\n\n  const body = await request.text();\n  const event = getStripe().webhooks.constructEvent(body, signature, webhookSecret);\n\n  // Persist event.id before applying side effects so retries remain idempotent.\n  return Response.json({ received: true, id: event.id, type: event.type });\n}\n"
    },
    {
      "path": "apps/web/src/app/(console)/billing/page.tsx",
      "type": "registry:page",
      "target": "apps/web/src/app/(console)/billing/page.tsx",
      "content": "export default function BillingPage() {\n  return (\n    <main className=\"flex flex-col gap-4 p-6\">\n      <div>\n        <h1 className=\"text-2xl font-semibold\">Billing</h1>\n        <p className=\"text-muted-foreground\">Manage checkout, subscriptions, invoices, and entitlements.</p>\n      </div>\n    </main>\n  );\n}\n"
    }
  ],
  "maintenanceSkills": [
    {
      "name": "stripe-billing",
      "target": ".stackfoundry/skills/stripe-billing/SKILL.md",
      "content": "---\nname: stripe-billing\ndescription: Maintain the Stripe billing module installed by StackFoundry.\n---\n\n# Stripe Billing Operating Instructions\n\n- Verify all Stripe webhook signatures.\n- Store and check webhook event ids for idempotency.\n- Map Stripe subscription state into internal entitlement state.\n- Do not trust client-provided price ids without validating them against server config.\n- Keep checkout, portal, webhook, and entitlement changes documented together.\n- Test subscription created, updated, canceled, payment failed, and checkout completed flows.\n\n## Shared Skills\n\nWhen provider, framework, or database behavior changes, load the installed shared skill before editing implementation details:\n\n- `.stackfoundry/skills/stripe/SKILL.md` (source: `registry/skills/stripe/SKILL.md`)\n- `.stackfoundry/skills/drizzle/SKILL.md` (source: `registry/skills/drizzle/SKILL.md`)\n- `.stackfoundry/skills/nextjs/SKILL.md` (source: `registry/skills/nextjs/SKILL.md`)\n\nKeep this module skill focused on ownership, installed files, env vars, deployment checks, and module-specific invariants.\n\n"
    },
    {
      "name": "stripe",
      "target": ".stackfoundry/skills/stripe/SKILL.md",
      "content": "---\nname: stripe\ndescription: Maintain Stripe integrations installed by StackFoundry modules.\n---\n\n# Stripe Operating Instructions\n\n## Installed Location\n\n- Installed target: `.stackfoundry/skills/stripe/SKILL.md`\n- Registry source: `registry/skills/stripe/SKILL.md`\n\nAgents maintaining an installed module should load this shared skill from the installed target when provider, framework, database, SDK, or platform behavior is involved. Keep provider-specific API details here instead of duplicating them inside module maintenance skills.\n\n- Verify webhook signatures before reading event payloads.\n- Persist webhook event ids and make handlers idempotent.\n- Treat prices, products, customer ids, and subscription ids as server-owned state.\n- Never trust client-provided price ids without validating them against server configuration.\n- Test checkout completed, subscription updated, subscription canceled, and payment failed flows.\n"
    },
    {
      "name": "drizzle",
      "target": ".stackfoundry/skills/drizzle/SKILL.md",
      "content": "---\nname: drizzle\ndescription: Maintain Drizzle ORM and Postgres code installed by StackFoundry modules.\n---\n\n# Drizzle Operating Instructions\n\n## Installed Location\n\n- Installed target: `.stackfoundry/skills/drizzle/SKILL.md`\n- Registry source: `registry/skills/drizzle/SKILL.md`\n\nAgents maintaining an installed module should load this shared skill from the installed target when provider, framework, database, SDK, or platform behavior is involved. Keep provider-specific API details here instead of duplicating them inside module maintenance skills.\n\n- Keep database access in server-only code.\n- Add schema changes under `packages/db/src/schema` and export shared tables from the schema barrel.\n- Generate and commit migrations when schema changes are intended.\n- Use typed query helpers instead of raw SQL unless the query needs a documented escape hatch.\n- Include tenant, organization, or user scope in queries and cache tags whenever data is not global.\n"
    },
    {
      "name": "nextjs",
      "target": ".stackfoundry/skills/nextjs/SKILL.md",
      "content": "---\nname: nextjs\ndescription: Maintain Next.js App Router code installed by StackFoundry modules.\n---\n\n# Next.js Operating Instructions\n\n## Installed Location\n\n- Installed target: `.stackfoundry/skills/nextjs/SKILL.md`\n- Registry source: `registry/skills/nextjs/SKILL.md`\n\nAgents maintaining an installed module should load this shared skill from the installed target when provider, framework, database, SDK, or platform behavior is involved. Keep provider-specific API details here instead of duplicating them inside module maintenance skills.\n\n- Keep server-only data access out of Client Components.\n- Put route handlers under `app/api` and UI routes under the relevant App Router segment.\n- Prefer Server Components for data loading and add `\"use client\"` only for interactivity.\n- Keep public environment variables prefixed with `NEXT_PUBLIC_`; keep secrets server-only.\n- Re-run typecheck and build after changing route handlers, layouts, or shared app configuration.\n"
    }
  ],
  "envVars": {
    "STRIPE_SECRET_KEY": "",
    "STRIPE_WEBHOOK_SECRET": "",
    "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY": ""
  },
  "docs": "# Stripe Billing Module\n\nAdds Stripe checkout, customer portal, subscription sync, webhook handling, and entitlement mapping.\n\nThis is the first concrete payment-provider adapter for the source-owned billing modules. Keep it optional and isolated so teams can choose Stripe directly, Autumn on top of Stripe, or another billing adapter.\n\n## Owns\n\n- billing tables\n- Stripe client wrapper\n- checkout and portal actions\n- webhook route\n- event dedupe\n- plan/entitlement mapping\n\n## Environment\n\nInstalling this module generates `.env.stackfoundry.stripe-billing.example` with:\n\n- `STRIPE_SECRET_KEY`\n- `STRIPE_WEBHOOK_SECRET`\n- `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY`\n\nKeep secret keys server-only and configure webhook secrets separately for preview and production.\n\n## Rules\n\n- Verify webhook signatures.\n- Dedupe events before applying side effects.\n- Never trust plan or price values from the client.\n- Keep billing provider code behind `billing-core`.\n",
  "meta": {
    "category": "billing",
    "env": [
      "STRIPE_SECRET_KEY",
      "STRIPE_WEBHOOK_SECRET",
      "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY"
    ],
    "status": "ready",
    "maturity": "ready",
    "drizzle": {
      "schemaExports": [
        "billingCustomers",
        "subscriptions",
        "webhookEvents"
      ],
      "migrationRecommended": true
    },
    "recommendedFor": []
  }
}
