@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.
- package/CHANGELOG.md +247 -0
- package/LICENSE +200 -0
- package/NOTICE +62 -0
- package/README.md +398 -0
- package/SKILL_GRAPH.md +443 -0
- package/bin/skill-graph.js +374 -0
- package/docs/ADOPTION.md +117 -0
- package/docs/CONFORMANCE.md +66 -0
- package/docs/PRIMER.md +384 -0
- package/docs/QUICKSTART-30MIN.md +333 -0
- package/docs/ROUTING-METRICS.md +120 -0
- package/docs/SKILL-MD-FORMAT-COMPATIBILITY.md +127 -0
- package/docs/SKILL_AUDIT_CHECKLIST.md +199 -0
- package/docs/SKILL_AUDIT_LOOP.md +195 -0
- package/docs/SKILL_METADATA_PROTOCOL.md +609 -0
- package/docs/_archived/marketplace-publication-priority-2026-05-18.md +239 -0
- package/docs/adr/0001-predicate-set.md +69 -0
- package/docs/adr/0002-json-ld-context.md +82 -0
- package/docs/adr/0003-ontoclean-rigidity-tags.md +65 -0
- package/docs/adr/0004-persistent-identifiers.md +74 -0
- package/docs/adr/0005-freshness-consolidation.md +70 -0
- package/docs/adr/0006-revise-predicate-rename.md +105 -0
- package/docs/adr/0007-audit-loop-cadence.md +99 -0
- package/docs/adr/0008-skill-surface-split-and-curation-policy.md +93 -0
- package/docs/category-consumers.md +168 -0
- package/docs/concept-map.md +194 -0
- package/docs/diagrams/drift-states.mmd +21 -0
- package/docs/diagrams/manifest-pipeline.mmd +25 -0
- package/docs/diagrams/routing-harness.mmd +41 -0
- package/docs/diagrams/starter-graph.mmd +53 -0
- package/docs/field-decision-guide.md +315 -0
- package/docs/field-rationale.md +211 -0
- package/docs/field-reference.generated.md +624 -0
- package/docs/field-reference.md +1426 -0
- package/docs/glossary.md +190 -0
- package/docs/head-noun-glossary.md +63 -0
- package/docs/images/audit-phases.png +0 -0
- package/docs/images/drift-states.png +0 -0
- package/docs/images/graded-mode.png +0 -0
- package/docs/images/manifest-pipeline.png +0 -0
- package/docs/images/routing-harness.png +0 -0
- package/docs/images/skill-anatomy.png +0 -0
- package/docs/images/starter-graph.png +0 -0
- package/docs/images/system-model.png +0 -0
- package/docs/integrations/github-actions.md +155 -0
- package/docs/manifest-field-mapping.md +443 -0
- package/docs/marketplace-publication-queue.generated.md +240 -0
- package/docs/marketplace-release-agent-prompt.md +82 -0
- package/docs/marketplace-skill-candidate-list.md +272 -0
- package/docs/marketplace-syndication.md +222 -0
- package/docs/migration-sample-review.md +155 -0
- package/docs/migrations/v4-to-v5.md +168 -0
- package/docs/migrations/v5-to-v6.md +221 -0
- package/docs/name-exceptions.yaml +37 -0
- package/docs/plans/marketplace-p1-public-migration-plan.md +41 -0
- package/docs/plans/multi-root-workspace.md +148 -0
- package/docs/plans/scripts-roadmap.md +107 -0
- package/docs/plans/v4-schema-bump.md +160 -0
- package/docs/plans/wave-2-extraction.md +122 -0
- package/docs/positioning-vs-marketplaces.md +175 -0
- package/docs/proposals/skill-audit-loop-positioning.md +160 -0
- package/docs/quality-doctrine.md +138 -0
- package/docs/recommended-skills.md +150 -0
- package/docs/research/skill-comprehension-eval-research.md +1830 -0
- package/docs/research/skill-retrieval-evidence.md +66 -0
- package/docs/skill-metadata-protocol.md +471 -0
- package/docs/skills-sh-maintainer-cleanup-request.md +80 -0
- package/examples/audits/a11y/findings.md +52 -0
- package/examples/audits/a11y/scorecard.md +21 -0
- package/examples/audits/a11y/verdict.md +44 -0
- package/examples/audits/debugging/findings.md +59 -0
- package/examples/audits/debugging/scorecard.md +22 -0
- package/examples/audits/debugging/verdict.md +33 -0
- package/examples/audits/documentation/findings.md +59 -0
- package/examples/audits/documentation/scorecard.md +22 -0
- package/examples/audits/documentation/verdict.md +33 -0
- package/examples/evals/a11y.json +140 -0
- package/examples/evals/api-design.json +52 -0
- package/examples/evals/code-review.json +52 -0
- package/examples/evals/data-modeling.json +52 -0
- package/examples/evals/database-migration.json +52 -0
- package/examples/evals/debugging.json +118 -0
- package/examples/evals/dependency-architecture.json +52 -0
- package/examples/evals/design-system-architecture.json +52 -0
- package/examples/evals/error-tracking.json +52 -0
- package/examples/evals/event-contract-design.json +52 -0
- package/examples/evals/form-ux-architecture.json +52 -0
- package/examples/evals/framework-fit-analysis.json +52 -0
- package/examples/evals/graph-audit.json +139 -0
- package/examples/evals/information-architecture.json +52 -0
- package/examples/evals/interaction-feedback.json +52 -0
- package/examples/evals/interaction-patterns.json +52 -0
- package/examples/evals/layout-composition.json +52 -0
- package/examples/evals/lint-overlay.json +117 -0
- package/examples/evals/microcopy.json +52 -0
- package/examples/evals/observability-modeling.json +52 -0
- package/examples/evals/pattern-recognition.json +96 -0
- package/examples/evals/performance-engineering.json +52 -0
- package/examples/evals/refactor.json +128 -0
- package/examples/evals/semiotics.json +52 -0
- package/examples/evals/skill-infrastructure.json +96 -0
- package/examples/evals/skill-router.json +140 -0
- package/examples/evals/skill-router.routing.json +113 -0
- package/examples/evals/system-interface-contracts.json +52 -0
- package/examples/evals/task-analysis.json +52 -0
- package/examples/evals/testing-strategy.json +118 -0
- package/examples/evals/type-safety.json +249 -0
- package/examples/evals/visual-design-foundations.json +52 -0
- package/examples/evals/webhook-integration.json +52 -0
- package/examples/exports/a11y.skill-md.md +80 -0
- package/examples/exports/debugging.skill-md.md +80 -0
- package/examples/exports/refactor.skill-md.md +78 -0
- package/examples/exports/testing-strategy.skill-md.md +81 -0
- package/examples/projects/markdown-static-site/README.md +115 -0
- package/examples/projects/markdown-static-site/skills/content-source-router/SKILL.md +131 -0
- package/examples/projects/markdown-static-site/skills/image-optimization-pipeline-config/SKILL.md +132 -0
- package/examples/projects/markdown-static-site/skills/link-rot-detection/SKILL.md +103 -0
- package/examples/projects/markdown-static-site/skills/markdown-post-frontmatter-validation/SKILL.md +133 -0
- package/examples/projects/markdown-static-site/skills/migrate-posts-to-v2-frontmatter/SKILL.md +140 -0
- package/examples/projects/saas-stripe-postgres/README.md +208 -0
- package/examples/projects/saas-stripe-postgres/db/migrations/0004_canonicalize_orders.sql +37 -0
- package/examples/projects/saas-stripe-postgres/db/schema.sql +112 -0
- package/examples/projects/saas-stripe-postgres/skills/migrate-orders-to-canonical-schema/SKILL.md +149 -0
- package/examples/projects/saas-stripe-postgres/skills/nextjs-server-action-validation/SKILL.md +154 -0
- package/examples/projects/saas-stripe-postgres/skills/payment-provider-router/SKILL.md +153 -0
- package/examples/projects/saas-stripe-postgres/skills/postgres-rls-pattern/SKILL.md +163 -0
- package/examples/projects/saas-stripe-postgres/skills/stripe-webhook-signature-verification/SKILL.md +137 -0
- package/examples/protocol/skill-metadata-template.md +301 -0
- package/examples/protocol/skills.manifest.sample.json +13245 -0
- package/examples/skill-metadata-template.md +317 -0
- package/examples/skills.manifest.sample.json +13519 -0
- package/examples/tests/v3-1-skos-fixture/SKILL.md +93 -0
- package/marketplace/README.md +17 -0
- package/marketplace/skills/a11y/SKILL.md +66 -0
- package/marketplace/skills/acid-fundamentals/SKILL.md +106 -0
- package/marketplace/skills/agent-engineering/SKILL.md +386 -0
- package/marketplace/skills/agent-eval-design/SKILL.md +55 -0
- package/marketplace/skills/ai-native-development/SKILL.md +294 -0
- package/marketplace/skills/api-design/SKILL.md +60 -0
- package/marketplace/skills/architecture-decision-records/SKILL.md +55 -0
- package/marketplace/skills/background-jobs/SKILL.md +265 -0
- package/marketplace/skills/bounded-context-mapping/SKILL.md +55 -0
- package/marketplace/skills/cap-theorem-tradeoffs/SKILL.md +127 -0
- package/marketplace/skills/client-server-boundary/SKILL.md +187 -0
- package/marketplace/skills/code-review/SKILL.md +120 -0
- package/marketplace/skills/color-system-design/SKILL.md +43 -0
- package/marketplace/skills/component-architecture/SKILL.md +126 -0
- package/marketplace/skills/compression/SKILL.md +112 -0
- package/marketplace/skills/conceptual-modeling/SKILL.md +181 -0
- package/marketplace/skills/connection-pooling/SKILL.md +105 -0
- package/marketplace/skills/constraint-awareness/SKILL.md +287 -0
- package/marketplace/skills/content-monitor/SKILL.md +209 -0
- package/marketplace/skills/context-engineering/SKILL.md +320 -0
- package/marketplace/skills/context-graph/SKILL.md +174 -0
- package/marketplace/skills/context-management/SKILL.md +174 -0
- package/marketplace/skills/context-window/SKILL.md +239 -0
- package/marketplace/skills/contract-testing/SKILL.md +120 -0
- package/marketplace/skills/cron-scheduling/SKILL.md +223 -0
- package/marketplace/skills/dark-mode-implementation/SKILL.md +47 -0
- package/marketplace/skills/data-modeling/SKILL.md +59 -0
- package/marketplace/skills/data-modeling-fundamentals/SKILL.md +117 -0
- package/marketplace/skills/database-migration/SKILL.md +429 -0
- package/marketplace/skills/debugging/SKILL.md +67 -0
- package/marketplace/skills/dependency-architecture/SKILL.md +58 -0
- package/marketplace/skills/design-module-composition/SKILL.md +43 -0
- package/marketplace/skills/design-system-architecture/SKILL.md +61 -0
- package/marketplace/skills/design-thinking/SKILL.md +44 -0
- package/marketplace/skills/diagnosis/SKILL.md +296 -0
- package/marketplace/skills/diff-analysis/SKILL.md +188 -0
- package/marketplace/skills/e2e-test-design/SKILL.md +113 -0
- package/marketplace/skills/entity-relationship-modeling/SKILL.md +218 -0
- package/marketplace/skills/epistemic-grounding/SKILL.md +112 -0
- package/marketplace/skills/error-boundary/SKILL.md +235 -0
- package/marketplace/skills/error-tracking/SKILL.md +261 -0
- package/marketplace/skills/eval-driven-development/SKILL.md +147 -0
- package/marketplace/skills/evaluation/SKILL.md +113 -0
- package/marketplace/skills/event-contract-design/SKILL.md +60 -0
- package/marketplace/skills/event-storming/SKILL.md +56 -0
- package/marketplace/skills/form-ux-architecture/SKILL.md +60 -0
- package/marketplace/skills/framework-fit-analysis/SKILL.md +59 -0
- package/marketplace/skills/frontend-architecture/SKILL.md +43 -0
- package/marketplace/skills/generative-ui/SKILL.md +118 -0
- package/marketplace/skills/graph-audit/SKILL.md +81 -0
- package/marketplace/skills/guardrails/SKILL.md +118 -0
- package/marketplace/skills/hooks-patterns/SKILL.md +185 -0
- package/marketplace/skills/http-semantics/SKILL.md +136 -0
- package/marketplace/skills/ideation/SKILL.md +41 -0
- package/marketplace/skills/indexing-strategy/SKILL.md +108 -0
- package/marketplace/skills/information-architecture/SKILL.md +59 -0
- package/marketplace/skills/integration-test-design/SKILL.md +111 -0
- package/marketplace/skills/intent-recognition/SKILL.md +136 -0
- package/marketplace/skills/interaction-feedback/SKILL.md +59 -0
- package/marketplace/skills/interaction-patterns/SKILL.md +59 -0
- package/marketplace/skills/journey-mapping/SKILL.md +41 -0
- package/marketplace/skills/keywords/SKILL.md +213 -0
- package/marketplace/skills/knowledge-modeling/SKILL.md +232 -0
- package/marketplace/skills/layout-composition/SKILL.md +59 -0
- package/marketplace/skills/linguistics/SKILL.md +429 -0
- package/marketplace/skills/lint-overlay/SKILL.md +76 -0
- package/marketplace/skills/mental-models/SKILL.md +126 -0
- package/marketplace/skills/merge-queue/SKILL.md +94 -0
- package/marketplace/skills/methodology/SKILL.md +317 -0
- package/marketplace/skills/microcopy/SKILL.md +232 -0
- package/marketplace/skills/middleware-patterns/SKILL.md +363 -0
- package/marketplace/skills/mobile-responsive-ux/SKILL.md +287 -0
- package/marketplace/skills/mutation-testing/SKILL.md +112 -0
- package/marketplace/skills/naming-conventions/SKILL.md +112 -0
- package/marketplace/skills/observability-modeling/SKILL.md +59 -0
- package/marketplace/skills/ontology-modeling/SKILL.md +67 -0
- package/marketplace/skills/owasp-security/SKILL.md +153 -0
- package/marketplace/skills/pattern-recognition/SKILL.md +472 -0
- package/marketplace/skills/performance-budgets/SKILL.md +185 -0
- package/marketplace/skills/performance-engineering/SKILL.md +58 -0
- package/marketplace/skills/performance-testing/SKILL.md +125 -0
- package/marketplace/skills/printify/SKILL.md +42 -0
- package/marketplace/skills/prioritization/SKILL.md +118 -0
- package/marketplace/skills/problem-framing/SKILL.md +41 -0
- package/marketplace/skills/problem-locating-solving/SKILL.md +203 -0
- package/marketplace/skills/project-knowledge-extraction/SKILL.md +54 -0
- package/marketplace/skills/prompt-craft/SKILL.md +134 -0
- package/marketplace/skills/prompt-injection-defense/SKILL.md +132 -0
- package/marketplace/skills/property-based-testing/SKILL.md +100 -0
- package/marketplace/skills/prototyping/SKILL.md +43 -0
- package/marketplace/skills/query-optimization/SKILL.md +144 -0
- package/marketplace/skills/real-time-updates/SKILL.md +324 -0
- package/marketplace/skills/ref-patterns/SKILL.md +284 -0
- package/marketplace/skills/refactor/SKILL.md +65 -0
- package/marketplace/skills/rendering-models/SKILL.md +142 -0
- package/marketplace/skills/replication-patterns/SKILL.md +110 -0
- package/marketplace/skills/research-synthesis/SKILL.md +41 -0
- package/marketplace/skills/route-handler-design/SKILL.md +347 -0
- package/marketplace/skills/schema-evolution/SKILL.md +140 -0
- package/marketplace/skills/security-fundamentals/SKILL.md +139 -0
- package/marketplace/skills/semantic-center/SKILL.md +194 -0
- package/marketplace/skills/semantic-relations/SKILL.md +250 -0
- package/marketplace/skills/semantics/SKILL.md +366 -0
- package/marketplace/skills/semiotics/SKILL.md +230 -0
- package/marketplace/skills/seo-strategy/SKILL.md +260 -0
- package/marketplace/skills/server-actions-design/SKILL.md +243 -0
- package/marketplace/skills/server-components-design/SKILL.md +190 -0
- package/marketplace/skills/sharding-strategy/SKILL.md +123 -0
- package/marketplace/skills/shopify/SKILL.md +42 -0
- package/marketplace/skills/skill-infrastructure/SKILL.md +320 -0
- package/marketplace/skills/skill-router/SKILL.md +71 -0
- package/marketplace/skills/skill-scaffold/SKILL.md +105 -0
- package/marketplace/skills/snapshot-testing/SKILL.md +120 -0
- package/marketplace/skills/spec-driven-development/SKILL.md +148 -0
- package/marketplace/skills/state-machine-modeling/SKILL.md +56 -0
- package/marketplace/skills/state-management/SKILL.md +134 -0
- package/marketplace/skills/streaming-architecture/SKILL.md +194 -0
- package/marketplace/skills/summarization/SKILL.md +156 -0
- package/marketplace/skills/suspense-patterns/SKILL.md +265 -0
- package/marketplace/skills/system-interface-contracts/SKILL.md +59 -0
- package/marketplace/skills/task-analysis/SKILL.md +201 -0
- package/marketplace/skills/taxonomy-design/SKILL.md +66 -0
- package/marketplace/skills/test-coverage-strategy/SKILL.md +108 -0
- package/marketplace/skills/test-doubles-design/SKILL.md +98 -0
- package/marketplace/skills/test-driven-development/SKILL.md +96 -0
- package/marketplace/skills/testing-strategy/SKILL.md +67 -0
- package/marketplace/skills/theme-system-design/SKILL.md +43 -0
- package/marketplace/skills/tool-call-flow/SKILL.md +229 -0
- package/marketplace/skills/tool-call-strategy/SKILL.md +292 -0
- package/marketplace/skills/transaction-isolation/SKILL.md +98 -0
- package/marketplace/skills/type-safety/SKILL.md +177 -0
- package/marketplace/skills/typography-system/SKILL.md +43 -0
- package/marketplace/skills/usability-testing/SKILL.md +43 -0
- package/marketplace/skills/user-research/SKILL.md +43 -0
- package/marketplace/skills/vercel-composition-patterns/SKILL.md +157 -0
- package/marketplace/skills/version-control/SKILL.md +233 -0
- package/marketplace/skills/visual-design-foundations/SKILL.md +59 -0
- package/marketplace/skills/visual-hierarchy/SKILL.md +43 -0
- package/marketplace/skills/webhook-integration/SKILL.md +331 -0
- package/marketplace/skills/writing-humanizer/SKILL.md +380 -0
- package/package.json +67 -0
- package/schemas/manifest.schema.json +811 -0
- package/schemas/manifest.v2.schema.json +164 -0
- package/schemas/manifest.v3.schema.json +758 -0
- package/schemas/manifest.v4.schema.json +755 -0
- package/schemas/manifest.v5.schema.json +755 -0
- package/schemas/manifest.v6.schema.json +811 -0
- package/schemas/skill.context.jsonld +279 -0
- package/schemas/skill.schema.json +919 -0
- package/schemas/skill.v2.schema.json +201 -0
- package/schemas/skill.v3.schema.json +827 -0
- package/schemas/skill.v4.schema.json +822 -0
- package/schemas/skill.v5.schema.json +830 -0
- package/schemas/skill.v6.schema.json +946 -0
- package/schemas/vocabulary/keywords.json +180 -0
- package/schemas/vocabulary/workspace_tags.json +23 -0
- package/scripts/__tests__/migrate-skill-v2-to-v3.test.js +161 -0
- package/scripts/__tests__/migrate-skill-v3-to-v4.test.js +158 -0
- package/scripts/__tests__/test-export-parser-drift.js +149 -0
- package/scripts/__tests__/test-marketplace-export.js +114 -0
- package/scripts/__tests__/test-router-paths.js +82 -0
- package/scripts/__tests__/test-stability-promotion.js +244 -0
- package/scripts/__tests__/test-v3-1-alias-contract.js +109 -0
- package/scripts/__tests__/test-v3-1-skos-runtime.js +116 -0
- package/scripts/backfill-schema-version.js +198 -0
- package/scripts/build-field-reference.js +160 -0
- package/scripts/build-retrieval-baseline.js +511 -0
- package/scripts/check-markdown-links.js +211 -0
- package/scripts/check-protocol-consistency.js +979 -0
- package/scripts/export-marketplace-skills.js +610 -0
- package/scripts/export-skill.js +374 -0
- package/scripts/generate-manifest.js +787 -0
- package/scripts/lib/alias-contract.js +83 -0
- package/scripts/lib/audit-prompt-builder.js +771 -0
- package/scripts/lib/mock-grader.js +134 -0
- package/scripts/lib/parse-frontmatter.js +429 -0
- package/scripts/lib/roots.js +119 -0
- package/scripts/lint/check-archetype-sections.js +185 -0
- package/scripts/lint/check-category-enum.js +83 -0
- package/scripts/lint/check-routing-eval.js +146 -0
- package/scripts/lint/check-routing-quality.js +211 -0
- package/scripts/lint/check-stability-promotion.js +220 -0
- package/scripts/lint/format-code-frame.js +206 -0
- package/scripts/marketplace-install.js +125 -0
- package/scripts/migrate-category-to-enum.js +169 -0
- package/scripts/migrate-skill-v2-to-v3.js +424 -0
- package/scripts/migrate-skill-v3-to-v4.js +200 -0
- package/scripts/migrate-skill-v5-to-v6.js +304 -0
- package/scripts/restructure-by-category.js +85 -0
- package/scripts/seed-publication-classification.js +282 -0
- package/scripts/skill-audit.js +893 -0
- package/scripts/skill-graph-drift.js +483 -0
- package/scripts/skill-graph-route.js +766 -0
- package/scripts/skill-graph-routing-eval.js +393 -0
- package/scripts/skill-lint.js +1317 -0
- package/scripts/skill-overlap.js +213 -0
- 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
|
+
);
|
package/examples/projects/saas-stripe-postgres/skills/migrate-orders-to-canonical-schema/SKILL.md
ADDED
|
@@ -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 |
|
package/examples/projects/saas-stripe-postgres/skills/nextjs-server-action-validation/SKILL.md
ADDED
|
@@ -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 |
|