@polymorphism-tech/morph-spec 8.46.0 → 9.0.0
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/CLAUDE.md +53 -44
- package/README.md +3 -4
- package/bin/morph-spec.js +38 -55
- package/bin/validate.js +5 -134
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +2 -2
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +41 -101
- package/framework/MORPH.md +95 -252
- package/framework/agents.json +75 -18
- package/framework/commands/create-umbraco-lp.md +2 -0
- package/framework/commands/migrate-lp-to-umbraco.md +2 -0
- package/framework/commands/morph-apply.md +5 -28
- package/framework/commands/morph-archive.md +2 -2
- package/framework/commands/morph-hotfix.md +8 -8
- package/framework/commands/morph-infra.md +17 -203
- package/framework/commands/morph-media.md +3 -0
- package/framework/commands/morph-preflight.md +7 -31
- package/framework/commands/morph-proposal.md +2 -50
- package/framework/commands/morph-status.md +3 -3
- package/framework/commands/morph-troubleshoot.md +2 -14
- package/framework/docs/CLI.md +585 -286
- package/framework/evals/review-checklist.md +82 -41
- package/framework/hooks/README.md +10 -11
- package/framework/hooks/claude-code/core/comment-guard.js +1 -1
- package/framework/hooks/claude-code/core/denylist.json +33 -3
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
- package/framework/hooks/claude-code/core/gate-guard.js +2 -11
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
- package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
- package/framework/hooks/claude-code/core/state-sync.js +17 -19
- package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
- package/framework/hooks/claude-code/core/validate-completion.js +33 -97
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
- package/framework/hooks/claude-code/core/worktree-context.js +5 -4
- package/framework/hooks/claude-code/statusline.py +55 -41
- package/framework/hooks/package.json +3 -0
- package/framework/hooks/shared/activity-logger.js +1 -1
- package/framework/hooks/shared/build-check.js +10 -6
- package/framework/hooks/shared/command-tokens.js +169 -0
- package/framework/hooks/shared/comment-rules.js +1 -1
- package/framework/hooks/shared/comment-scanner.js +10 -23
- package/framework/hooks/shared/comment-stripper.js +1 -1
- package/framework/hooks/shared/e2e-check.js +67 -31
- package/framework/hooks/shared/errata.js +72 -0
- package/framework/hooks/shared/feature-locator.js +1 -1
- package/framework/hooks/shared/frontend-locations.js +6 -0
- package/framework/hooks/shared/gate-question.js +18 -0
- package/framework/hooks/shared/phase-utils.js +15 -32
- package/framework/hooks/shared/slice-progress.js +118 -0
- package/framework/hooks/shared/spawn-command.js +23 -0
- package/framework/hooks/shared/state-reader.js +61 -87
- package/framework/hooks/shared/state-writer.js +3 -3
- package/framework/mcp/registry.json +3 -4
- package/framework/phases.json +23 -39
- package/framework/rules/ai-agents-standards.md +18 -16
- package/framework/rules/asaas-standards.md +3 -1
- package/framework/rules/csharp-standards.md +10 -8
- package/framework/rules/data-standards.md +5 -5
- package/framework/rules/dotnet-templates.md +1 -1
- package/framework/rules/frontend-standards.md +4 -2
- package/framework/rules/gsap-standards.md +10 -5
- package/framework/rules/hangfire-standards.md +3 -1
- package/framework/rules/infrastructure-standards.md +4 -2
- package/framework/rules/neon-auth-standards.md +3 -1
- package/framework/rules/neon-standards.md +6 -4
- package/framework/rules/nextjs-standards.md +13 -11
- package/framework/rules/nextjs-templates.md +1 -1
- package/framework/rules/resend-standards.md +3 -1
- package/framework/schemas/config.schema.json +14 -9
- package/framework/schemas/feature.schema.json +15 -52
- package/framework/skills/README.md +4 -5
- package/framework/skills/core/create-umbraco-lp/SKILL.md +3 -3
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +3 -3
- package/framework/skills/core/morph-apply/SKILL.md +150 -223
- package/framework/skills/core/morph-hotfix/SKILL.md +34 -43
- package/framework/skills/core/morph-machine/SKILL.md +4 -4
- package/framework/skills/core/morph-media/SKILL.md +3 -3
- package/framework/skills/core/morph-plan/SKILL.md +138 -300
- package/framework/skills/core/morph-review/SKILL.md +112 -137
- package/framework/skills/core/morph-standards/SKILL.md +29 -41
- package/framework/skills/core/morph-uiux/SKILL.md +78 -153
- package/framework/standards/STANDARDS.json +90 -1219
- package/framework/standards/STANDARDS.meta.json +1305 -0
- package/framework/standards/ai-agents/evals-with-cache.md +2 -3
- package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
- package/framework/standards/ai-agents/modalities-speech-to-text.md +36 -26
- package/framework/standards/ai-agents/prompt-sources.md +6 -4
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +67 -39
- package/framework/standards/ai-agents/setup.md +2 -2
- package/framework/standards/ai-agents/structured-output.md +1 -1
- package/framework/standards/ai-agents/sweet-spot.md +1 -1
- package/framework/standards/architecture/code-comments.md +8 -8
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +18 -9
- package/framework/standards/backend/api/validation.md +135 -47
- package/framework/standards/backend/database/postgresql/database.md +11 -7
- package/framework/standards/backend/database/vector-search-rag.md +207 -324
- package/framework/standards/backend/dotnet/fluent-validation-vsa.md +14 -8
- package/framework/standards/backend/dotnet/result-pattern.md +40 -6
- package/framework/standards/backend/dotnet/scalar-ui.md +13 -7
- package/framework/standards/backend/dotnet/scrutor.md +1 -1
- package/framework/standards/backend/dotnet/testing.md +1 -1
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +7 -7
- package/framework/standards/backend/integrations/asaas/asaas-api.md +309 -165
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +134 -154
- package/framework/standards/data/nosql/cache/redis.md +120 -44
- package/framework/standards/data/nosql/cosmos-db.md +114 -71
- package/framework/standards/frontend/design-system/animations.md +1 -1
- package/framework/standards/frontend/design-system/naming.md +1 -1
- package/framework/standards/frontend/nextjs/motion-patterns.md +45 -29
- package/framework/standards/frontend/nextjs/naming-conventions.md +1 -1
- package/framework/standards/infrastructure/docker/coolify-deploy.md +8 -6
- package/framework/standards/infrastructure/neon/neon-pgvector.md +120 -75
- package/framework/standards/infrastructure/neon/neon-setup.md +53 -31
- package/framework/standards/integration/mcp/mcp-tools.md +7 -7
- package/framework/templates/README.md +2 -3
- package/framework/templates/REGISTRY.json +6 -170
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +2 -2
- package/framework/templates/code/dotnet/extensions/database-extensions.cs +1 -1
- package/framework/templates/code/dotnet/extensions/result-extensions.cs +27 -0
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +2 -2
- package/framework/templates/docs/plan.md +72 -0
- package/framework/templates/dotnet/ai-kit/README.md +1 -9
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +1 -1
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
- package/framework/templates/infrastructure/docker/coolify.template.json +3 -3
- package/framework/templates/infrastructure/github/README.md +2 -1
- package/framework/templates/infrastructure/github/workflows/deploy-coolify.yml.hbs +2 -1
- package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
- package/package.json +1 -1
- package/src/commands/e2e/e2e.js +2 -2
- package/src/commands/observability/cost.js +18 -60
- package/src/commands/observability/telemetry.js +3 -5
- package/src/commands/project/doctor-procs.js +1 -1
- package/src/commands/project/doctor.js +9 -9
- package/src/commands/project/fleet.js +4 -4
- package/src/commands/project/heavy.js +3 -3
- package/src/commands/project/init.js +6 -1
- package/src/commands/project/setup-infra-cmd.js +2 -1
- package/src/commands/project/stack-scan.js +58 -0
- package/src/commands/project/standards.js +79 -25
- package/src/commands/project/status.js +29 -69
- package/src/commands/project/update.js +33 -19
- package/src/commands/project/worktree.js +1 -1
- package/src/commands/state/advance.js +62 -46
- package/src/commands/state/approve.js +95 -138
- package/src/commands/state/archive.js +20 -84
- package/src/commands/state/auto-approval.js +68 -0
- package/src/commands/state/create.js +28 -1
- package/src/commands/state/delete.js +1 -1
- package/src/commands/state/finish.js +11 -23
- package/src/commands/state/gate-check.js +86 -95
- package/src/commands/state/gate-decision.js +6 -4
- package/src/commands/state/handoff.js +1 -1
- package/src/commands/state/list.js +9 -9
- package/src/commands/state/report-check.js +157 -311
- package/src/commands/state/retype.js +9 -3
- package/src/commands/validation/mutate.js +77 -14
- package/src/commands/validation/plan-check.js +81 -0
- package/src/commands/validation/validate-feature.js +60 -135
- package/src/commands/validation/verify.js +41 -82
- package/src/core/paths/output-schema.js +20 -67
- package/src/core/state/gate-order.js +5 -6
- package/src/core/state/lifecycle-audit.js +2 -2
- package/src/core/state/state-manager.js +17 -98
- package/src/core/templates/template-registry.js +2 -2
- package/src/core/templates/template-seed.js +22 -0
- package/src/core/work-types.js +10 -10
- package/src/dashboard/app/App.jsx +0 -2
- package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
- package/src/dashboard/app/components/CommandPalette.jsx +7 -8
- package/src/dashboard/app/components/EntityLink.jsx +1 -2
- package/src/dashboard/app/components/Markdown.jsx +1 -1
- package/src/dashboard/app/components/Sidebar.jsx +1 -2
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
- package/src/dashboard/app/lib/phases.js +2 -4
- package/src/dashboard/app/pages/Activity.jsx +1 -1
- package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
- package/src/dashboard/app/pages/Features.jsx +9 -9
- package/src/dashboard/app/pages/Guide.jsx +2 -2
- package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
- package/src/dashboard/app/pages/RulesMap.jsx +3 -3
- package/src/dashboard/app/pages/SkillsLibrary.jsx +3 -3
- package/src/dashboard/app/pages/Standards.jsx +1 -1
- package/src/dashboard/scanner/activity-scanner.js +6 -16
- package/src/dashboard/scanner/agents-scanner.js +1 -1
- package/src/dashboard/scanner/docs-scanner.js +5 -23
- package/src/dashboard/scanner/features-scanner.js +95 -60
- package/src/dashboard/scanner/index.js +0 -2
- package/src/dashboard/scanner/project-api.js +1 -1
- package/src/dashboard/scanner/rules-scanner.js +15 -10
- package/src/dashboard/scanner/skills-scanner.js +2 -3
- package/src/dashboard/scanner/standards-scanner.js +2 -1
- package/src/lib/ci/ci-verify.js +17 -1
- package/src/lib/ci/github-ci.js +1 -1
- package/src/lib/detectors/index.js +3 -8
- package/src/lib/generators/handoff-generator.js +34 -65
- package/src/lib/generators/settings-generator.js +11 -89
- package/src/lib/git/evidence-staleness.js +1 -1
- package/src/lib/git/tree-fingerprint.js +19 -7
- package/src/lib/git/tree-state.js +2 -2
- package/src/lib/heavy/cleanup.js +1 -5
- package/src/lib/installers/memory-installer.js +6 -8
- package/src/lib/mutation/commands.js +3 -2
- package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
- package/src/lib/mutation/plan.js +14 -6
- package/src/lib/mutation/runner.js +1 -1
- package/src/lib/mutation/subset.js +46 -9
- package/src/lib/plan/artifact-vocabulary.js +60 -0
- package/src/lib/plan/checks.js +234 -0
- package/src/lib/plan/plan-parser.js +209 -0
- package/src/lib/plan/plan-template.js +18 -0
- package/src/lib/review-reports.js +222 -0
- package/src/lib/risk-detector.js +134 -135
- package/src/lib/specs/adr-index.js +50 -69
- package/src/lib/specs/design-seed.js +18 -0
- package/src/lib/specs/frontmatter.js +44 -0
- package/src/lib/specs/gfm-table.js +17 -0
- package/src/lib/specs/old-format.js +68 -0
- package/src/lib/specs/revocations.js +37 -95
- package/src/lib/specs/screen-checklist.js +18 -17
- package/src/lib/stack/dotnet-build-ignores.js +47 -0
- package/src/lib/stack/stack-profile.js +1 -1
- package/src/lib/stack/stack-scan.js +267 -0
- package/src/lib/standards/registry.js +43 -0
- package/src/lib/standards/sections.js +58 -0
- package/src/lib/tasks/eval-node.js +4 -12
- package/src/lib/tasks/test-nodes.js +1 -74
- package/src/lib/tasks/test-runner.js +75 -117
- package/src/lib/telemetry/metrics.js +8 -69
- package/src/lib/telemetry/telemetry-reader.js +5 -118
- package/src/lib/telemetry/verify-event.js +5 -7
- package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
- package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
- package/src/lib/validators/reuse/reuse-validator.js +24 -48
- package/src/lib/validators/reuse/symbol-scanner.js +2 -4
- package/src/lib/validators/traceability-check.js +85 -129
- package/src/lib/validators/validation-runner.js +8 -88
- package/src/lib/validators/verify-runner.js +134 -301
- package/src/lib/worktree/docker-audit.js +1 -2
- package/src/lib/worktree/fleet-scanner.js +25 -38
- package/src/scripts/setup-infra.js +42 -22
- package/src/utils/agents-installer.js +92 -13
- package/src/utils/claude-settings-manager.js +0 -35
- package/src/utils/file-copier.js +34 -6
- package/src/utils/hooks-installer.js +16 -9
- package/src/utils/skills-installer.js +1 -2
- package/framework/commands/commit.md +0 -166
- package/framework/evals/trace-schema.md +0 -67
- package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
- package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
- package/framework/hooks/git/pre-commit/specs.sh +0 -50
- package/framework/hooks/shared/tasks-validator.js +0 -111
- package/framework/rubrics/backend.json +0 -41
- package/framework/rubrics/default.json +0 -36
- package/framework/rubrics/frontend.json +0 -36
- package/framework/rules/morph-task-tracking.md +0 -54
- package/framework/rules/morph-workflow.md +0 -244
- package/framework/schemas/tasks.schema.json +0 -79
- package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
- package/framework/skills/core/morph-implement/SKILL.md +0 -194
- package/framework/skills/core/morph-proposal/SKILL.md +0 -152
- package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
- package/framework/templates/docs/proposal.md +0 -192
- package/framework/templates/docs/spec.md +0 -57
- package/framework/templates/docs/ui-components.md +0 -52
- package/framework/templates/docs/ui-design-system.md +0 -76
- package/framework/templates/docs/ui-flows.md +0 -167
- package/framework/templates/examples/spec-examples.md +0 -91
- package/framework/templates/feature/decisions.md +0 -124
- package/framework/templates/feature/recap.md +0 -145
- package/framework/templates/project/validate.js +0 -97
- package/framework/templates/tasks.template.json +0 -17
- package/src/commands/state/usecase.js +0 -143
- package/src/commands/tasks/dag.js +0 -181
- package/src/dashboard/app/pages/Rubrics.jsx +0 -97
- package/src/dashboard/scanner/rubrics-scanner.js +0 -12
- package/src/lib/agents/output-collector.js +0 -76
- package/src/lib/evaluator-report.js +0 -91
- package/src/lib/specs/acervo-format.js +0 -70
- package/src/lib/specs/acervo-lifecycle.js +0 -264
- package/src/lib/specs/done-criteria-check.js +0 -200
- package/src/lib/specs/usecase-check.js +0 -301
- package/src/lib/specs/usecase-promoter.js +0 -301
- package/src/lib/specs/usecase-registry.js +0 -237
- package/src/lib/tasks/dispatch-planner.js +0 -319
- package/src/lib/tasks/mermaid-renderer.js +0 -132
- package/src/lib/tasks/task-classifier.js +0 -30
- package/src/lib/tasks/task-parser.js +0 -209
- package/src/lib/tasks/tasks-json-parser.js +0 -296
- package/src/lib/tasks/test-scope.js +0 -597
- package/src/lib/validators/architecture/architecture-validator.js +0 -270
- package/src/lib/validators/architecture/index.js +0 -7
- package/src/lib/validators/css/css-validator.js +0 -340
- package/src/lib/validators/css/index.js +0 -7
- package/src/lib/validators/design-system/design-system-validator.js +0 -237
- package/src/lib/validators/design-system/index.js +0 -7
- package/src/lib/validators/score-aggregator.js +0 -244
- package/src/lib/validators/ui/index.js +0 -7
- package/src/lib/validators/ui/ui-contrast-validator.js +0 -422
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
> **Keywords:** coolify, docker, deploy, deployment, container, vps, ssh
|
|
6
6
|
> **Load When:** coolify or docker deployment keywords detected
|
|
7
7
|
|
|
8
|
-
**Verified against:** .NET 10 + Next.js 15 + Node 22 (Docker base images). Last-verified: 2026-
|
|
8
|
+
**Verified against:** .NET 10 + Next.js 15 + Node 22 (Docker base images) + Coolify 4.x API. The `POST /api/v1/deploy` contract was checked against the Coolify docs/OpenAPI on 2026-10-03, and Coolify 4.3.23 was observed answering `GET /deploy` with "This endpoint has changed to a POST request"; Dockerfiles and snippets are documentary. Last-verified: 2026-10-03.
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -17,14 +17,14 @@ Stack: Next.js 15 + Neon PostgreSQL + .NET Backend
|
|
|
17
17
|
|
|
18
18
|
Coolify is Polymorphism Tech's standard self-hosted deployment platform for Docker workloads.
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
We do not use a Coolify MCP server or CLI. All access is via **SSH into the VPS**, from which the Coolify API is reached at `http://localhost:8000/api/v1/`: keep the VPS firewall to SSH and HTTP(S) only, so port 8000 never answers from outside. The API token lives server-side only (a `chmod 600` file in the SSH user's home, written here as `~/<token-file>`) -- read it inside the remote command, never print it, never commit it, never let it leave the SSH session.
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
# Canonical access pattern — token is read and used entirely server-side
|
|
24
|
-
ssh <vps-ssh-alias> 'T=$(cat
|
|
24
|
+
ssh <vps-ssh-alias> 'T=$(cat ~/<token-file>); curl -s -H "Authorization: Bearer $T" http://localhost:8000/api/v1/projects'
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
Useful endpoints:
|
|
27
|
+
Useful endpoints: `GET /version`, `GET /projects`, `GET /servers`, `GET /applications`, `POST /deploy?uuid={uuid}`. Changing a domain: `PATCH /applications/{uuid}` with `{"domains":"https://x,https://www.x"}` (a `dockercompose` application refuses `domains` and takes `docker_compose_domains: [{"name":"<service>","domain":"https://x:<port>"}]`), then `POST /deploy?uuid={uuid}` to redeploy. The deploy trigger is **POST**: Coolify 4.3.23 answers a `GET /deploy` with "This endpoint has changed to a POST request" and deploys nothing, so a script that only greps for an error status reports a deploy that never happened.
|
|
28
28
|
|
|
29
29
|
## Core Rules
|
|
30
30
|
|
|
@@ -202,13 +202,15 @@ const logger = pino({ level: process.env.LOG_LEVEL ?? "info" });
|
|
|
202
202
|
|
|
203
203
|
## Triggering a Deploy via SSH + API
|
|
204
204
|
|
|
205
|
-
|
|
205
|
+
Every deploy operation (manual redeploy, CI/CD, or troubleshooting) goes through the same SSH + curl pattern used for all other Coolify access:
|
|
206
206
|
|
|
207
207
|
```bash
|
|
208
208
|
# Redeploy an existing application after a new image/commit is ready
|
|
209
|
-
ssh <vps-ssh-alias> 'T=$(cat
|
|
209
|
+
ssh <vps-ssh-alias> 'T=$(cat ~/<token-file>); curl -s -X POST -H "Authorization: Bearer $T" "http://localhost:8000/api/v1/deploy?uuid=<application-uuid>"'
|
|
210
210
|
```
|
|
211
211
|
|
|
212
|
+
A queued deploy answers `{"deployments":[{"message":...,"resource_uuid":...,"deployment_uuid":...}]}`; treat a response without `deployment_uuid` as "nothing was deployed". `force=true` rebuilds without the build cache.
|
|
213
|
+
|
|
212
214
|
In GitHub Actions this becomes an SSH step using a deploy key stored as a secret (see the `github-workflow-deploy-coolify` template) -- never a direct HTTP call to the Coolify API from the runner, since the API port is not reachable outside the VPS.
|
|
213
215
|
|
|
214
216
|
## Deployment Checklist
|
|
@@ -5,20 +5,22 @@
|
|
|
5
5
|
> **Keywords:** pgvector, vector, embeddings, similarity search, HNSW, cosine
|
|
6
6
|
> **Load When:** pgvector or embedding keywords detected
|
|
7
7
|
|
|
8
|
-
**Verified against:** .NET 10 + Pgvector.EntityFrameworkCore +
|
|
8
|
+
**Verified against:** .NET 10 (SDK 10.0.400-preview) + Npgsql.EntityFrameworkCore.PostgreSQL 10.0.3 + Pgvector.EntityFrameworkCore 0.3.0 + Microsoft.Extensions.AI 10.9.0 (ai-pin 2026-09-08). The entity, configuration, DbContext, search-handler and raw-Npgsql blocks compile with `TreatWarningsAsErrors` (handler over the `Result`/`IHandler` of `result-pattern.md`/`vertical-slice.md`), and the handler's query shape was translated with `ToQueryString()` to `ORDER BY d.embedding <=> @query`; the SQL (index DDL, `ef_search`, the two SQL functions) is documentary, checked against the pgvector README. Last-verified: 2026-10-03.
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
-
Stack: .NET VSA + Neon Serverless Postgres
|
|
12
|
+
Stack: .NET VSA + Neon Serverless Postgres. For the end-to-end RAG pipeline (ingestion, retrieval, generation) on top of this mapping, see `backend/database/vector-search-rag.md`.
|
|
13
13
|
|
|
14
14
|
## Core Rules
|
|
15
15
|
|
|
16
16
|
- ALWAYS use HNSW indexes for production (faster queries, no training data required)
|
|
17
17
|
- ALWAYS match vector dimensions to your embedding model (e.g., 1536 for text-embedding-3-small)
|
|
18
18
|
- NEVER store embeddings without an index -- full table scan at query time
|
|
19
|
-
- ALWAYS call `UseVector()` on `NpgsqlDataSourceBuilder`
|
|
20
|
-
- ALWAYS register the `vector` extension in `OnModelCreating`
|
|
21
|
-
-
|
|
19
|
+
- ALWAYS call `UseVector()` on the `UseNpgsql(...)` options builder (EF Core); `NpgsqlDataSourceBuilder.UseVector()` is only for raw Npgsql
|
|
20
|
+
- ALWAYS register the `vector` extension in `OnModelCreating` (`HasPostgresExtension("vector")`)
|
|
21
|
+
- Map vector properties as `Pgvector.Vector` -- EF Core does not map `float[]` or `ReadOnlyMemory<float>` to the `vector` column
|
|
22
|
+
- Query through the `ApplicationDbContext` in the slice handler with the `CosineDistance`/`L2Distance`/`MaxInnerProduct` LINQ translations -- no repository class, no raw SQL from C# without a `decisions.md` justification
|
|
23
|
+
- IDs come from the app (`Guid.CreateVersion7()`), never from a `gen_random_uuid()` column default
|
|
22
24
|
|
|
23
25
|
## Enable pgvector
|
|
24
26
|
|
|
@@ -26,54 +28,56 @@ Stack: .NET VSA + Neon Serverless Postgres
|
|
|
26
28
|
CREATE EXTENSION IF NOT EXISTS vector;
|
|
27
29
|
```
|
|
28
30
|
|
|
31
|
+
`HasPostgresExtension("vector")` in `OnModelCreating` makes the EF Core migration emit this statement, so a hand-written migration is not needed.
|
|
32
|
+
|
|
29
33
|
## EF Core Entity Mapping
|
|
30
34
|
|
|
31
|
-
### NuGet
|
|
35
|
+
### NuGet Packages
|
|
32
36
|
|
|
33
|
-
```
|
|
34
|
-
|
|
37
|
+
```xml
|
|
38
|
+
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
|
|
39
|
+
<PackageReference Include="Pgvector.EntityFrameworkCore" Version="0.3.0" />
|
|
35
40
|
```
|
|
36
41
|
|
|
42
|
+
`Pgvector.EntityFrameworkCore` 0.3.0 requires `Npgsql.EntityFrameworkCore.PostgreSQL` >= 9.0.1 and brings `Pgvector` (the `Vector` type and the raw-Npgsql plugin) transitively. The configuration below maps each column to the snake_case names of `backend/database/postgresql/database.md` (`embedding`, `created_at`) with `HasColumnName`; without it EF creates `"Embedding"`, `"CreatedAt"` and every SQL snippet below would have to quote PascalCase names. (A project that wants this for every entity can add `EFCore.NamingConventions` and `UseSnakeCaseNamingConvention()` once, instead of per column -- a project-wide decision for `decisions.md`.)
|
|
43
|
+
|
|
37
44
|
### Entity Definition
|
|
38
45
|
|
|
39
46
|
```csharp
|
|
40
|
-
using System.ComponentModel.DataAnnotations.Schema;
|
|
41
47
|
using Pgvector;
|
|
42
48
|
|
|
43
|
-
public class Document
|
|
49
|
+
public sealed class Document
|
|
44
50
|
{
|
|
45
51
|
public Guid Id { get; set; }
|
|
46
52
|
public string Title { get; set; } = string.Empty;
|
|
47
53
|
public string Content { get; set; } = string.Empty;
|
|
48
54
|
public string? Metadata { get; set; }
|
|
49
|
-
|
|
50
|
-
[Column(TypeName = "vector(1536)")]
|
|
51
55
|
public Vector? Embedding { get; set; }
|
|
52
|
-
|
|
53
|
-
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
|
56
|
+
public DateTimeOffset CreatedAt { get; set; }
|
|
54
57
|
}
|
|
55
58
|
```
|
|
56
59
|
|
|
60
|
+
`Id` and `CreatedAt` are set by the handler that creates the row (`Guid.CreateVersion7()` and `TimeProvider.GetUtcNow()`), not by property initializers or column defaults. `Embedding` is nullable because a row can exist before its embedding is generated.
|
|
61
|
+
|
|
57
62
|
### IEntityTypeConfiguration
|
|
58
63
|
|
|
59
64
|
```csharp
|
|
60
65
|
using Microsoft.EntityFrameworkCore;
|
|
61
66
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
|
62
67
|
|
|
63
|
-
public class DocumentConfiguration : IEntityTypeConfiguration<Document>
|
|
68
|
+
public sealed class DocumentConfiguration : IEntityTypeConfiguration<Document>
|
|
64
69
|
{
|
|
65
70
|
public void Configure(EntityTypeBuilder<Document> builder)
|
|
66
71
|
{
|
|
67
72
|
builder.ToTable("documents");
|
|
68
73
|
builder.HasKey(d => d.Id);
|
|
69
|
-
builder.Property(d => d.Id).
|
|
70
|
-
builder.Property(d => d.Title).IsRequired();
|
|
71
|
-
builder.Property(d => d.Content).IsRequired();
|
|
72
|
-
builder.Property(d => d.Metadata).HasColumnType("jsonb");
|
|
73
|
-
builder.Property(d => d.Embedding).HasColumnType("vector(1536)");
|
|
74
|
-
builder.Property(d => d.CreatedAt).
|
|
75
|
-
|
|
76
|
-
// HNSW index for cosine similarity
|
|
74
|
+
builder.Property(d => d.Id).HasColumnName("id").ValueGeneratedNever();
|
|
75
|
+
builder.Property(d => d.Title).HasColumnName("title").IsRequired();
|
|
76
|
+
builder.Property(d => d.Content).HasColumnName("content").IsRequired();
|
|
77
|
+
builder.Property(d => d.Metadata).HasColumnName("metadata").HasColumnType("jsonb");
|
|
78
|
+
builder.Property(d => d.Embedding).HasColumnName("embedding").HasColumnType("vector(1536)");
|
|
79
|
+
builder.Property(d => d.CreatedAt).HasColumnName("created_at");
|
|
80
|
+
|
|
77
81
|
builder.HasIndex(d => d.Embedding)
|
|
78
82
|
.HasMethod("hnsw")
|
|
79
83
|
.HasOperators("vector_cosine_ops")
|
|
@@ -86,12 +90,12 @@ public class DocumentConfiguration : IEntityTypeConfiguration<Document>
|
|
|
86
90
|
### DbContext Setup
|
|
87
91
|
|
|
88
92
|
```csharp
|
|
89
|
-
|
|
93
|
+
using Microsoft.EntityFrameworkCore;
|
|
94
|
+
|
|
95
|
+
public sealed class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : DbContext(options)
|
|
90
96
|
{
|
|
91
97
|
public DbSet<Document> Documents => Set<Document>();
|
|
92
98
|
|
|
93
|
-
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
|
|
94
|
-
|
|
95
99
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
96
100
|
{
|
|
97
101
|
modelBuilder.HasPostgresExtension("vector");
|
|
@@ -99,13 +103,22 @@ public class AppDbContext : DbContext
|
|
|
99
103
|
}
|
|
100
104
|
}
|
|
101
105
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
106
|
+
public static class VectorDatabaseRegistration
|
|
107
|
+
{
|
|
108
|
+
public static IServiceCollection AddVectorDatabase(this IServiceCollection services, IConfiguration configuration) =>
|
|
109
|
+
services.AddDbContext<ApplicationDbContext>(options =>
|
|
110
|
+
options.UseNpgsql(
|
|
111
|
+
configuration.GetConnectionString("DefaultConnection"),
|
|
112
|
+
npgsql => npgsql.UseVector()));
|
|
113
|
+
}
|
|
105
114
|
```
|
|
106
115
|
|
|
116
|
+
The connection string comes from configuration (user-secrets in development, environment variables in Coolify) -- see `infrastructure/neon/neon-setup.md`.
|
|
117
|
+
|
|
107
118
|
## HNSW Index Creation (Raw SQL)
|
|
108
119
|
|
|
120
|
+
The EF configuration above generates the cosine index. The SQL equivalents, for reference and for hand-written migrations:
|
|
121
|
+
|
|
109
122
|
```sql
|
|
110
123
|
-- Cosine distance (most common for normalized embeddings)
|
|
111
124
|
CREATE INDEX idx_documents_embedding ON documents
|
|
@@ -115,10 +128,15 @@ CREATE INDEX idx_documents_embedding ON documents
|
|
|
115
128
|
CREATE INDEX idx_documents_embedding_l2 ON documents
|
|
116
129
|
USING hnsw (embedding vector_l2_ops) WITH (m = 16, ef_construction = 64);
|
|
117
130
|
|
|
118
|
-
-- Tune search accuracy
|
|
119
|
-
|
|
131
|
+
-- Tune search accuracy for the current transaction
|
|
132
|
+
BEGIN;
|
|
133
|
+
SET LOCAL hnsw.ef_search = 100;
|
|
134
|
+
-- ... similarity query ...
|
|
135
|
+
COMMIT;
|
|
120
136
|
```
|
|
121
137
|
|
|
138
|
+
On the Neon pooled (`-pooler`, PgBouncer transaction mode) endpoint a plain `SET` does not stick to your next statement: use `SET LOCAL` inside the same transaction as the query, or set it per role/database.
|
|
139
|
+
|
|
122
140
|
### HNSW Parameters
|
|
123
141
|
|
|
124
142
|
| Parameter | Default | Tuning |
|
|
@@ -129,15 +147,58 @@ SET hnsw.ef_search = 100;
|
|
|
129
147
|
|
|
130
148
|
## Distance Operators
|
|
131
149
|
|
|
132
|
-
| Operator |
|
|
133
|
-
|
|
134
|
-
| `<=>` |
|
|
135
|
-
| `<->` |
|
|
136
|
-
| `<#>` |
|
|
150
|
+
| Operator | LINQ (Pgvector.EntityFrameworkCore) | Index Ops | Use Case |
|
|
151
|
+
|----------|-------------------------------------|-----------|----------|
|
|
152
|
+
| `<=>` | `CosineDistance` | `vector_cosine_ops` | Normalized embeddings (most common) |
|
|
153
|
+
| `<->` | `L2Distance` | `vector_l2_ops` | Spatial/positional data |
|
|
154
|
+
| `<#>` | `MaxInnerProduct` (negative inner product) | `vector_ip_ops` | Pre-normalized, max similarity |
|
|
155
|
+
|
|
156
|
+
The index operator class must match the function used in the query, or Postgres ignores the index and falls back to a sequential scan.
|
|
137
157
|
|
|
138
158
|
## Similarity Search
|
|
139
159
|
|
|
140
|
-
###
|
|
160
|
+
### Search handler (EF Core in the slice)
|
|
161
|
+
|
|
162
|
+
```csharp
|
|
163
|
+
// Features/DocumentFeature/SearchDocuments/SearchDocumentsHandler.cs
|
|
164
|
+
using Microsoft.EntityFrameworkCore;
|
|
165
|
+
using Microsoft.Extensions.AI;
|
|
166
|
+
using Pgvector;
|
|
167
|
+
using Pgvector.EntityFrameworkCore;
|
|
168
|
+
|
|
169
|
+
public sealed record SearchDocumentsRequest(string Query, int Limit = 10, double MinSimilarity = 0.78);
|
|
170
|
+
public sealed record DocumentMatch(Guid Id, string Title, string Content, double Similarity);
|
|
171
|
+
|
|
172
|
+
public sealed class SearchDocumentsHandler(
|
|
173
|
+
ApplicationDbContext db,
|
|
174
|
+
IEmbeddingGenerator<string, Embedding<float>> embeddings)
|
|
175
|
+
: IHandler<SearchDocumentsRequest, Result<IReadOnlyList<DocumentMatch>>>
|
|
176
|
+
{
|
|
177
|
+
public async Task<Result<IReadOnlyList<DocumentMatch>>> HandleAsync(
|
|
178
|
+
SearchDocumentsRequest request, CancellationToken cancellationToken)
|
|
179
|
+
{
|
|
180
|
+
var query = new Vector(await embeddings.GenerateVectorAsync(request.Query, cancellationToken: cancellationToken));
|
|
181
|
+
var maxDistance = 1 - request.MinSimilarity;
|
|
182
|
+
|
|
183
|
+
var matches = await db.Documents
|
|
184
|
+
.Where(d => d.Embedding != null)
|
|
185
|
+
.Select(d => new { d.Id, d.Title, d.Content, Distance = d.Embedding!.CosineDistance(query) })
|
|
186
|
+
.Where(x => x.Distance <= maxDistance)
|
|
187
|
+
.OrderBy(x => x.Distance)
|
|
188
|
+
.Take(request.Limit)
|
|
189
|
+
.Select(x => new DocumentMatch(x.Id, x.Title, x.Content, 1 - x.Distance))
|
|
190
|
+
.ToListAsync(cancellationToken);
|
|
191
|
+
|
|
192
|
+
return Result.Success<IReadOnlyList<DocumentMatch>>(matches);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Storing an embedding is an ordinary EF write in the handler that owns the document: `document.Embedding = new Vector(await embeddings.GenerateVectorAsync(text, cancellationToken: ct));` followed by `SaveChangesAsync`. No `UPDATE ... SET embedding` from C#.
|
|
198
|
+
|
|
199
|
+
### SQL Functions (only with a `decisions.md` justification)
|
|
200
|
+
|
|
201
|
+
When the query must live in the database (called from several apps, or a hybrid score the LINQ translation cannot express), a SQL function is acceptable -- record why in `decisions.md`, call it through EF Core with interpolated parameters, and keep the same distance rules:
|
|
141
202
|
|
|
142
203
|
```sql
|
|
143
204
|
CREATE OR REPLACE FUNCTION match_documents(
|
|
@@ -155,40 +216,10 @@ LANGUAGE sql STABLE AS $$
|
|
|
155
216
|
$$;
|
|
156
217
|
```
|
|
157
218
|
|
|
158
|
-
### C# Repository
|
|
159
|
-
|
|
160
|
-
```csharp
|
|
161
|
-
using Pgvector;
|
|
162
|
-
|
|
163
|
-
public sealed class DocumentRepository(AppDbContext db)
|
|
164
|
-
{
|
|
165
|
-
public async Task StoreEmbeddingAsync(
|
|
166
|
-
Guid documentId, float[] embedding, CancellationToken ct = default)
|
|
167
|
-
{
|
|
168
|
-
await db.Database.ExecuteSqlInterpolatedAsync(
|
|
169
|
-
$"UPDATE documents SET embedding = {new Vector(embedding)} WHERE id = {documentId}", ct);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
public async Task<List<DocumentMatch>> SearchSimilarAsync(
|
|
173
|
-
float[] queryEmbedding, int limit = 10, float threshold = 0.78f,
|
|
174
|
-
CancellationToken ct = default)
|
|
175
|
-
{
|
|
176
|
-
return await db.Database.SqlQuery<DocumentMatch>($"""
|
|
177
|
-
SELECT id, title, content,
|
|
178
|
-
1 - (embedding <=> {new Vector(queryEmbedding)}::vector) AS similarity
|
|
179
|
-
FROM documents
|
|
180
|
-
WHERE 1 - (embedding <=> {new Vector(queryEmbedding)}::vector) > {threshold}
|
|
181
|
-
ORDER BY embedding <=> {new Vector(queryEmbedding)}::vector
|
|
182
|
-
LIMIT {limit}
|
|
183
|
-
""").ToListAsync(ct);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
public record DocumentMatch(Guid Id, string Title, string Content, float Similarity);
|
|
188
|
-
```
|
|
189
|
-
|
|
190
219
|
## Hybrid Search (Vector + Full-Text)
|
|
191
220
|
|
|
221
|
+
Blends the vector score with `ts_rank`. Same rule as above: a SQL function, justified in `decisions.md`.
|
|
222
|
+
|
|
192
223
|
```sql
|
|
193
224
|
CREATE OR REPLACE FUNCTION hybrid_search(
|
|
194
225
|
query_text text,
|
|
@@ -218,14 +249,25 @@ LANGUAGE sql STABLE AS $$
|
|
|
218
249
|
$$;
|
|
219
250
|
```
|
|
220
251
|
|
|
252
|
+
For Portuguese content use the `'portuguese'` text search configuration in both `to_tsvector` and `plainto_tsquery`.
|
|
253
|
+
|
|
221
254
|
## Npgsql Vector Type Handler
|
|
222
255
|
|
|
223
|
-
|
|
256
|
+
With EF Core, `npgsql.UseVector()` inside `UseNpgsql(...)` registers the type mapping and the LINQ translations. For raw Npgsql (without EF Core), register the plugin on the data source:
|
|
224
257
|
|
|
225
258
|
```csharp
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
259
|
+
using Npgsql;
|
|
260
|
+
using Pgvector.Npgsql;
|
|
261
|
+
|
|
262
|
+
public static class RawVectorDataSource
|
|
263
|
+
{
|
|
264
|
+
public static NpgsqlDataSource Build(string connectionString)
|
|
265
|
+
{
|
|
266
|
+
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);
|
|
267
|
+
dataSourceBuilder.UseVector();
|
|
268
|
+
return dataSourceBuilder.Build();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
229
271
|
```
|
|
230
272
|
|
|
231
273
|
## Common Mistakes
|
|
@@ -234,7 +276,10 @@ await using var dataSource = dataSourceBuilder.Build();
|
|
|
234
276
|
|-------|-------|-----|
|
|
235
277
|
| No index on embedding column | HNSW index | Full table scan, extremely slow |
|
|
236
278
|
| `ORDER BY similarity DESC` | `ORDER BY embedding <=> query ASC` | Operators return distance, not similarity |
|
|
237
|
-
| Missing `UseVector()` |
|
|
279
|
+
| Missing `UseVector()` | `npgsql.UseVector()` in `UseNpgsql(...)` | Npgsql cannot map vector types without it |
|
|
238
280
|
| Mixing embedding dimensions | Consistent dimensions per column | Dimension mismatch causes runtime errors |
|
|
239
|
-
| `float[]` for EF
|
|
281
|
+
| `float[]` / `ReadOnlyMemory<float>` for the EF property | `Vector` from the Pgvector package | EF Core mapping requires the Pgvector type |
|
|
240
282
|
| No `HasPostgresExtension("vector")` | Add in `OnModelCreating` | Extension must be declared for migrations |
|
|
283
|
+
| A `DocumentRepository` with `ExecuteSqlInterpolatedAsync`/`SqlQuery` | LINQ `CosineDistance` in the slice handler | `DbSet<T>` is already the repository; raw SQL bypasses the translation that keeps the index in use |
|
|
284
|
+
| `SET hnsw.ef_search` on the pooled endpoint | `SET LOCAL` inside the query's transaction | PgBouncer transaction mode hands the next statement another server session |
|
|
285
|
+
| `HasDefaultValueSql("gen_random_uuid()")` on the id | `Id = Guid.CreateVersion7()` in the handler | App-generated, time-ordered ids |
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Neon Serverless Postgres Standard
|
|
2
2
|
|
|
3
3
|
> **Scope:** neon
|
|
4
|
-
> **Layer:** 0 (
|
|
4
|
+
> **Layer:** 0 (resumo injetado pela rule `neon-standards` em `**/neon/**`, `**/database/**` e migrations `.sql`; texto completo sob demanda via `morph-spec standards --find`)
|
|
5
5
|
> **Keywords:** neon, serverless postgres, branching, scale-to-zero, connection string, neonctl
|
|
6
6
|
> **Load When:** neon stack detected
|
|
7
7
|
|
|
8
|
-
**Verified against:** .NET 10 + Npgsql
|
|
8
|
+
**Verified against:** .NET 10 (SDK 10.0.400-preview) + Npgsql.EntityFrameworkCore.PostgreSQL 10.0.3 + neonctl. The DbContext registration, design-time factory and cold-start blocks compile with `TreatWarningsAsErrors`; that Npgsql 10 rejects `postgresql://` URIs and marks `Trust Server Certificate` as "no longer needed and does nothing" was measured on 10.0.3; the suspend-timeout API and `--cu` ranges were checked against the Neon docs (context7). neonctl commands are documentary. Last-verified: 2026-10-03.
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -15,7 +15,7 @@ Stack: .NET + Neon Serverless Postgres
|
|
|
15
15
|
|
|
16
16
|
- ALWAYS use pooled connection strings (`-pooler` endpoint) for application traffic
|
|
17
17
|
- ALWAYS use direct connection strings for migrations and schema changes
|
|
18
|
-
- NEVER hardcode connection strings --
|
|
18
|
+
- NEVER hardcode connection strings or commit them in `appsettings*.json` -- user-secrets in development, environment variables in Coolify
|
|
19
19
|
- ALWAYS enable SSL (`SSL Mode=Require`) for all connections
|
|
20
20
|
- ALWAYS configure retry policies for cold-start resilience
|
|
21
21
|
- NEVER leave branches without expiration -- set TTL for dev/CI branches
|
|
@@ -42,38 +42,49 @@ neonctl projects list
|
|
|
42
42
|
| Pooled | `ep-xxxx-pooler.region.aws.neon.tech` | Application queries, high concurrency |
|
|
43
43
|
| Direct | `ep-xxxx.region.aws.neon.tech` | Migrations, EF Core `dotnet ef`, advisory locks |
|
|
44
44
|
|
|
45
|
-
###
|
|
45
|
+
### Configuration Keys
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|----------|---------|
|
|
49
|
-
| `DATABASE_URL` | Pooled connection for application runtime |
|
|
50
|
-
| `DATABASE_URL_UNPOOLED` | Direct connection for migrations and CLI tools |
|
|
47
|
+
The .NET app reads two connection strings through `IConfiguration`. Neon's integrations call them `DATABASE_URL` / `DATABASE_URL_UNPOOLED`; in .NET they are:
|
|
51
48
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
DATABASE_URL
|
|
49
|
+
| .NET key (env var form) | Neon name | Purpose |
|
|
50
|
+
|-------------------------|-----------|---------|
|
|
51
|
+
| `ConnectionStrings:DefaultConnection` (`ConnectionStrings__DefaultConnection`) | `DATABASE_URL` | Pooled connection for application runtime |
|
|
52
|
+
| `ConnectionStrings:MigrationConnection` (`ConnectionStrings__MigrationConnection`) | `DATABASE_URL_UNPOOLED` | Direct connection for migrations and CLI tools |
|
|
55
53
|
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
Npgsql only parses the **keyword** format (`Host=...;Database=...`). The `postgresql://user:pass@host/db?sslmode=require` URI that the Neon console shows by default throws `ArgumentException` in `NpgsqlConnectionStringBuilder` (measured on Npgsql 10.0.3): copy the .NET form from the console's connection dialog, or convert it.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Development: user-secrets (stored outside the repo, per user)
|
|
58
|
+
dotnet user-secrets init
|
|
59
|
+
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Host=<endpoint-id>-pooler.<region>.aws.neon.tech;Database=<db>;Username=<role>;Password=<password>;SSL Mode=Require"
|
|
60
|
+
dotnet user-secrets set "ConnectionStrings:MigrationConnection" "Host=<endpoint-id>.<region>.aws.neon.tech;Database=<db>;Username=<role>;Password=<password>;SSL Mode=Require"
|
|
61
|
+
|
|
62
|
+
# Production (Coolify environment variables, set in the UI or API -- never in the Dockerfile)
|
|
63
|
+
# ConnectionStrings__DefaultConnection=Host=<endpoint-id>-pooler.<region>.aws.neon.tech;...;SSL Mode=Require
|
|
64
|
+
# ConnectionStrings__MigrationConnection=Host=<endpoint-id>.<region>.aws.neon.tech;...;SSL Mode=Require
|
|
58
65
|
```
|
|
59
66
|
|
|
60
67
|
## EF Core Configuration
|
|
61
68
|
|
|
62
69
|
### appsettings.json
|
|
63
70
|
|
|
71
|
+
`appsettings.json` is committed, so it carries **no** connection string with a password -- the keys come from user-secrets / environment variables, which override it:
|
|
72
|
+
|
|
64
73
|
```json
|
|
65
74
|
{
|
|
66
75
|
"ConnectionStrings": {
|
|
67
|
-
"DefaultConnection": "
|
|
68
|
-
"MigrationConnection": "
|
|
76
|
+
"DefaultConnection": "",
|
|
77
|
+
"MigrationConnection": ""
|
|
69
78
|
}
|
|
70
79
|
}
|
|
71
80
|
```
|
|
72
81
|
|
|
82
|
+
Drop `Trust Server Certificate=true` from older samples (including Neon's own): since Npgsql 6 it is obsolete, and on Npgsql 10 the builder's attribute reads "no longer needed and does nothing". `SSL Mode=Require` encrypts; `SSL Mode=VerifyFull` also validates the certificate chain and host name.
|
|
83
|
+
|
|
73
84
|
### DbContext Registration
|
|
74
85
|
|
|
75
86
|
```csharp
|
|
76
|
-
builder.Services.AddDbContext<
|
|
87
|
+
builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
|
77
88
|
options.UseNpgsql(
|
|
78
89
|
builder.Configuration.GetConnectionString("DefaultConnection"),
|
|
79
90
|
npgsqlOptions =>
|
|
@@ -89,24 +100,30 @@ builder.Services.AddDbContext<AppDbContext>(options =>
|
|
|
89
100
|
### Design-Time Factory (for migrations using direct connection)
|
|
90
101
|
|
|
91
102
|
```csharp
|
|
92
|
-
|
|
103
|
+
using Microsoft.EntityFrameworkCore;
|
|
104
|
+
using Microsoft.EntityFrameworkCore.Design;
|
|
105
|
+
|
|
106
|
+
public sealed class ApplicationDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
|
|
93
107
|
{
|
|
94
|
-
public
|
|
108
|
+
public ApplicationDbContext CreateDbContext(string[] args)
|
|
95
109
|
{
|
|
96
110
|
var configuration = new ConfigurationBuilder()
|
|
97
111
|
.AddJsonFile("appsettings.json")
|
|
112
|
+
.AddUserSecrets<ApplicationDbContextFactory>(optional: true)
|
|
98
113
|
.AddEnvironmentVariables()
|
|
99
114
|
.Build();
|
|
100
115
|
|
|
101
|
-
var optionsBuilder = new DbContextOptionsBuilder<
|
|
116
|
+
var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
|
|
102
117
|
optionsBuilder.UseNpgsql(
|
|
103
118
|
configuration.GetConnectionString("MigrationConnection"));
|
|
104
119
|
|
|
105
|
-
return new
|
|
120
|
+
return new ApplicationDbContext(optionsBuilder.Options);
|
|
106
121
|
}
|
|
107
122
|
}
|
|
108
123
|
```
|
|
109
124
|
|
|
125
|
+
`AddUserSecrets` reads the same secrets the app uses in development, and `AddEnvironmentVariables` (last, so it wins) lets CI pass `ConnectionStrings__MigrationConnection`.
|
|
126
|
+
|
|
110
127
|
## Branching Workflow
|
|
111
128
|
|
|
112
129
|
Neon branches are copy-on-write clones of your database -- instant, zero-cost until data diverges.
|
|
@@ -149,8 +166,10 @@ Neon automatically suspends computes after inactivity (default: 5 minutes). This
|
|
|
149
166
|
### Mitigate Cold Starts
|
|
150
167
|
|
|
151
168
|
```csharp
|
|
169
|
+
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
|
|
170
|
+
|
|
152
171
|
// 1. Configure retry policy to handle connection drops on wake-up
|
|
153
|
-
builder.Services.AddDbContext<
|
|
172
|
+
builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
|
154
173
|
options.UseNpgsql(connectionString, npgsqlOptions =>
|
|
155
174
|
{
|
|
156
175
|
npgsqlOptions.EnableRetryOnFailure(
|
|
@@ -159,32 +178,35 @@ builder.Services.AddDbContext<AppDbContext>(options =>
|
|
|
159
178
|
errorCodesToAdd: null);
|
|
160
179
|
}));
|
|
161
180
|
|
|
162
|
-
// 2. Health check to warm the connection pool
|
|
181
|
+
// 2. Health check to warm the connection pool (package AspNetCore.HealthChecks.NpgSql)
|
|
163
182
|
builder.Services.AddHealthChecks()
|
|
164
|
-
.AddNpgSql(connectionString
|
|
183
|
+
.AddNpgSql(connectionString!, name: "neon-db");
|
|
165
184
|
```
|
|
166
185
|
|
|
167
186
|
### Suspend Timeout Configuration
|
|
168
187
|
|
|
169
|
-
|
|
170
|
-
# Set suspend timeout to 10 minutes (600 seconds)
|
|
171
|
-
neonctl branches create --project-id <project-id> \
|
|
172
|
-
--name main \
|
|
173
|
-
--suspend-timeout 600
|
|
188
|
+
Set it when the branch (and its compute) is created; `main` already exists, so it is never recreated to change a setting:
|
|
174
189
|
|
|
175
|
-
|
|
190
|
+
```bash
|
|
191
|
+
# New branch with a 10-minute suspend timeout and an autoscaling range
|
|
176
192
|
neonctl branches create --project-id <project-id> \
|
|
177
|
-
--name
|
|
193
|
+
--name feature/my-feature \
|
|
194
|
+
--parent main \
|
|
195
|
+
--suspend-timeout 600 \
|
|
178
196
|
--cu "0.25-4"
|
|
179
197
|
```
|
|
180
198
|
|
|
199
|
+
For an **existing** compute (e.g. `main`'s), change `suspend_timeout_seconds` in the Console or with the API (`PATCH /projects/{project-id}/endpoints/{endpoint-id}` with `{"endpoint":{"suspend_timeout_seconds":600}}`; default 300, min 60, max 604800), with the Neon API key read from the environment, never typed into the command.
|
|
200
|
+
|
|
181
201
|
## Common Mistakes
|
|
182
202
|
|
|
183
203
|
| Wrong | Right | Why |
|
|
184
204
|
|-------|-------|-----|
|
|
185
205
|
| Direct connection for app queries | Pooled (`-pooler`) connection | Direct connections exhaust compute limits |
|
|
186
206
|
| No retry policy | `EnableRetryOnFailure()` | Cold starts drop first connection |
|
|
187
|
-
| Hardcoded connection strings |
|
|
207
|
+
| Hardcoded / committed connection strings | user-secrets (dev), env vars (Coolify) | Branches have unique endpoints, and a committed password leaks |
|
|
208
|
+
| `postgresql://` URI in `ConnectionStrings` | Keyword format (`Host=...;SSL Mode=Require`) | Npgsql does not parse URIs |
|
|
209
|
+
| `Trust Server Certificate=true` | Drop it (`SSL Mode=Require` or `VerifyFull`) | Obsolete; does nothing on Npgsql 10 |
|
|
188
210
|
| Permanent CI branches | `--expires-at` TTL | Orphaned branches waste resources |
|
|
189
211
|
| Running migrations on pooled connection | Direct connection for `dotnet ef` | PgBouncer doesn't support advisory locks |
|
|
190
212
|
| No SSL | `SSL Mode=Require` | Neon requires encrypted connections |
|
|
@@ -195,14 +195,14 @@ Fallback: WebSearch for library capabilities.
|
|
|
195
195
|
|
|
196
196
|
| Tool | Use Case | Example |
|
|
197
197
|
|------|----------|---------|
|
|
198
|
-
| **GitHub CLI (`gh`)** | Create issues from
|
|
198
|
+
| **GitHub CLI (`gh`)** | Create issues from slices, link to milestone | `gh issue create` (Bash) |
|
|
199
199
|
| **Context7** | Estimate complexity based on library docs | `mcp__context7__query-docs()` |
|
|
200
200
|
|
|
201
201
|
```bash
|
|
202
|
-
# Create GitHub issues from
|
|
203
|
-
# — one `gh issue create` per
|
|
204
|
-
gh issue create --title "
|
|
205
|
-
--body "
|
|
202
|
+
# Create GitHub issues from the plan's slices (if team uses GitHub Projects)
|
|
203
|
+
# — one `gh issue create` per slice of .morph/features/{f}/2-plan/checks.json:
|
|
204
|
+
gh issue create --title "F1: Create Entity Lead" \
|
|
205
|
+
--body "fatia F1 do plan.md + cenários que ela cobre" \
|
|
206
206
|
--label "backend" --milestone "feature-x"
|
|
207
207
|
```
|
|
208
208
|
|
|
@@ -234,7 +234,7 @@ psql $(neonctl connection-string) -c "SELECT column_name, data_type FROM informa
|
|
|
234
234
|
psql $(neonctl connection-string) -c "SELECT policyname, cmd, qual FROM pg_policies WHERE tablename = 'leads'"
|
|
235
235
|
```
|
|
236
236
|
|
|
237
|
-
Look up the API while coding, then smoke test the deployed feature: the snapshot shows whether the page renders, the console messages catch JavaScript errors and failed API calls, and the screenshot goes to `
|
|
237
|
+
Look up the API while coding, then smoke test the deployed feature: the snapshot shows whether the page renders, the console messages catch JavaScript errors and failed API calls, and the screenshot goes to the slice report (`3-implement/reports/F{n}.md`). Vercel covers deployment status, logs and environment variables.
|
|
238
238
|
|
|
239
239
|
```javascript
|
|
240
240
|
const docs = await mcp__context7__query-docs({
|
|
@@ -329,7 +329,7 @@ await mcp__playwright__browser_file_upload({ ref: "s1e20", paths: ["/path/to/fil
|
|
|
329
329
|
| Implement (7) | Smoke test deployed features | `browser_navigate` + `browser_click` + `browser_snapshot` |
|
|
330
330
|
| Implement (7) | Verify form flows end-to-end | `browser_fill_form` + `browser_click` + `browser_snapshot` |
|
|
331
331
|
| Implement (7) | Check console errors after deploy | `browser_navigate` + `browser_console_messages` |
|
|
332
|
-
| Implement (7) | Screenshot for
|
|
332
|
+
| Implement (7) | Screenshot for the slice report | `browser_take_screenshot` |
|
|
333
333
|
|
|
334
334
|
---
|
|
335
335
|
|
|
@@ -18,8 +18,7 @@ through the registry, and this README never repeats a path the registry already
|
|
|
18
18
|
|
|
19
19
|
```
|
|
20
20
|
framework/templates/
|
|
21
|
-
├── docs/ #
|
|
22
|
-
├── feature/ # decisions.md, recap.md
|
|
21
|
+
├── docs/ # plan.md (the plan's single source) and ui-mockups.md
|
|
23
22
|
├── context/ # ARCHITECTURE.md, design-system.md
|
|
24
23
|
│
|
|
25
24
|
├── code/ # Code templates, by technology
|
|
@@ -38,7 +37,7 @@ framework/templates/
|
|
|
38
37
|
├── integrations/ # asaas-client, asaas-webhook, neon-auth-config
|
|
39
38
|
├── ui/ # design-system.css
|
|
40
39
|
├── meta-prompts/ # fusion, squad-leaders, validators
|
|
41
|
-
├── examples/ #
|
|
40
|
+
├── examples/ # design-system-examples.md
|
|
42
41
|
└── REGISTRY.json # template metadata — the index every consumer reads
|
|
43
42
|
```
|
|
44
43
|
|