@skill-graph/cli 0.5.6

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 (330) hide show
  1. package/CHANGELOG.md +247 -0
  2. package/LICENSE +200 -0
  3. package/NOTICE +62 -0
  4. package/README.md +398 -0
  5. package/SKILL_GRAPH.md +443 -0
  6. package/bin/skill-graph.js +374 -0
  7. package/docs/ADOPTION.md +117 -0
  8. package/docs/CONFORMANCE.md +66 -0
  9. package/docs/PRIMER.md +384 -0
  10. package/docs/QUICKSTART-30MIN.md +333 -0
  11. package/docs/ROUTING-METRICS.md +120 -0
  12. package/docs/SKILL-MD-FORMAT-COMPATIBILITY.md +127 -0
  13. package/docs/SKILL_AUDIT_CHECKLIST.md +199 -0
  14. package/docs/SKILL_AUDIT_LOOP.md +195 -0
  15. package/docs/SKILL_METADATA_PROTOCOL.md +609 -0
  16. package/docs/_archived/marketplace-publication-priority-2026-05-18.md +239 -0
  17. package/docs/adr/0001-predicate-set.md +69 -0
  18. package/docs/adr/0002-json-ld-context.md +82 -0
  19. package/docs/adr/0003-ontoclean-rigidity-tags.md +65 -0
  20. package/docs/adr/0004-persistent-identifiers.md +74 -0
  21. package/docs/adr/0005-freshness-consolidation.md +70 -0
  22. package/docs/adr/0006-revise-predicate-rename.md +105 -0
  23. package/docs/adr/0007-audit-loop-cadence.md +99 -0
  24. package/docs/adr/0008-skill-surface-split-and-curation-policy.md +93 -0
  25. package/docs/category-consumers.md +168 -0
  26. package/docs/concept-map.md +194 -0
  27. package/docs/diagrams/drift-states.mmd +21 -0
  28. package/docs/diagrams/manifest-pipeline.mmd +25 -0
  29. package/docs/diagrams/routing-harness.mmd +41 -0
  30. package/docs/diagrams/starter-graph.mmd +53 -0
  31. package/docs/field-decision-guide.md +315 -0
  32. package/docs/field-rationale.md +211 -0
  33. package/docs/field-reference.generated.md +624 -0
  34. package/docs/field-reference.md +1426 -0
  35. package/docs/glossary.md +190 -0
  36. package/docs/head-noun-glossary.md +63 -0
  37. package/docs/images/audit-phases.png +0 -0
  38. package/docs/images/drift-states.png +0 -0
  39. package/docs/images/graded-mode.png +0 -0
  40. package/docs/images/manifest-pipeline.png +0 -0
  41. package/docs/images/routing-harness.png +0 -0
  42. package/docs/images/skill-anatomy.png +0 -0
  43. package/docs/images/starter-graph.png +0 -0
  44. package/docs/images/system-model.png +0 -0
  45. package/docs/integrations/github-actions.md +155 -0
  46. package/docs/manifest-field-mapping.md +443 -0
  47. package/docs/marketplace-publication-queue.generated.md +240 -0
  48. package/docs/marketplace-release-agent-prompt.md +82 -0
  49. package/docs/marketplace-skill-candidate-list.md +272 -0
  50. package/docs/marketplace-syndication.md +222 -0
  51. package/docs/migration-sample-review.md +155 -0
  52. package/docs/migrations/v4-to-v5.md +168 -0
  53. package/docs/migrations/v5-to-v6.md +221 -0
  54. package/docs/name-exceptions.yaml +37 -0
  55. package/docs/plans/marketplace-p1-public-migration-plan.md +41 -0
  56. package/docs/plans/multi-root-workspace.md +148 -0
  57. package/docs/plans/scripts-roadmap.md +107 -0
  58. package/docs/plans/v4-schema-bump.md +160 -0
  59. package/docs/plans/wave-2-extraction.md +122 -0
  60. package/docs/positioning-vs-marketplaces.md +175 -0
  61. package/docs/proposals/skill-audit-loop-positioning.md +160 -0
  62. package/docs/quality-doctrine.md +138 -0
  63. package/docs/recommended-skills.md +150 -0
  64. package/docs/research/skill-comprehension-eval-research.md +1830 -0
  65. package/docs/research/skill-retrieval-evidence.md +66 -0
  66. package/docs/skill-metadata-protocol.md +471 -0
  67. package/docs/skills-sh-maintainer-cleanup-request.md +80 -0
  68. package/examples/audits/a11y/findings.md +52 -0
  69. package/examples/audits/a11y/scorecard.md +21 -0
  70. package/examples/audits/a11y/verdict.md +44 -0
  71. package/examples/audits/debugging/findings.md +59 -0
  72. package/examples/audits/debugging/scorecard.md +22 -0
  73. package/examples/audits/debugging/verdict.md +33 -0
  74. package/examples/audits/documentation/findings.md +59 -0
  75. package/examples/audits/documentation/scorecard.md +22 -0
  76. package/examples/audits/documentation/verdict.md +33 -0
  77. package/examples/evals/a11y.json +140 -0
  78. package/examples/evals/api-design.json +52 -0
  79. package/examples/evals/code-review.json +52 -0
  80. package/examples/evals/data-modeling.json +52 -0
  81. package/examples/evals/database-migration.json +52 -0
  82. package/examples/evals/debugging.json +118 -0
  83. package/examples/evals/dependency-architecture.json +52 -0
  84. package/examples/evals/design-system-architecture.json +52 -0
  85. package/examples/evals/error-tracking.json +52 -0
  86. package/examples/evals/event-contract-design.json +52 -0
  87. package/examples/evals/form-ux-architecture.json +52 -0
  88. package/examples/evals/framework-fit-analysis.json +52 -0
  89. package/examples/evals/graph-audit.json +139 -0
  90. package/examples/evals/information-architecture.json +52 -0
  91. package/examples/evals/interaction-feedback.json +52 -0
  92. package/examples/evals/interaction-patterns.json +52 -0
  93. package/examples/evals/layout-composition.json +52 -0
  94. package/examples/evals/lint-overlay.json +117 -0
  95. package/examples/evals/microcopy.json +52 -0
  96. package/examples/evals/observability-modeling.json +52 -0
  97. package/examples/evals/pattern-recognition.json +96 -0
  98. package/examples/evals/performance-engineering.json +52 -0
  99. package/examples/evals/refactor.json +128 -0
  100. package/examples/evals/semiotics.json +52 -0
  101. package/examples/evals/skill-infrastructure.json +96 -0
  102. package/examples/evals/skill-router.json +140 -0
  103. package/examples/evals/skill-router.routing.json +113 -0
  104. package/examples/evals/system-interface-contracts.json +52 -0
  105. package/examples/evals/task-analysis.json +52 -0
  106. package/examples/evals/testing-strategy.json +118 -0
  107. package/examples/evals/type-safety.json +249 -0
  108. package/examples/evals/visual-design-foundations.json +52 -0
  109. package/examples/evals/webhook-integration.json +52 -0
  110. package/examples/exports/a11y.skill-md.md +80 -0
  111. package/examples/exports/debugging.skill-md.md +80 -0
  112. package/examples/exports/refactor.skill-md.md +78 -0
  113. package/examples/exports/testing-strategy.skill-md.md +81 -0
  114. package/examples/projects/markdown-static-site/README.md +115 -0
  115. package/examples/projects/markdown-static-site/skills/content-source-router/SKILL.md +131 -0
  116. package/examples/projects/markdown-static-site/skills/image-optimization-pipeline-config/SKILL.md +132 -0
  117. package/examples/projects/markdown-static-site/skills/link-rot-detection/SKILL.md +103 -0
  118. package/examples/projects/markdown-static-site/skills/markdown-post-frontmatter-validation/SKILL.md +133 -0
  119. package/examples/projects/markdown-static-site/skills/migrate-posts-to-v2-frontmatter/SKILL.md +140 -0
  120. package/examples/projects/saas-stripe-postgres/README.md +208 -0
  121. package/examples/projects/saas-stripe-postgres/db/migrations/0004_canonicalize_orders.sql +37 -0
  122. package/examples/projects/saas-stripe-postgres/db/schema.sql +112 -0
  123. package/examples/projects/saas-stripe-postgres/skills/migrate-orders-to-canonical-schema/SKILL.md +149 -0
  124. package/examples/projects/saas-stripe-postgres/skills/nextjs-server-action-validation/SKILL.md +154 -0
  125. package/examples/projects/saas-stripe-postgres/skills/payment-provider-router/SKILL.md +153 -0
  126. package/examples/projects/saas-stripe-postgres/skills/postgres-rls-pattern/SKILL.md +163 -0
  127. package/examples/projects/saas-stripe-postgres/skills/stripe-webhook-signature-verification/SKILL.md +137 -0
  128. package/examples/protocol/skill-metadata-template.md +301 -0
  129. package/examples/protocol/skills.manifest.sample.json +13245 -0
  130. package/examples/skill-metadata-template.md +317 -0
  131. package/examples/skills.manifest.sample.json +13519 -0
  132. package/examples/tests/v3-1-skos-fixture/SKILL.md +93 -0
  133. package/marketplace/README.md +17 -0
  134. package/marketplace/skills/a11y/SKILL.md +66 -0
  135. package/marketplace/skills/acid-fundamentals/SKILL.md +106 -0
  136. package/marketplace/skills/agent-engineering/SKILL.md +386 -0
  137. package/marketplace/skills/agent-eval-design/SKILL.md +55 -0
  138. package/marketplace/skills/ai-native-development/SKILL.md +294 -0
  139. package/marketplace/skills/api-design/SKILL.md +60 -0
  140. package/marketplace/skills/architecture-decision-records/SKILL.md +55 -0
  141. package/marketplace/skills/background-jobs/SKILL.md +265 -0
  142. package/marketplace/skills/bounded-context-mapping/SKILL.md +55 -0
  143. package/marketplace/skills/cap-theorem-tradeoffs/SKILL.md +127 -0
  144. package/marketplace/skills/client-server-boundary/SKILL.md +187 -0
  145. package/marketplace/skills/code-review/SKILL.md +120 -0
  146. package/marketplace/skills/color-system-design/SKILL.md +43 -0
  147. package/marketplace/skills/component-architecture/SKILL.md +126 -0
  148. package/marketplace/skills/compression/SKILL.md +112 -0
  149. package/marketplace/skills/conceptual-modeling/SKILL.md +181 -0
  150. package/marketplace/skills/connection-pooling/SKILL.md +105 -0
  151. package/marketplace/skills/constraint-awareness/SKILL.md +287 -0
  152. package/marketplace/skills/content-monitor/SKILL.md +209 -0
  153. package/marketplace/skills/context-engineering/SKILL.md +320 -0
  154. package/marketplace/skills/context-graph/SKILL.md +174 -0
  155. package/marketplace/skills/context-management/SKILL.md +174 -0
  156. package/marketplace/skills/context-window/SKILL.md +239 -0
  157. package/marketplace/skills/contract-testing/SKILL.md +120 -0
  158. package/marketplace/skills/cron-scheduling/SKILL.md +223 -0
  159. package/marketplace/skills/dark-mode-implementation/SKILL.md +47 -0
  160. package/marketplace/skills/data-modeling/SKILL.md +59 -0
  161. package/marketplace/skills/data-modeling-fundamentals/SKILL.md +117 -0
  162. package/marketplace/skills/database-migration/SKILL.md +429 -0
  163. package/marketplace/skills/debugging/SKILL.md +67 -0
  164. package/marketplace/skills/dependency-architecture/SKILL.md +58 -0
  165. package/marketplace/skills/design-module-composition/SKILL.md +43 -0
  166. package/marketplace/skills/design-system-architecture/SKILL.md +61 -0
  167. package/marketplace/skills/design-thinking/SKILL.md +44 -0
  168. package/marketplace/skills/diagnosis/SKILL.md +296 -0
  169. package/marketplace/skills/diff-analysis/SKILL.md +188 -0
  170. package/marketplace/skills/e2e-test-design/SKILL.md +113 -0
  171. package/marketplace/skills/entity-relationship-modeling/SKILL.md +218 -0
  172. package/marketplace/skills/epistemic-grounding/SKILL.md +112 -0
  173. package/marketplace/skills/error-boundary/SKILL.md +235 -0
  174. package/marketplace/skills/error-tracking/SKILL.md +261 -0
  175. package/marketplace/skills/eval-driven-development/SKILL.md +147 -0
  176. package/marketplace/skills/evaluation/SKILL.md +113 -0
  177. package/marketplace/skills/event-contract-design/SKILL.md +60 -0
  178. package/marketplace/skills/event-storming/SKILL.md +56 -0
  179. package/marketplace/skills/form-ux-architecture/SKILL.md +60 -0
  180. package/marketplace/skills/framework-fit-analysis/SKILL.md +59 -0
  181. package/marketplace/skills/frontend-architecture/SKILL.md +43 -0
  182. package/marketplace/skills/generative-ui/SKILL.md +118 -0
  183. package/marketplace/skills/graph-audit/SKILL.md +81 -0
  184. package/marketplace/skills/guardrails/SKILL.md +118 -0
  185. package/marketplace/skills/hooks-patterns/SKILL.md +185 -0
  186. package/marketplace/skills/http-semantics/SKILL.md +136 -0
  187. package/marketplace/skills/ideation/SKILL.md +41 -0
  188. package/marketplace/skills/indexing-strategy/SKILL.md +108 -0
  189. package/marketplace/skills/information-architecture/SKILL.md +59 -0
  190. package/marketplace/skills/integration-test-design/SKILL.md +111 -0
  191. package/marketplace/skills/intent-recognition/SKILL.md +136 -0
  192. package/marketplace/skills/interaction-feedback/SKILL.md +59 -0
  193. package/marketplace/skills/interaction-patterns/SKILL.md +59 -0
  194. package/marketplace/skills/journey-mapping/SKILL.md +41 -0
  195. package/marketplace/skills/keywords/SKILL.md +213 -0
  196. package/marketplace/skills/knowledge-modeling/SKILL.md +232 -0
  197. package/marketplace/skills/layout-composition/SKILL.md +59 -0
  198. package/marketplace/skills/linguistics/SKILL.md +429 -0
  199. package/marketplace/skills/lint-overlay/SKILL.md +76 -0
  200. package/marketplace/skills/mental-models/SKILL.md +126 -0
  201. package/marketplace/skills/merge-queue/SKILL.md +94 -0
  202. package/marketplace/skills/methodology/SKILL.md +317 -0
  203. package/marketplace/skills/microcopy/SKILL.md +232 -0
  204. package/marketplace/skills/middleware-patterns/SKILL.md +363 -0
  205. package/marketplace/skills/mobile-responsive-ux/SKILL.md +287 -0
  206. package/marketplace/skills/mutation-testing/SKILL.md +112 -0
  207. package/marketplace/skills/naming-conventions/SKILL.md +112 -0
  208. package/marketplace/skills/observability-modeling/SKILL.md +59 -0
  209. package/marketplace/skills/ontology-modeling/SKILL.md +67 -0
  210. package/marketplace/skills/owasp-security/SKILL.md +153 -0
  211. package/marketplace/skills/pattern-recognition/SKILL.md +472 -0
  212. package/marketplace/skills/performance-budgets/SKILL.md +185 -0
  213. package/marketplace/skills/performance-engineering/SKILL.md +58 -0
  214. package/marketplace/skills/performance-testing/SKILL.md +125 -0
  215. package/marketplace/skills/printify/SKILL.md +42 -0
  216. package/marketplace/skills/prioritization/SKILL.md +118 -0
  217. package/marketplace/skills/problem-framing/SKILL.md +41 -0
  218. package/marketplace/skills/problem-locating-solving/SKILL.md +203 -0
  219. package/marketplace/skills/project-knowledge-extraction/SKILL.md +54 -0
  220. package/marketplace/skills/prompt-craft/SKILL.md +134 -0
  221. package/marketplace/skills/prompt-injection-defense/SKILL.md +132 -0
  222. package/marketplace/skills/property-based-testing/SKILL.md +100 -0
  223. package/marketplace/skills/prototyping/SKILL.md +43 -0
  224. package/marketplace/skills/query-optimization/SKILL.md +144 -0
  225. package/marketplace/skills/real-time-updates/SKILL.md +324 -0
  226. package/marketplace/skills/ref-patterns/SKILL.md +284 -0
  227. package/marketplace/skills/refactor/SKILL.md +65 -0
  228. package/marketplace/skills/rendering-models/SKILL.md +142 -0
  229. package/marketplace/skills/replication-patterns/SKILL.md +110 -0
  230. package/marketplace/skills/research-synthesis/SKILL.md +41 -0
  231. package/marketplace/skills/route-handler-design/SKILL.md +347 -0
  232. package/marketplace/skills/schema-evolution/SKILL.md +140 -0
  233. package/marketplace/skills/security-fundamentals/SKILL.md +139 -0
  234. package/marketplace/skills/semantic-center/SKILL.md +194 -0
  235. package/marketplace/skills/semantic-relations/SKILL.md +250 -0
  236. package/marketplace/skills/semantics/SKILL.md +366 -0
  237. package/marketplace/skills/semiotics/SKILL.md +230 -0
  238. package/marketplace/skills/seo-strategy/SKILL.md +260 -0
  239. package/marketplace/skills/server-actions-design/SKILL.md +243 -0
  240. package/marketplace/skills/server-components-design/SKILL.md +190 -0
  241. package/marketplace/skills/sharding-strategy/SKILL.md +123 -0
  242. package/marketplace/skills/shopify/SKILL.md +42 -0
  243. package/marketplace/skills/skill-infrastructure/SKILL.md +320 -0
  244. package/marketplace/skills/skill-router/SKILL.md +71 -0
  245. package/marketplace/skills/skill-scaffold/SKILL.md +105 -0
  246. package/marketplace/skills/snapshot-testing/SKILL.md +120 -0
  247. package/marketplace/skills/spec-driven-development/SKILL.md +148 -0
  248. package/marketplace/skills/state-machine-modeling/SKILL.md +56 -0
  249. package/marketplace/skills/state-management/SKILL.md +134 -0
  250. package/marketplace/skills/streaming-architecture/SKILL.md +194 -0
  251. package/marketplace/skills/summarization/SKILL.md +156 -0
  252. package/marketplace/skills/suspense-patterns/SKILL.md +265 -0
  253. package/marketplace/skills/system-interface-contracts/SKILL.md +59 -0
  254. package/marketplace/skills/task-analysis/SKILL.md +201 -0
  255. package/marketplace/skills/taxonomy-design/SKILL.md +66 -0
  256. package/marketplace/skills/test-coverage-strategy/SKILL.md +108 -0
  257. package/marketplace/skills/test-doubles-design/SKILL.md +98 -0
  258. package/marketplace/skills/test-driven-development/SKILL.md +96 -0
  259. package/marketplace/skills/testing-strategy/SKILL.md +67 -0
  260. package/marketplace/skills/theme-system-design/SKILL.md +43 -0
  261. package/marketplace/skills/tool-call-flow/SKILL.md +229 -0
  262. package/marketplace/skills/tool-call-strategy/SKILL.md +292 -0
  263. package/marketplace/skills/transaction-isolation/SKILL.md +98 -0
  264. package/marketplace/skills/type-safety/SKILL.md +177 -0
  265. package/marketplace/skills/typography-system/SKILL.md +43 -0
  266. package/marketplace/skills/usability-testing/SKILL.md +43 -0
  267. package/marketplace/skills/user-research/SKILL.md +43 -0
  268. package/marketplace/skills/vercel-composition-patterns/SKILL.md +157 -0
  269. package/marketplace/skills/version-control/SKILL.md +233 -0
  270. package/marketplace/skills/visual-design-foundations/SKILL.md +59 -0
  271. package/marketplace/skills/visual-hierarchy/SKILL.md +43 -0
  272. package/marketplace/skills/webhook-integration/SKILL.md +331 -0
  273. package/marketplace/skills/writing-humanizer/SKILL.md +380 -0
  274. package/package.json +67 -0
  275. package/schemas/manifest.schema.json +811 -0
  276. package/schemas/manifest.v2.schema.json +164 -0
  277. package/schemas/manifest.v3.schema.json +758 -0
  278. package/schemas/manifest.v4.schema.json +755 -0
  279. package/schemas/manifest.v5.schema.json +755 -0
  280. package/schemas/manifest.v6.schema.json +811 -0
  281. package/schemas/skill.context.jsonld +279 -0
  282. package/schemas/skill.schema.json +919 -0
  283. package/schemas/skill.v2.schema.json +201 -0
  284. package/schemas/skill.v3.schema.json +827 -0
  285. package/schemas/skill.v4.schema.json +822 -0
  286. package/schemas/skill.v5.schema.json +830 -0
  287. package/schemas/skill.v6.schema.json +946 -0
  288. package/schemas/vocabulary/keywords.json +180 -0
  289. package/schemas/vocabulary/workspace_tags.json +23 -0
  290. package/scripts/__tests__/migrate-skill-v2-to-v3.test.js +161 -0
  291. package/scripts/__tests__/migrate-skill-v3-to-v4.test.js +158 -0
  292. package/scripts/__tests__/test-export-parser-drift.js +149 -0
  293. package/scripts/__tests__/test-marketplace-export.js +114 -0
  294. package/scripts/__tests__/test-router-paths.js +82 -0
  295. package/scripts/__tests__/test-stability-promotion.js +244 -0
  296. package/scripts/__tests__/test-v3-1-alias-contract.js +109 -0
  297. package/scripts/__tests__/test-v3-1-skos-runtime.js +116 -0
  298. package/scripts/backfill-schema-version.js +198 -0
  299. package/scripts/build-field-reference.js +160 -0
  300. package/scripts/build-retrieval-baseline.js +511 -0
  301. package/scripts/check-markdown-links.js +211 -0
  302. package/scripts/check-protocol-consistency.js +979 -0
  303. package/scripts/export-marketplace-skills.js +610 -0
  304. package/scripts/export-skill.js +374 -0
  305. package/scripts/generate-manifest.js +787 -0
  306. package/scripts/lib/alias-contract.js +83 -0
  307. package/scripts/lib/audit-prompt-builder.js +771 -0
  308. package/scripts/lib/mock-grader.js +134 -0
  309. package/scripts/lib/parse-frontmatter.js +429 -0
  310. package/scripts/lib/roots.js +119 -0
  311. package/scripts/lint/check-archetype-sections.js +185 -0
  312. package/scripts/lint/check-category-enum.js +83 -0
  313. package/scripts/lint/check-routing-eval.js +146 -0
  314. package/scripts/lint/check-routing-quality.js +211 -0
  315. package/scripts/lint/check-stability-promotion.js +220 -0
  316. package/scripts/lint/format-code-frame.js +206 -0
  317. package/scripts/marketplace-install.js +125 -0
  318. package/scripts/migrate-category-to-enum.js +169 -0
  319. package/scripts/migrate-skill-v2-to-v3.js +424 -0
  320. package/scripts/migrate-skill-v3-to-v4.js +200 -0
  321. package/scripts/migrate-skill-v5-to-v6.js +304 -0
  322. package/scripts/restructure-by-category.js +85 -0
  323. package/scripts/seed-publication-classification.js +282 -0
  324. package/scripts/skill-audit.js +893 -0
  325. package/scripts/skill-graph-drift.js +483 -0
  326. package/scripts/skill-graph-route.js +766 -0
  327. package/scripts/skill-graph-routing-eval.js +393 -0
  328. package/scripts/skill-lint.js +1317 -0
  329. package/scripts/skill-overlap.js +213 -0
  330. package/scripts/verify-skill-md-export.js +201 -0
@@ -0,0 +1,112 @@
1
+ -- Canonical schema for saas-stripe-postgres example project
2
+ -- Demonstrates: multi-tenant tables, RLS policies, provider-agnostic payment columns
3
+
4
+ -- ─────────────────────────────────────────────────
5
+ -- Organizations (tenant root table)
6
+ -- ─────────────────────────────────────────────────
7
+ CREATE TABLE organizations (
8
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
9
+ name TEXT NOT NULL,
10
+ slug TEXT NOT NULL UNIQUE,
11
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
12
+ );
13
+
14
+ -- ─────────────────────────────────────────────────
15
+ -- Users
16
+ -- ─────────────────────────────────────────────────
17
+ CREATE TABLE users (
18
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
19
+ org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
20
+ email TEXT NOT NULL,
21
+ role TEXT NOT NULL DEFAULT 'member' CHECK (role IN ('owner', 'admin', 'member')),
22
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
23
+ UNIQUE (org_id, email)
24
+ );
25
+
26
+ ALTER TABLE users ENABLE ROW LEVEL SECURITY;
27
+ ALTER TABLE users FORCE ROW LEVEL SECURITY;
28
+
29
+ CREATE POLICY users_org_select ON users
30
+ FOR SELECT
31
+ USING (org_id = current_setting('app.current_org_id', true)::uuid);
32
+
33
+ CREATE POLICY users_org_insert ON users
34
+ FOR INSERT
35
+ WITH CHECK (org_id = current_setting('app.current_org_id', true)::uuid);
36
+
37
+ CREATE POLICY users_org_update ON users
38
+ FOR UPDATE
39
+ USING (org_id = current_setting('app.current_org_id', true)::uuid)
40
+ WITH CHECK (org_id = current_setting('app.current_org_id', true)::uuid);
41
+
42
+ CREATE POLICY users_org_delete ON users
43
+ FOR DELETE
44
+ USING (org_id = current_setting('app.current_org_id', true)::uuid);
45
+
46
+ -- ─────────────────────────────────────────────────
47
+ -- Subscriptions
48
+ -- ─────────────────────────────────────────────────
49
+ CREATE TABLE subscriptions (
50
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
51
+ org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
52
+ plan TEXT NOT NULL,
53
+ status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'past_due', 'canceled', 'trialing')),
54
+ current_period_end TIMESTAMPTZ,
55
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
56
+ );
57
+
58
+ ALTER TABLE subscriptions ENABLE ROW LEVEL SECURITY;
59
+ ALTER TABLE subscriptions FORCE ROW LEVEL SECURITY;
60
+
61
+ CREATE POLICY subscriptions_org_select ON subscriptions
62
+ FOR SELECT USING (org_id = current_setting('app.current_org_id', true)::uuid);
63
+ CREATE POLICY subscriptions_org_insert ON subscriptions
64
+ FOR INSERT WITH CHECK (org_id = current_setting('app.current_org_id', true)::uuid);
65
+ CREATE POLICY subscriptions_org_update ON subscriptions
66
+ FOR UPDATE
67
+ USING (org_id = current_setting('app.current_org_id', true)::uuid)
68
+ WITH CHECK (org_id = current_setting('app.current_org_id', true)::uuid);
69
+ CREATE POLICY subscriptions_org_delete ON subscriptions
70
+ FOR DELETE USING (org_id = current_setting('app.current_org_id', true)::uuid);
71
+
72
+ -- ─────────────────────────────────────────────────
73
+ -- Orders (canonical provider-agnostic schema)
74
+ -- Post-migration 0004: uses provider/provider_order_id columns,
75
+ -- not the Stripe-specific stripe_session_id/stripe_customer_id.
76
+ -- ─────────────────────────────────────────────────
77
+ CREATE TABLE orders (
78
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
79
+ org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
80
+ provider TEXT NOT NULL DEFAULT 'stripe', -- 'stripe', 'paddle', etc.
81
+ provider_order_id TEXT NOT NULL, -- was: stripe_session_id
82
+ provider_customer_id TEXT, -- was: stripe_customer_id
83
+ amount_cents INTEGER NOT NULL,
84
+ currency TEXT NOT NULL DEFAULT 'usd',
85
+ status TEXT NOT NULL DEFAULT 'pending',
86
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
87
+ UNIQUE (provider, provider_order_id)
88
+ );
89
+
90
+ ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
91
+ ALTER TABLE orders FORCE ROW LEVEL SECURITY;
92
+
93
+ CREATE POLICY orders_org_select ON orders
94
+ FOR SELECT USING (org_id = current_setting('app.current_org_id', true)::uuid);
95
+ CREATE POLICY orders_org_insert ON orders
96
+ FOR INSERT WITH CHECK (org_id = current_setting('app.current_org_id', true)::uuid);
97
+ CREATE POLICY orders_org_update ON orders
98
+ FOR UPDATE
99
+ USING (org_id = current_setting('app.current_org_id', true)::uuid)
100
+ WITH CHECK (org_id = current_setting('app.current_org_id', true)::uuid);
101
+ CREATE POLICY orders_org_delete ON orders
102
+ FOR DELETE USING (org_id = current_setting('app.current_org_id', true)::uuid);
103
+
104
+ -- ─────────────────────────────────────────────────
105
+ -- Webhook events (idempotency log)
106
+ -- Not tenant-bound — events arrive before org context is known.
107
+ -- ─────────────────────────────────────────────────
108
+ CREATE TABLE webhook_events (
109
+ event_id TEXT PRIMARY KEY, -- Stripe event.id or provider equivalent
110
+ provider TEXT NOT NULL DEFAULT 'stripe',
111
+ processed_at TIMESTAMPTZ NOT NULL DEFAULT now()
112
+ );
@@ -0,0 +1,149 @@
1
+ ---
2
+ # yaml-language-server: $schema=https://skillgraph.dev/schemas/skill.v6.schema.json
3
+ schema_version: 6
4
+ name: migrate-orders-to-canonical-schema
5
+ description: "Use when running migration 0004 that normalizes the orders table from a Stripe-specific shape (stripe_session_id, stripe_customer_id as top-level columns) to a canonical provider-agnostic shape (provider, provider_order_id, provider_customer_id). Covers the four-phase safe migration procedure — add nullable columns, backfill from existing data, validate, drop legacy columns — and the RLS policy update that must accompany the column rename. Do NOT use for unrelated schema migrations (write a fresh skill anchored to that migration's number), for designing a new canonical schema from scratch, or for the ongoing orgQuery access pattern (use postgres-rls-pattern)."
6
+ version: 0.1.0
7
+ type: workflow
8
+ category: engineering
9
+ domain: engineering/database
10
+ scope: codebase
11
+ owner: saas-stripe-postgres-example
12
+ freshness: "2026-05-18"
13
+ drift_check:
14
+ last_verified: "2026-05-18"
15
+ eval_artifacts: none
16
+ eval_state: unverified
17
+ routing_eval: absent
18
+ stability: experimental
19
+ license: MIT
20
+ compatibility:
21
+ runtimes:
22
+ - node
23
+ node: ">=20"
24
+ notes: "Postgres >=14; assumes psql or pg driver. Dry-run mode requires a non-production database."
25
+ allowed-tools: Read Grep Bash
26
+ keywords:
27
+ - orders table migration
28
+ - canonical schema migration
29
+ - stripe to provider-agnostic
30
+ - column rename migration
31
+ - four-phase migration
32
+ - add nullable backfill validate drop
33
+ - provider_order_id migration
34
+ - rls policy update on migration
35
+ - 0004 orders migration
36
+ - safe column removal
37
+ triggers:
38
+ - migrate-orders-to-canonical-schema
39
+ paths:
40
+ - "db/migrations/0004_canonicalize_orders.sql"
41
+ - "db/schema.sql"
42
+ - "scripts/migrate-orders.ts"
43
+ examples:
44
+ - "run the 0004 orders migration that renames stripe_session_id to provider_order_id"
45
+ - "safely remove the stripe_customer_id column after backfilling provider_customer_id"
46
+ - "update the RLS policy on orders after the column rename"
47
+ - "verify that every order row has a non-null provider_order_id before dropping the old column"
48
+ anti_examples:
49
+ - "design a different migration for the invoices table"
50
+ - "add row level security to a new table"
51
+ - "query the orders table in an application route"
52
+ relations:
53
+ boundary:
54
+ - skill: postgres-rls-pattern
55
+ reason: "postgres-rls-pattern defines the ongoing RLS access pattern; this skill owns the one-time migration that changes the table structure those policies reference"
56
+ - skill: payment-provider-router
57
+ reason: "payment-provider-router reads orders to check idempotency; this migration changes the column the idempotency check uses — coordinate migration timing with router deployment"
58
+ depends_on:
59
+ - skill: postgres-rls-pattern
60
+ reason: "the canonical schema migration must update RLS policies — postgres-rls-pattern defines the policy triple to follow"
61
+ verify_with:
62
+ - postgres-rls-pattern
63
+ grounding:
64
+ domain_object: "Migration 0004 — the four-phase procedure that canonicalizes the orders table from Stripe-specific column names to provider-agnostic column names, with an RLS policy update in the same migration"
65
+ grounding_mode: repo_specific
66
+ truth_sources:
67
+ - path: examples/projects/saas-stripe-postgres/db/migrations/0004_canonicalize_orders.sql
68
+ note: "The migration file — source of truth for column renames and RLS policy updates"
69
+ - path: examples/projects/saas-stripe-postgres/db/schema.sql
70
+ note: "The target canonical schema the migration produces"
71
+ failure_modes:
72
+ - dropping_column_before_backfill_verified
73
+ - rls_policy_not_updated_to_reference_new_column_name
74
+ - application_code_still_references_old_column_after_drop
75
+ - migration_run_without_dry_run_gate_first
76
+ - rollback_path_not_documented_before_irreversible_step
77
+ evidence_priority: repo_code_first
78
+ portability:
79
+ readiness: scripted
80
+ targets:
81
+ - skill-md
82
+ lifecycle:
83
+ stale_after_days: 30
84
+ review_cadence: quarterly
85
+ ---
86
+
87
+ # Migrate Orders to Canonical Schema
88
+
89
+ ## Coverage
90
+
91
+ - The four-phase safe migration procedure applied to the orders table: *add nullable columns → backfill from existing → validate → drop legacy columns*; why collapsing any two phases is unsafe under live traffic
92
+ - The canonical column mapping: `stripe_session_id` → `provider_order_id`, `stripe_customer_id` → `provider_customer_id`, with a new `provider` column set to `'stripe'` for existing rows
93
+ - The RLS policy update — the existing `orders_org_select` policy must be updated in the same migration that renames the columns if the policy references them (it does not in this case, but the checklist step prevents future drift)
94
+ - Application code audit — grepping for `stripe_session_id` and `stripe_customer_id` in the codebase to find every reference that must be updated before the old columns are dropped
95
+ - The dry-run gate — `scripts/migrate-orders.ts` runs in `--dry-run` by default, printing the diff without committing; `--apply` is the explicit opt-in
96
+
97
+ ## Philosophy
98
+
99
+ A column rename under live traffic is a non-trivial operation even on a small table. The temptation to write one migration that renames columns atomically and ships them fails because application code reads the old column names until the new application version deploys, and the deployment window is not instant. The four-phase procedure exists because the new column can be null during the window, the application can write both, and the old column can be dropped only after the new application version has been running for a full observation period with zero reads of the old column name in logs.
100
+
101
+ ## Workflow
102
+
103
+ | Phase | Precondition | Action | Success criterion |
104
+ |---|---|---|---|
105
+ | 1. Add nullable columns | Production schema has no `provider` or `provider_order_id` | Add `provider TEXT`, `provider_order_id TEXT`, `provider_customer_id TEXT` as nullable | Build passes; existing rows unaffected |
106
+ | 2. Backfill | Phase 1 deployed | `UPDATE orders SET provider = 'stripe', provider_order_id = stripe_session_id, provider_customer_id = stripe_customer_id WHERE provider IS NULL` | `SELECT COUNT(*) FROM orders WHERE provider IS NULL` returns 0 |
107
+ | 3. Validate and update application | Phase 2 complete | Search codebase for `stripe_session_id` and `stripe_customer_id` references; update to `provider_order_id` / `provider_customer_id`; deploy the updated application | Zero reads of old column names in production logs for 24 hours |
108
+ | 4. Drop legacy columns | Phase 3 observation period complete | `ALTER TABLE orders DROP COLUMN stripe_session_id, DROP COLUMN stripe_customer_id` | Schema matches `db/schema.sql`; `npm run verify` passes |
109
+
110
+ ## Migration SQL
111
+
112
+ ```sql
113
+ -- Phase 1: Add nullable canonical columns
114
+ ALTER TABLE orders
115
+ ADD COLUMN IF NOT EXISTS provider TEXT,
116
+ ADD COLUMN IF NOT EXISTS provider_order_id TEXT,
117
+ ADD COLUMN IF NOT EXISTS provider_customer_id TEXT;
118
+
119
+ -- Phase 2: Backfill from Stripe-specific columns
120
+ UPDATE orders
121
+ SET
122
+ provider = 'stripe',
123
+ provider_order_id = stripe_session_id,
124
+ provider_customer_id = stripe_customer_id
125
+ WHERE provider IS NULL;
126
+
127
+ -- Phase 4 (only after Phase 3 observation period):
128
+ -- ALTER TABLE orders
129
+ -- DROP COLUMN stripe_session_id,
130
+ -- DROP COLUMN stripe_customer_id;
131
+ ```
132
+
133
+ ## Verification
134
+
135
+ - [ ] Phase 1 was deployed to production and verified (build passed, existing rows intact) before Phase 2 ran
136
+ - [ ] Phase 2 backfill was run with `--dry-run` first; the dry-run output is committed under `db/migrations/0004-dry-run.log`
137
+ - [ ] `SELECT COUNT(*) FROM orders WHERE provider IS NULL` returns 0 before Phase 3 begins
138
+ - [ ] Phase 3 application code audit found and updated every reference to `stripe_session_id` and `stripe_customer_id`
139
+ - [ ] The observation window (minimum 24 hours) elapsed with zero old-column reads in production logs before Phase 4 was run
140
+ - [ ] Phase 4 (DROP COLUMN) is in a separate migration file from Phases 1-2, deployed only after Phase 3 sign-off
141
+ - [ ] RLS policies were reviewed after the column rename (even if not updated — the review is recorded in the migration PR)
142
+
143
+ ## Do NOT Use When
144
+
145
+ | Use instead | When |
146
+ |---|---|
147
+ | `postgres-rls-pattern` | The task is the ongoing RLS access pattern, not the one-time migration |
148
+ | (a fresh migration skill) | The task is a different migration with no relation to the 0004 orders canonicalization |
149
+ | `payment-provider-router` | The task is updating the router to use `provider_order_id` after the migration |
@@ -0,0 +1,154 @@
1
+ ---
2
+ # yaml-language-server: $schema=https://skillgraph.dev/schemas/skill.v6.schema.json
3
+ schema_version: 6
4
+ name: nextjs-server-action-validation
5
+ description: "Use when writing a Next.js Server Action that accepts user-submitted form data, mutation parameters, or any client-originated input. Every Server Action is a public HTTP endpoint regardless of how it is called — validate with Zod and check authentication as the first two operations before touching the database. Do NOT use for GET route handlers or Server Components that fetch data (those have no user-supplied input); do NOT use for Stripe webhook handlers (use stripe-webhook-signature-verification instead)."
6
+ version: 0.1.0
7
+ type: capability
8
+ category: engineering
9
+ domain: engineering/web
10
+ scope: portable
11
+ owner: saas-stripe-postgres-example
12
+ freshness: "2026-05-18"
13
+ drift_check:
14
+ last_verified: "2026-05-18"
15
+ eval_artifacts: none
16
+ eval_state: unverified
17
+ routing_eval: absent
18
+ stability: experimental
19
+ license: MIT
20
+ compatibility:
21
+ runtimes:
22
+ - node
23
+ node: ">=20"
24
+ notes: "Next.js App Router >=14 with server actions enabled; Zod >=3."
25
+ allowed-tools: Read Grep
26
+ keywords:
27
+ - next.js server action validation
28
+ - zod input validation
29
+ - server action security
30
+ - server action auth check
31
+ - server action public endpoint
32
+ - use server directive
33
+ - form action security
34
+ - server action pattern
35
+ - mutation validation
36
+ - next.js app router actions
37
+ triggers:
38
+ - nextjs-server-action-validation
39
+ paths:
40
+ - "app/actions/*.ts"
41
+ - "lib/actions/*.ts"
42
+ examples:
43
+ - "write a Server Action for the checkout form that validates the selected plan before creating a Stripe session"
44
+ - "secure a Server Action so it rejects unauthenticated requests"
45
+ - "validate user input with Zod in a Server Action before writing to Postgres"
46
+ - "my Server Action is being called directly via fetch — is that safe?"
47
+ anti_examples:
48
+ - "validate the stripe-signature header in a webhook route handler"
49
+ - "fetch data in a Server Component to display on a page"
50
+ - "write a GET route handler that returns public product data"
51
+ relations:
52
+ boundary:
53
+ - skill: stripe-webhook-signature-verification
54
+ reason: "stripe-webhook-signature-verification validates Stripe's HMAC signature on webhook route handlers; this skill validates user-submitted input on Server Actions — different trust model, different entry point"
55
+ - skill: postgres-rls-pattern
56
+ reason: "postgres-rls-pattern governs the database layer; this skill governs the action layer — both are required in a secure Server Action, but at separate tiers"
57
+ depends_on:
58
+ - skill: postgres-rls-pattern
59
+ reason: "Server Actions that write to Postgres must call orgQuery to enforce tenant isolation after input is validated"
60
+ verify_with:
61
+ - stripe-webhook-signature-verification
62
+ portability:
63
+ readiness: portable
64
+ targets:
65
+ - skill-md
66
+ lifecycle:
67
+ stale_after_days: 90
68
+ review_cadence: quarterly
69
+ ---
70
+
71
+ # Next.js Server Action Validation
72
+
73
+ ## Coverage
74
+
75
+ - The public endpoint reality — `'use server'` functions are exposed as POST endpoints that any HTTP client can call directly; the Next.js call graph is not a security boundary
76
+ - Validation order — auth check BEFORE Zod parse, Zod parse BEFORE database access; any other order produces an exploitable window
77
+ - Zod schema design for actions — using `.safeParse()` over `.parse()` to return structured errors rather than throwing; returning `{ error: ... }` shapes that the calling Client Component can render
78
+ - Org-scoping after authentication — verifying that `input.orgId` matches the session's org, not just that the user is logged in
79
+ - Error surface control — how `try/catch` around database calls prevents internal errors from propagating to the client response
80
+
81
+ ## Philosophy
82
+
83
+ Server Actions are a convenience feature that makes form submission feel like a function call. That convenience obscures a critical fact: the action is a public HTTP POST endpoint. A developer who writes `const { data } = await myAction(formData)` in a Client Component is writing what looks like a local function call, but the runtime sends an HTTP request that any script can replicate. Skipping auth or validation because "it's called from our own UI" is the same reasoning that made `getServerSideProps` data-fetching functions leaky in the Pages Router — the server boundary does not restrict callers.
84
+
85
+ ## Standard Action Pattern
86
+
87
+ ```typescript
88
+ "use server";
89
+ import { z } from "zod";
90
+ import { getServerSession } from "@/lib/auth";
91
+ import { orgQuery } from "@/lib/db";
92
+
93
+ const CreateOrderSchema = z.object({
94
+ orgId: z.string().uuid(),
95
+ planId: z.string().min(1),
96
+ quantity: z.number().int().positive(),
97
+ });
98
+
99
+ export async function createOrder(input: unknown) {
100
+ // 1. Auth check — before anything else
101
+ const session = await getServerSession();
102
+ if (!session?.user) {
103
+ return { error: "Unauthorized" };
104
+ }
105
+
106
+ // 2. Zod parse — structured errors, not thrown exceptions
107
+ const parsed = CreateOrderSchema.safeParse(input);
108
+ if (!parsed.success) {
109
+ return { error: parsed.error.flatten() };
110
+ }
111
+
112
+ // 3. Org-scope check — user must belong to the org they are acting on
113
+ if (session.user.orgId !== parsed.data.orgId) {
114
+ return { error: "Forbidden" };
115
+ }
116
+
117
+ // 4. Database write — inside orgQuery for RLS enforcement
118
+ try {
119
+ const [order] = await orgQuery(parsed.data.orgId, (tx) =>
120
+ tx`INSERT INTO orders (org_id, plan_id, quantity) VALUES (
121
+ ${parsed.data.orgId}, ${parsed.data.planId}, ${parsed.data.quantity}
122
+ ) RETURNING *`
123
+ );
124
+ return { data: order };
125
+ } catch {
126
+ return { error: "Failed to create order" };
127
+ }
128
+ }
129
+ ```
130
+
131
+ ## Validation Order Rationale
132
+
133
+ | Order | Risk |
134
+ |---|---|
135
+ | Auth → Zod → orgScope → DB | Correct — each gate eliminates the next attack surface |
136
+ | Zod → Auth → DB | Attacker can probe the schema structure without authenticating |
137
+ | Auth → DB (no Zod) | Malformed input reaches the query layer; injection risk |
138
+ | DB → Auth (anywhere) | Unauthenticated database reads before rejection |
139
+
140
+ ## Verification
141
+
142
+ - [ ] Every `'use server'` function calls `getServerSession()` as its first statement
143
+ - [ ] Every `'use server'` function runs `Schema.safeParse()` before any database call
144
+ - [ ] The session org ID is compared to the input org ID before the database call
145
+ - [ ] Database calls are wrapped in `orgQuery`, not bare SQL
146
+ - [ ] Errors returned to the client do not include stack traces or query text
147
+
148
+ ## Do NOT Use When
149
+
150
+ | Use instead | When |
151
+ |---|---|
152
+ | `stripe-webhook-signature-verification` | The entry point is a webhook route handler, not a Server Action |
153
+ | (a data-fetching skill) | The function is a Server Component that fetches data, with no user-submitted input |
154
+ | `postgres-rls-pattern` | The task is defining the database-layer policy, not the action-layer validation |
@@ -0,0 +1,153 @@
1
+ ---
2
+ # yaml-language-server: $schema=https://skillgraph.dev/schemas/skill.v6.schema.json
3
+ schema_version: 6
4
+ name: payment-provider-router
5
+ description: "Use when dispatching a verified payment event (Stripe webhook or future provider) to the correct downstream handler based on event type. Routes `checkout.session.completed` to subscription provisioning, `invoice.payment_failed` to dunning logic, and `customer.subscription.deleted` to cancellation. Do NOT use for signature verification of the incoming event (use stripe-webhook-signature-verification first) or for the actual subscription database writes (use the per-handler skill or postgres-rls-pattern)."
6
+ version: 0.1.0
7
+ type: router
8
+ category: engineering
9
+ domain: engineering/payments
10
+ scope: portable
11
+ owner: saas-stripe-postgres-example
12
+ freshness: "2026-05-18"
13
+ drift_check:
14
+ last_verified: "2026-05-18"
15
+ eval_artifacts: none
16
+ eval_state: unverified
17
+ routing_eval: absent
18
+ stability: experimental
19
+ license: MIT
20
+ compatibility:
21
+ runtimes:
22
+ - node
23
+ node: ">=20"
24
+ notes: "Stripe SDK >=14; event types sourced from Stripe's published event catalog."
25
+ allowed-tools: Read Grep
26
+ keywords:
27
+ - payment event routing
28
+ - stripe event type dispatch
29
+ - webhook event router
30
+ - checkout.session.completed
31
+ - invoice.payment_failed
32
+ - customer.subscription.deleted
33
+ - payment provider dispatch
34
+ - event type switch
35
+ - payment event handler
36
+ - multi-provider payment routing
37
+ triggers:
38
+ - payment-provider-router
39
+ paths:
40
+ - "lib/payments/router.ts"
41
+ - "app/api/webhooks/stripe/route.ts"
42
+ examples:
43
+ - "route checkout.session.completed to subscription provisioning"
44
+ - "which handler should process invoice.payment_failed for dunning?"
45
+ - "add a new event type handler for customer.subscription.updated"
46
+ - "design the event router so it is extensible to a second payment provider"
47
+ anti_examples:
48
+ - "verify that the webhook request is genuinely from Stripe"
49
+ - "write the database insert that creates the subscription record"
50
+ - "handle a failed Stripe API call when creating a payment intent"
51
+ relations:
52
+ boundary:
53
+ - skill: stripe-webhook-signature-verification
54
+ reason: "stripe-webhook-signature-verification verifies the event is authentic before it reaches this router; routing an unverified event is a security failure"
55
+ - skill: postgres-rls-pattern
56
+ reason: "postgres-rls-pattern governs the database writes that each handler performs; this router decides which handler runs, not how it writes"
57
+ depends_on:
58
+ - skill: stripe-webhook-signature-verification
59
+ reason: "this router must only receive events that have already passed signature verification — call stripe-webhook-signature-verification first"
60
+ verify_with:
61
+ - stripe-webhook-signature-verification
62
+ portability:
63
+ readiness: portable
64
+ targets:
65
+ - skill-md
66
+ lifecycle:
67
+ stale_after_days: 90
68
+ review_cadence: quarterly
69
+ ---
70
+
71
+ # Payment Provider Router
72
+
73
+ ## Coverage
74
+
75
+ - The routing table — a typed dispatch map from `Stripe.Event["type"]` to handler functions, with a structured "unknown event" fallback that returns 200 (to prevent Stripe retry storms) and logs the unhandled type
76
+ - Handler isolation — each handler receives only the specific event subtype it needs (e.g. `Stripe.CheckoutSessionCompletedEvent`), not the generic `Stripe.Event`, to avoid casts inside handlers
77
+ - Provider abstraction — how to wrap the Stripe-specific router behind a `PaymentEvent` canonical type so a future provider can be added without changing handler code
78
+ - Error surface — handlers must catch their own errors and return a structured result; an uncaught exception must not produce a 500 that triggers Stripe's retry mechanism with an exponential backoff cascade
79
+ - Event type coverage audit — which event types are handled, which are known-ignored (acknowledged with a comment), and which are genuinely unknown
80
+
81
+ ## Philosophy
82
+
83
+ A payment event router has the same discipline requirement as a content source router: prefer an explicit handler over an implicit fallback, surface unhandled events loudly (in logs, not in HTTP status codes — a 400 triggers a retry, a 200 with a log entry does not), and never let one handler own two semantically distinct events. The event type is the authoritative signal for which business operation to perform; ambiguity at this layer produces double-charges, missed provisioning, and unfired dunning emails.
84
+
85
+ ## Routing Rules
86
+
87
+ ```typescript
88
+ // lib/payments/router.ts
89
+ import Stripe from "stripe";
90
+ import { handleCheckoutComplete } from "./handlers/checkout-complete";
91
+ import { handlePaymentFailed } from "./handlers/payment-failed";
92
+ import { handleSubscriptionDeleted } from "./handlers/subscription-deleted";
93
+
94
+ type HandlerResult = { ok: boolean; message?: string };
95
+
96
+ const EVENT_HANDLERS: Partial<
97
+ Record<Stripe.Event["type"], (event: Stripe.Event) => Promise<HandlerResult>>
98
+ > = {
99
+ "checkout.session.completed": (e) =>
100
+ handleCheckoutComplete(e as Stripe.CheckoutSessionCompletedEvent),
101
+ "invoice.payment_failed": (e) =>
102
+ handlePaymentFailed(e as Stripe.InvoicePaymentFailedEvent),
103
+ "customer.subscription.deleted": (e) =>
104
+ handleSubscriptionDeleted(e as Stripe.CustomerSubscriptionDeletedEvent),
105
+ // Acknowledged non-actionable events — log and return OK
106
+ "invoice.paid": async () => ({ ok: true, message: "acknowledged" }),
107
+ };
108
+
109
+ export async function routePaymentEvent(event: Stripe.Event): Promise<HandlerResult> {
110
+ const handler = EVENT_HANDLERS[event.type];
111
+
112
+ if (!handler) {
113
+ console.warn("[payment-router] unhandled event type", { type: event.type, id: event.id });
114
+ // Return 200 — a 4xx or 5xx would trigger Stripe retry with backoff
115
+ return { ok: true, message: "unhandled_event_type" };
116
+ }
117
+
118
+ return handler(event);
119
+ }
120
+ ```
121
+
122
+ ## Routing Decision Rules
123
+
124
+ | Event type | Handler | Rationale |
125
+ |---|---|---|
126
+ | `checkout.session.completed` | `handleCheckoutComplete` | Provision subscription, create org record |
127
+ | `invoice.payment_failed` | `handlePaymentFailed` | Trigger dunning, update subscription status |
128
+ | `customer.subscription.deleted` | `handleSubscriptionDeleted` | Revoke access, archive subscription |
129
+ | `invoice.paid` | acknowledged | No action — success is implicit from `checkout.session.completed` |
130
+ | anything else | log + 200 | Unknown event — log for triage, do not retry |
131
+
132
+ ## Adding a New Event Type
133
+
134
+ 1. Add the Stripe event type string to `EVENT_HANDLERS` with a typed cast.
135
+ 2. Write the handler in `lib/payments/handlers/<name>.ts` — it receives the specific subtype.
136
+ 3. Add a row to the routing table above documenting what the handler does.
137
+ 4. If the event should be intentionally ignored, add it to the "acknowledged" row rather than leaving it in the unknown bucket.
138
+
139
+ ## Verification
140
+
141
+ - [ ] Every routable event type is in `EVENT_HANDLERS` with an explicit handler or acknowledgement
142
+ - [ ] Unknown events return 200 (not 400 or 500) to prevent Stripe retry cascades
143
+ - [ ] Each handler receives a typed subtype, not the generic `Stripe.Event`
144
+ - [ ] Handler errors are caught inside the handler and returned as `{ ok: false }` — they do not propagate to the router
145
+ - [ ] `routePaymentEvent` is only called after signature verification (grep for `routePaymentEvent` — every call site should be downstream of `constructEvent`)
146
+
147
+ ## Do NOT Use When
148
+
149
+ | Use instead | When |
150
+ |---|---|
151
+ | `stripe-webhook-signature-verification` | The task is verifying the event's authenticity before routing |
152
+ | `postgres-rls-pattern` | The task is writing the database statements inside a specific handler |
153
+ | (a generic event bus skill) | The application uses an event bus that is not payment-provider-specific |