@polymorphism-tech/morph-spec 4.7.1 → 4.7.2
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/.morph/.morphversion +5 -0
- package/.morph/analytics/threads-log.jsonl +5 -0
- package/.morph/config/config.json +8 -0
- package/.morph/framework/agents.json +1815 -0
- package/.morph/framework/hooks/README.md +205 -0
- package/.morph/framework/hooks/claude-code/notification/approval-reminder.js +54 -0
- package/.morph/framework/hooks/claude-code/post-tool-use/dispatch.js +83 -0
- package/.morph/framework/hooks/claude-code/post-tool-use/handle-tool-failure.js +42 -0
- package/.morph/framework/hooks/claude-code/pre-compact/save-morph-context.js +61 -0
- package/.morph/framework/hooks/claude-code/pre-tool-use/enforce-phase-writes.js +71 -0
- package/.morph/framework/hooks/claude-code/pre-tool-use/protect-readonly-files.js +58 -0
- package/.morph/framework/hooks/claude-code/pre-tool-use/protect-spec-files.js +64 -0
- package/.morph/framework/hooks/claude-code/session-start/inject-morph-context.js +94 -0
- package/.morph/framework/hooks/claude-code/statusline.py +538 -0
- package/.morph/framework/hooks/claude-code/statusline.sh +7 -0
- package/.morph/framework/hooks/claude-code/stop/validate-completion.js +88 -0
- package/.morph/framework/hooks/claude-code/user-prompt/enrich-prompt.js +91 -0
- package/.morph/framework/hooks/git/commit-msg/conventional-commits.sh +33 -0
- package/.morph/framework/hooks/git/pre-commit/agents.sh +25 -0
- package/.morph/framework/hooks/git/pre-commit/orchestrator.sh +64 -0
- package/.morph/framework/hooks/git/pre-commit/specs.sh +50 -0
- package/.morph/framework/hooks/git/pre-push/run-tests.sh +44 -0
- package/.morph/framework/hooks/shared/hook-response.js +45 -0
- package/.morph/framework/hooks/shared/phase-utils.js +129 -0
- package/.morph/framework/hooks/shared/state-reader.js +138 -0
- package/.morph/framework/hooks/shared/stdin-reader.js +26 -0
- package/.morph/framework/standards/STANDARDS.json +933 -0
- package/.morph/framework/standards/ai-agents/blazor-ui.md +364 -0
- package/.morph/framework/standards/ai-agents/production.md +415 -0
- package/.morph/framework/standards/ai-agents/setup.md +418 -0
- package/.morph/framework/standards/ai-agents/team-orchestration.md +479 -0
- package/.morph/framework/standards/ai-agents/workflows.md +354 -0
- package/.morph/framework/standards/architecture/ddd/aggregates.md +120 -0
- package/.morph/framework/standards/architecture/ddd/bounded-contexts.md +105 -0
- package/.morph/framework/standards/architecture/ddd/complexity-levels.md +108 -0
- package/.morph/framework/standards/architecture/ddd/entities.md +99 -0
- package/.morph/framework/standards/architecture/ddd/ubiquitous-language.md +58 -0
- package/.morph/framework/standards/architecture/ddd/value-objects.md +124 -0
- package/.morph/framework/standards/backend/api/minimal-api.md +494 -0
- package/.morph/framework/standards/backend/api/rest.md +492 -0
- package/.morph/framework/standards/backend/api/validation.md +88 -0
- package/.morph/framework/standards/backend/authentication/passkeys.md +428 -0
- package/.morph/framework/standards/backend/database/ef-core.md +199 -0
- package/.morph/framework/standards/backend/database/migrations.md +393 -0
- package/.morph/framework/standards/backend/database/postgresql/database.md +352 -0
- package/.morph/framework/standards/backend/database/repository-patterns.md +528 -0
- package/.morph/framework/standards/backend/database/vector-search-rag.md +541 -0
- package/.morph/framework/standards/backend/dotnet/async.md +366 -0
- package/.morph/framework/standards/backend/dotnet/core.md +117 -0
- package/.morph/framework/standards/backend/dotnet/di.md +439 -0
- package/.morph/framework/standards/backend/dotnet/program-cs-checklist.md +92 -0
- package/.morph/framework/standards/backend/integrations/asaas/asaas-api.md +216 -0
- package/.morph/framework/standards/backend/integrations/clerk/clerk-auth.md +290 -0
- package/.morph/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +350 -0
- package/.morph/framework/standards/backend/integrations/resend/resend-email.md +385 -0
- package/.morph/framework/standards/context/analytics.md +96 -0
- package/.morph/framework/standards/context/bundles.md +110 -0
- package/.morph/framework/standards/context/priming.md +78 -0
- package/.morph/framework/standards/core/architecture.md +185 -0
- package/.morph/framework/standards/core/coding.md +214 -0
- package/.morph/framework/standards/core/git-branching-strategy.md +403 -0
- package/.morph/framework/standards/core/git.md +185 -0
- package/.morph/framework/standards/core/testing.md +295 -0
- package/.morph/framework/standards/data/nosql/blob-storage.md +102 -0
- package/.morph/framework/standards/data/nosql/cache/redis.md +97 -0
- package/.morph/framework/standards/data/nosql/cosmos-db.md +118 -0
- package/.morph/framework/standards/data/vector-search/azure-ai-search.md +121 -0
- package/.morph/framework/standards/data/vector-search/rag-chunking.md +104 -0
- package/.morph/framework/standards/frontend/blazor/design-checklist.md +222 -0
- package/.morph/framework/standards/frontend/blazor/fluent-ui-setup.md +595 -0
- package/.morph/framework/standards/frontend/blazor/fluent-ui.md +137 -0
- package/.morph/framework/standards/frontend/blazor/html-conversion.md +184 -0
- package/.morph/framework/standards/frontend/blazor/lifecycle.md +195 -0
- package/.morph/framework/standards/frontend/blazor/pitfalls.md +198 -0
- package/.morph/framework/standards/frontend/blazor/state.md +191 -0
- package/.morph/framework/standards/frontend/design-system/animations.md +151 -0
- package/.morph/framework/standards/frontend/design-system/naming.md +64 -0
- package/.morph/framework/standards/frontend/nextjs/app-router.md +123 -0
- package/.morph/framework/standards/frontend/nextjs/components.md +132 -0
- package/.morph/framework/standards/frontend/nextjs/data-fetching.md +126 -0
- package/.morph/framework/standards/frontend/nextjs/forms.md +128 -0
- package/.morph/framework/standards/frontend/nextjs/naming-conventions.md +67 -0
- package/.morph/framework/standards/frontend/nextjs/nextjs-patterns.md +215 -0
- package/.morph/framework/standards/frontend/nextjs/project-structure.md +102 -0
- package/.morph/framework/standards/frontend/nextjs/state-management.md +72 -0
- package/.morph/framework/standards/frontend/nextjs/testing.md +111 -0
- package/.morph/framework/standards/infrastructure/azure/azure.md +624 -0
- package/.morph/framework/standards/infrastructure/azure/bicep/bicep-patterns.md +422 -0
- package/.morph/framework/standards/infrastructure/azure/devops/azure-devops-setup.md +516 -0
- package/.morph/framework/standards/infrastructure/azure/devops/local-development.md +520 -0
- package/.morph/framework/standards/infrastructure/azure/services/functions.md +486 -0
- package/.morph/framework/standards/infrastructure/azure/services/service-bus.md +459 -0
- package/.morph/framework/standards/infrastructure/azure/services/storage.md +407 -0
- package/.morph/framework/standards/infrastructure/docker/easypanel-deploy.md +196 -0
- package/.morph/framework/standards/infrastructure/supabase/mcp-setup.md +252 -0
- package/.morph/framework/standards/infrastructure/supabase/supabase-auth.md +176 -0
- package/.morph/framework/standards/infrastructure/supabase/supabase-pgvector.md +169 -0
- package/.morph/framework/standards/infrastructure/supabase/supabase-rls.md +184 -0
- package/.morph/framework/standards/infrastructure/supabase/supabase-storage.md +153 -0
- package/.morph/framework/standards/integration/api/graphql.md +91 -0
- package/.morph/framework/standards/integration/api/grpc.md +114 -0
- package/.morph/framework/standards/integration/api/rest-design.md +95 -0
- package/.morph/framework/standards/integration/event-driven/cqrs.md +101 -0
- package/.morph/framework/standards/integration/event-driven/event-sourcing.md +124 -0
- package/.morph/framework/standards/integration/event-driven/service-bus.md +95 -0
- package/.morph/framework/standards/integration/mcp/mcp-tools.md +384 -0
- package/.morph/framework/standards/observability/logging.md +131 -0
- package/.morph/framework/standards/observability/metrics.md +121 -0
- package/.morph/framework/standards/observability/monitoring.md +114 -0
- package/.morph/framework/standards/observability/tracing.md +132 -0
- package/.morph/framework/standards/workflows/parallel-execution.md +112 -0
- package/.morph/framework/standards/workflows/thread-management.md +113 -0
- package/.morph/framework/templates/.idea/morph-templates.xml +92 -0
- package/.morph/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/.morph/framework/templates/IDE-SNIPPETS.md +266 -0
- package/.morph/framework/templates/README.md +814 -0
- package/.morph/framework/templates/REGISTRY.json +1888 -0
- package/.morph/framework/templates/code/dotnet/backend/repository.cs +141 -0
- package/.morph/framework/templates/code/dotnet/backend/service.cs +139 -0
- package/.morph/framework/templates/code/dotnet/contracts/Commands.cs +74 -0
- package/.morph/framework/templates/code/dotnet/contracts/Entities.cs +25 -0
- package/.morph/framework/templates/code/dotnet/contracts/Queries.cs +74 -0
- package/.morph/framework/templates/code/dotnet/contracts/README.md +74 -0
- package/.morph/framework/templates/code/dotnet/contracts/api-contracts.cs +173 -0
- package/.morph/framework/templates/code/dotnet/contracts/contracts-level1.cs +69 -0
- package/.morph/framework/templates/code/dotnet/contracts/contracts-level2.cs +86 -0
- package/.morph/framework/templates/code/dotnet/contracts/contracts-level3.cs +41 -0
- package/.morph/framework/templates/code/dotnet/database/migration.cs +83 -0
- package/.morph/framework/templates/code/dotnet/frontend/component.razor +239 -0
- package/.morph/framework/templates/code/dotnet/jobs/agent.cs +163 -0
- package/.morph/framework/templates/code/dotnet/jobs/job.cs +171 -0
- package/.morph/framework/templates/code/dotnet/test.cs +239 -0
- package/.morph/framework/templates/code/sql/rls-policy.sql +57 -0
- package/.morph/framework/templates/code/sql/supabase-migration.sql +100 -0
- package/.morph/framework/templates/code/sql/supabase-migration.template.sql +113 -0
- package/.morph/framework/templates/code/typescript/contracts.ts +168 -0
- package/.morph/framework/templates/context/CONTEXT-FEATURE.md +276 -0
- package/.morph/framework/templates/context/CONTEXT.md +181 -0
- package/.morph/framework/templates/docs/clarifications.md +253 -0
- package/.morph/framework/templates/docs/onboarding.md +123 -0
- package/.morph/framework/templates/docs/proposal.md +182 -0
- package/.morph/framework/templates/docs/schema-analysis.md +119 -0
- package/.morph/framework/templates/docs/spec.md +198 -0
- package/.morph/framework/templates/docs/ui-components.md +124 -0
- package/.morph/framework/templates/docs/ui-design-system.md +76 -0
- package/.morph/framework/templates/docs/ui-flows.md +167 -0
- package/.morph/framework/templates/docs/ui-mockups.md +98 -0
- package/.morph/framework/templates/docs/user-stories.md +34 -0
- package/.morph/framework/templates/examples/design-system-examples.md +357 -0
- package/.morph/framework/templates/examples/spec-examples.md +90 -0
- package/.morph/framework/templates/feature/decisions.md +187 -0
- package/.morph/framework/templates/feature/recap.md +146 -0
- package/.morph/framework/templates/feature/tasks.md +199 -0
- package/.morph/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -0
- package/.morph/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -0
- package/.morph/framework/templates/frontend/nextjs/env.mjs.hbs +32 -0
- package/.morph/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -0
- package/.morph/framework/templates/frontend/nextjs/page.tsx.hbs +22 -0
- package/.morph/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -0
- package/.morph/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -0
- package/.morph/framework/templates/infrastructure/azure/Dockerfile.example +82 -0
- package/.morph/framework/templates/infrastructure/azure/README.md +286 -0
- package/.morph/framework/templates/infrastructure/azure/app-insights.bicep +63 -0
- package/.morph/framework/templates/infrastructure/azure/app-service.bicep +164 -0
- package/.morph/framework/templates/infrastructure/azure/container-app-env.bicep +49 -0
- package/.morph/framework/templates/infrastructure/azure/container-app.bicep +156 -0
- package/.morph/framework/templates/infrastructure/azure/deploy-checklist.md +426 -0
- package/.morph/framework/templates/infrastructure/azure/deploy.ps1 +229 -0
- package/.morph/framework/templates/infrastructure/azure/deploy.sh +208 -0
- package/.morph/framework/templates/infrastructure/azure/key-vault.bicep +91 -0
- package/.morph/framework/templates/infrastructure/azure/main.bicep +189 -0
- package/.morph/framework/templates/infrastructure/azure/parameters.dev.json +29 -0
- package/.morph/framework/templates/infrastructure/azure/parameters.prod.json +29 -0
- package/.morph/framework/templates/infrastructure/azure/parameters.staging.json +29 -0
- package/.morph/framework/templates/infrastructure/azure/sql-database.bicep +103 -0
- package/.morph/framework/templates/infrastructure/azure/storage.bicep +106 -0
- package/.morph/framework/templates/infrastructure/docker/Dockerfile.template +58 -0
- package/.morph/framework/templates/infrastructure/docker/docker-compose.template.yml +67 -0
- package/.morph/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -0
- package/.morph/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -0
- package/.morph/framework/templates/infrastructure/docker/easypanel.template.json +54 -0
- package/.morph/framework/templates/infrastructure/github/README.md +593 -0
- package/.morph/framework/templates/infrastructure/github/actions/azure-auth/action.yml.hbs +22 -0
- package/.morph/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -0
- package/.morph/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -0
- package/.morph/framework/templates/infrastructure/github/workflows/deploy-azure-app-service.yml.hbs +61 -0
- package/.morph/framework/templates/infrastructure/github/workflows/deploy-easypanel.yml.hbs +31 -0
- package/.morph/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -0
- package/.morph/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -0
- package/.morph/framework/templates/integrations/asaas-client.cs +387 -0
- package/.morph/framework/templates/integrations/asaas-webhook.cs +351 -0
- package/.morph/framework/templates/integrations/azure-identity-config.cs +288 -0
- package/.morph/framework/templates/integrations/clerk-config.cs +258 -0
- package/.morph/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -0
- package/.morph/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -0
- package/.morph/framework/templates/meta-prompts/hops/hop-retry.md +78 -0
- package/.morph/framework/templates/meta-prompts/hops/hop-validation.md +97 -0
- package/.morph/framework/templates/meta-prompts/hops/hop-wrapper.md +36 -0
- package/.morph/framework/templates/meta-prompts/parallel-workers/parallel-coordinator.md +113 -0
- package/.morph/framework/templates/meta-prompts/parallel-workers/parallel-worker.md +80 -0
- package/.morph/framework/templates/meta-prompts/squad-leaders/backend-squad.md +90 -0
- package/.morph/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +126 -0
- package/.morph/framework/templates/meta-prompts/squad-leaders/squad-leader.md +43 -0
- package/.morph/framework/templates/meta-prompts/validators/checkpoint-validator.md +107 -0
- package/.morph/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -0
- package/.morph/framework/templates/project-structure/dotnet-ddd.md +70 -0
- package/.morph/framework/templates/saas/subscription.cs +347 -0
- package/.morph/framework/templates/saas/tenant.cs +338 -0
- package/.morph/framework/templates/state.template.json +17 -0
- package/.morph/framework/templates/ui/FluentDesignTheme.cs +149 -0
- package/.morph/framework/templates/ui/MudTheme.cs +281 -0
- package/.morph/framework/templates/ui/design-system.css +226 -0
- package/.morph/logs/tool-failures.log +17 -0
- package/.morph/memory/pre-compact-2026-02-24T17-43-30-049Z.json +16 -0
- package/.morph/plans/eager-watching-bunny.md +105 -0
- package/.morph/plans/temporal-seeking-nebula.md +45 -0
- package/.morph/state.json +48 -0
- package/CLAUDE.md +1 -1
- package/README.md +2 -2
- package/bin/morph-spec.js +0 -9
- package/framework/CLAUDE.md +1 -1
- package/framework/hooks/README.md +10 -6
- package/framework/hooks/claude-code/notification/approval-reminder.js +2 -0
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +1 -1
- package/framework/hooks/claude-code/stop/validate-completion.js +1 -1
- package/framework/hooks/claude-code/user-prompt/enrich-prompt.js +1 -1
- package/package.json +1 -1
- package/src/commands/project/init.js +15 -42
- package/src/commands/project/update.js +22 -37
- package/src/lib/installers/mcp-installer.js +18 -3
- package/src/utils/hooks-installer.js +5 -15
- package/src/commands/project/detect.js +0 -114
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
# Azure Storage Services Standard
|
|
2
|
+
|
|
3
|
+
> **Scope:** blazor-azure
|
|
4
|
+
> **Layer:** 2 (on keyword)
|
|
5
|
+
> **Keywords:** azure storage, blob, queue, table, file storage
|
|
6
|
+
> **Load When:** azure storage keywords detected
|
|
7
|
+
|
|
8
|
+
Azure Storage services patterns for Blob, Queue, Table, and File storage.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
Azure Storage provides:
|
|
15
|
+
- **Blob Storage**: Object storage for unstructured data (files, images, videos)
|
|
16
|
+
- **Queue Storage**: Message queuing for async processing
|
|
17
|
+
- **Table Storage**: NoSQL key-value store
|
|
18
|
+
- **File Storage**: SMB file shares
|
|
19
|
+
|
|
20
|
+
**Stack:** .NET 10 + Blazor Server
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Core Principles
|
|
25
|
+
|
|
26
|
+
1. **Managed Identity**: Use Managed Identity over connection strings
|
|
27
|
+
2. **SAS Tokens**: Use SAS for temporary access delegation
|
|
28
|
+
3. **Retry Policies**: Configure exponential backoff for transient failures
|
|
29
|
+
4. **Lifecycle Management**: Auto-delete old blobs with lifecycle policies
|
|
30
|
+
5. **Cost Optimization**: Use appropriate storage tiers (Hot/Cool/Archive)
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
dotnet add package Azure.Storage.Blobs
|
|
38
|
+
dotnet add package Azure.Storage.Queues
|
|
39
|
+
dotnet add package Azure.Identity
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Blob Storage
|
|
45
|
+
|
|
46
|
+
### DI Configuration
|
|
47
|
+
|
|
48
|
+
```csharp
|
|
49
|
+
// Program.cs
|
|
50
|
+
using Azure.Identity;
|
|
51
|
+
using Azure.Storage.Blobs;
|
|
52
|
+
|
|
53
|
+
builder.Services.AddSingleton(sp =>
|
|
54
|
+
{
|
|
55
|
+
var blobServiceUri = new Uri(builder.Configuration["AzureStorage:BlobServiceUri"]!);
|
|
56
|
+
return new BlobServiceClient(blobServiceUri, new DefaultAzureCredential());
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Upload Blob
|
|
61
|
+
|
|
62
|
+
```csharp
|
|
63
|
+
public class FileUploadService
|
|
64
|
+
{
|
|
65
|
+
private readonly BlobServiceClient _blobServiceClient;
|
|
66
|
+
|
|
67
|
+
public FileUploadService(BlobServiceClient blobServiceClient)
|
|
68
|
+
{
|
|
69
|
+
_blobServiceClient = blobServiceClient;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public async Task<string> UploadFileAsync(Stream fileStream, string fileName, string containerName)
|
|
73
|
+
{
|
|
74
|
+
var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
|
|
75
|
+
await containerClient.CreateIfNotExistsAsync();
|
|
76
|
+
|
|
77
|
+
var blobClient = containerClient.GetBlobClient(fileName);
|
|
78
|
+
|
|
79
|
+
await blobClient.UploadAsync(fileStream, overwrite: true);
|
|
80
|
+
|
|
81
|
+
return blobClient.Uri.ToString();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Download Blob
|
|
87
|
+
|
|
88
|
+
```csharp
|
|
89
|
+
public async Task<Stream> DownloadFileAsync(string fileName, string containerName)
|
|
90
|
+
{
|
|
91
|
+
var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
|
|
92
|
+
var blobClient = containerClient.GetBlobClient(fileName);
|
|
93
|
+
|
|
94
|
+
var response = await blobClient.DownloadAsync();
|
|
95
|
+
return response.Value.Content;
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Generate SAS Token
|
|
100
|
+
|
|
101
|
+
```csharp
|
|
102
|
+
using Azure.Storage.Sas;
|
|
103
|
+
|
|
104
|
+
public string GenerateSasUri(string containerName, string blobName, TimeSpan validity)
|
|
105
|
+
{
|
|
106
|
+
var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
|
|
107
|
+
var blobClient = containerClient.GetBlobClient(blobName);
|
|
108
|
+
|
|
109
|
+
var sasBuilder = new BlobSasBuilder
|
|
110
|
+
{
|
|
111
|
+
BlobContainerName = containerName,
|
|
112
|
+
BlobName = blobName,
|
|
113
|
+
Resource = "b", // Blob
|
|
114
|
+
StartsOn = DateTimeOffset.UtcNow,
|
|
115
|
+
ExpiresOn = DateTimeOffset.UtcNow.Add(validity)
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
sasBuilder.SetPermissions(BlobSasPermissions.Read);
|
|
119
|
+
|
|
120
|
+
var sasUri = blobClient.GenerateSasUri(sasBuilder);
|
|
121
|
+
return sasUri.ToString();
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Queue Storage
|
|
128
|
+
|
|
129
|
+
### DI Configuration
|
|
130
|
+
|
|
131
|
+
```csharp
|
|
132
|
+
// Program.cs
|
|
133
|
+
builder.Services.AddSingleton(sp =>
|
|
134
|
+
{
|
|
135
|
+
var queueServiceUri = new Uri(builder.Configuration["AzureStorage:QueueServiceUri"]!);
|
|
136
|
+
return new QueueServiceClient(queueServiceUri, new DefaultAzureCredential());
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Send Message
|
|
141
|
+
|
|
142
|
+
```csharp
|
|
143
|
+
public class QueueService
|
|
144
|
+
{
|
|
145
|
+
private readonly QueueServiceClient _queueServiceClient;
|
|
146
|
+
|
|
147
|
+
public QueueService(QueueServiceClient queueServiceClient)
|
|
148
|
+
{
|
|
149
|
+
_queueServiceClient = queueServiceClient;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
public async Task SendMessageAsync(string queueName, string message)
|
|
153
|
+
{
|
|
154
|
+
var queueClient = _queueServiceClient.GetQueueClient(queueName);
|
|
155
|
+
await queueClient.CreateIfNotExistsAsync();
|
|
156
|
+
|
|
157
|
+
await queueClient.SendMessageAsync(message);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
public async Task SendMessageWithDelayAsync(string queueName, string message, TimeSpan delay)
|
|
161
|
+
{
|
|
162
|
+
var queueClient = _queueServiceClient.GetQueueClient(queueName);
|
|
163
|
+
await queueClient.CreateIfNotExistsAsync();
|
|
164
|
+
|
|
165
|
+
await queueClient.SendMessageAsync(message, visibilityTimeout: delay);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Process Messages
|
|
171
|
+
|
|
172
|
+
```csharp
|
|
173
|
+
using Azure.Storage.Queues.Models;
|
|
174
|
+
|
|
175
|
+
public async Task ProcessMessagesAsync(string queueName, CancellationToken cancellationToken)
|
|
176
|
+
{
|
|
177
|
+
var queueClient = _queueServiceClient.GetQueueClient(queueName);
|
|
178
|
+
|
|
179
|
+
while (!cancellationToken.IsCancellationRequested)
|
|
180
|
+
{
|
|
181
|
+
QueueMessage[] messages = await queueClient.ReceiveMessagesAsync(maxMessages: 32);
|
|
182
|
+
|
|
183
|
+
foreach (var message in messages)
|
|
184
|
+
{
|
|
185
|
+
try
|
|
186
|
+
{
|
|
187
|
+
// Process message
|
|
188
|
+
await ProcessMessageAsync(message.MessageText);
|
|
189
|
+
|
|
190
|
+
// Delete message after successful processing
|
|
191
|
+
await queueClient.DeleteMessageAsync(message.MessageId, message.PopReceipt);
|
|
192
|
+
}
|
|
193
|
+
catch (Exception ex)
|
|
194
|
+
{
|
|
195
|
+
// Log error, message will become visible again after timeout
|
|
196
|
+
_logger.LogError(ex, "Failed to process message {MessageId}", message.MessageId);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (messages.Length == 0)
|
|
201
|
+
{
|
|
202
|
+
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Table Storage
|
|
211
|
+
|
|
212
|
+
### DI Configuration
|
|
213
|
+
|
|
214
|
+
```csharp
|
|
215
|
+
dotnet add package Azure.Data.Tables
|
|
216
|
+
|
|
217
|
+
// Program.cs
|
|
218
|
+
builder.Services.AddSingleton(sp =>
|
|
219
|
+
{
|
|
220
|
+
var tableServiceUri = new Uri(builder.Configuration["AzureStorage:TableServiceUri"]!);
|
|
221
|
+
return new TableServiceClient(tableServiceUri, new DefaultAzureCredential());
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Entity Model
|
|
226
|
+
|
|
227
|
+
```csharp
|
|
228
|
+
using Azure;
|
|
229
|
+
using Azure.Data.Tables;
|
|
230
|
+
|
|
231
|
+
public class ProductEntity : ITableEntity
|
|
232
|
+
{
|
|
233
|
+
public string PartitionKey { get; set; } = default!; // Category
|
|
234
|
+
public string RowKey { get; set; } = default!; // ProductId
|
|
235
|
+
public DateTimeOffset? Timestamp { get; set; }
|
|
236
|
+
public ETag ETag { get; set; }
|
|
237
|
+
|
|
238
|
+
public string Name { get; set; } = string.Empty;
|
|
239
|
+
public decimal Price { get; set; }
|
|
240
|
+
public int Stock { get; set; }
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### CRUD Operations
|
|
245
|
+
|
|
246
|
+
```csharp
|
|
247
|
+
public class ProductTableService
|
|
248
|
+
{
|
|
249
|
+
private readonly TableClient _tableClient;
|
|
250
|
+
|
|
251
|
+
public ProductTableService(TableServiceClient tableServiceClient)
|
|
252
|
+
{
|
|
253
|
+
_tableClient = tableServiceClient.GetTableClient("products");
|
|
254
|
+
_tableClient.CreateIfNotExists();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
public async Task AddProductAsync(ProductEntity product)
|
|
258
|
+
{
|
|
259
|
+
await _tableClient.AddEntityAsync(product);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
public async Task<ProductEntity?> GetProductAsync(string category, string productId)
|
|
263
|
+
{
|
|
264
|
+
try
|
|
265
|
+
{
|
|
266
|
+
return await _tableClient.GetEntityAsync<ProductEntity>(category, productId);
|
|
267
|
+
}
|
|
268
|
+
catch (RequestFailedException ex) when (ex.Status == 404)
|
|
269
|
+
{
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
public async Task<List<ProductEntity>> QueryProductsAsync(string category)
|
|
275
|
+
{
|
|
276
|
+
var query = _tableClient.QueryAsync<ProductEntity>(p => p.PartitionKey == category);
|
|
277
|
+
|
|
278
|
+
var results = new List<ProductEntity>();
|
|
279
|
+
await foreach (var product in query)
|
|
280
|
+
{
|
|
281
|
+
results.Add(product);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return results;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
public async Task UpdateProductAsync(ProductEntity product)
|
|
288
|
+
{
|
|
289
|
+
await _tableClient.UpdateEntityAsync(product, ETag.All, TableUpdateMode.Replace);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
public async Task DeleteProductAsync(string category, string productId)
|
|
293
|
+
{
|
|
294
|
+
await _tableClient.DeleteEntityAsync(category, productId);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Best Practices
|
|
302
|
+
|
|
303
|
+
### Retry Policy
|
|
304
|
+
|
|
305
|
+
```csharp
|
|
306
|
+
using Azure.Core;
|
|
307
|
+
|
|
308
|
+
var options = new BlobClientOptions
|
|
309
|
+
{
|
|
310
|
+
Retry =
|
|
311
|
+
{
|
|
312
|
+
Mode = RetryMode.Exponential,
|
|
313
|
+
MaxRetries = 3,
|
|
314
|
+
Delay = TimeSpan.FromSeconds(2),
|
|
315
|
+
MaxDelay = TimeSpan.FromSeconds(10)
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
var blobServiceClient = new BlobServiceClient(blobServiceUri, credential, options);
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Blob Metadata
|
|
323
|
+
|
|
324
|
+
```csharp
|
|
325
|
+
var metadata = new Dictionary<string, string>
|
|
326
|
+
{
|
|
327
|
+
{ "uploadedBy", userId },
|
|
328
|
+
{ "fileType", "invoice" },
|
|
329
|
+
{ "department", "finance" }
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
await blobClient.UploadAsync(stream, metadata: metadata);
|
|
333
|
+
|
|
334
|
+
// Retrieve metadata
|
|
335
|
+
var properties = await blobClient.GetPropertiesAsync();
|
|
336
|
+
var uploadedBy = properties.Value.Metadata["uploadedBy"];
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Lifecycle Management
|
|
340
|
+
|
|
341
|
+
```json
|
|
342
|
+
// Configure in Azure Portal or via Bicep
|
|
343
|
+
{
|
|
344
|
+
"rules": [
|
|
345
|
+
{
|
|
346
|
+
"name": "deleteOldBackups",
|
|
347
|
+
"enabled": true,
|
|
348
|
+
"type": "Lifecycle",
|
|
349
|
+
"definition": {
|
|
350
|
+
"filters": {
|
|
351
|
+
"blobTypes": ["blockBlob"],
|
|
352
|
+
"prefixMatch": ["backups/"]
|
|
353
|
+
},
|
|
354
|
+
"actions": {
|
|
355
|
+
"baseBlob": {
|
|
356
|
+
"delete": {
|
|
357
|
+
"daysAfterModificationGreaterThan": 30
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
]
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
## Bicep Configuration
|
|
370
|
+
|
|
371
|
+
```bicep
|
|
372
|
+
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
|
|
373
|
+
name: storageAccountName
|
|
374
|
+
location: location
|
|
375
|
+
sku: {
|
|
376
|
+
name: 'Standard_LRS'
|
|
377
|
+
}
|
|
378
|
+
kind: 'StorageV2'
|
|
379
|
+
properties: {
|
|
380
|
+
allowBlobPublicAccess: false
|
|
381
|
+
minimumTlsVersion: 'TLS1_2'
|
|
382
|
+
supportsHttpsTrafficOnly: true
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Grant Container App Blob access
|
|
387
|
+
resource blobRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
|
|
388
|
+
scope: storageAccount
|
|
389
|
+
name: guid(storageAccount.id, principalId, 'Storage Blob Data Contributor')
|
|
390
|
+
properties: {
|
|
391
|
+
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
|
|
392
|
+
principalId: principalId
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
## References
|
|
400
|
+
|
|
401
|
+
- [Azure Blob Storage](https://learn.microsoft.com/azure/storage/blobs/)
|
|
402
|
+
- [Azure Queue Storage](https://learn.microsoft.com/azure/storage/queues/)
|
|
403
|
+
- [Azure Table Storage](https://learn.microsoft.com/azure/storage/tables/)
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
*MORPH-SPEC by Polymorphism Tech*
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# EasyPanel Deployment Standard
|
|
2
|
+
|
|
3
|
+
> **Scope:** nextjs-supabase
|
|
4
|
+
> **Layer:** 2 (on keyword)
|
|
5
|
+
> **Keywords:** easypanel, docker, deploy, deployment, container
|
|
6
|
+
> **Load When:** easypanel or docker deployment keywords detected
|
|
7
|
+
|
|
8
|
+
Stack: Next.js 15 + Supabase + .NET Backend
|
|
9
|
+
|
|
10
|
+
## Core Rules
|
|
11
|
+
|
|
12
|
+
- ALWAYS use multi-stage Docker builds for minimal image size
|
|
13
|
+
- ALWAYS configure health checks for zero-downtime deploys
|
|
14
|
+
- NEVER hardcode secrets in Dockerfiles -- use EasyPanel environment variables
|
|
15
|
+
- ALWAYS use `.dockerignore` to exclude node_modules, .git, .env files
|
|
16
|
+
- SSL is automatic via Let's Encrypt -- no manual certificate management
|
|
17
|
+
|
|
18
|
+
## .NET 10 Dockerfile
|
|
19
|
+
|
|
20
|
+
```dockerfile
|
|
21
|
+
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
|
22
|
+
WORKDIR /src
|
|
23
|
+
COPY *.csproj .
|
|
24
|
+
RUN dotnet restore
|
|
25
|
+
COPY . .
|
|
26
|
+
RUN dotnet publish -c Release -o /app/publish --no-restore
|
|
27
|
+
|
|
28
|
+
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
|
|
29
|
+
WORKDIR /app
|
|
30
|
+
RUN adduser --disabled-password --gecos "" appuser
|
|
31
|
+
USER appuser
|
|
32
|
+
COPY --from=build /app/publish .
|
|
33
|
+
ENV ASPNETCORE_URLS=http://+:8080
|
|
34
|
+
EXPOSE 8080
|
|
35
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
|
36
|
+
CMD curl -f http://localhost:8080/health || exit 1
|
|
37
|
+
ENTRYPOINT ["dotnet", "MyApp.Api.dll"]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Next.js Standalone Dockerfile
|
|
41
|
+
|
|
42
|
+
```dockerfile
|
|
43
|
+
FROM node:22-alpine AS base
|
|
44
|
+
|
|
45
|
+
FROM base AS builder
|
|
46
|
+
WORKDIR /app
|
|
47
|
+
COPY package.json package-lock.json ./
|
|
48
|
+
RUN npm ci
|
|
49
|
+
COPY . .
|
|
50
|
+
ENV NEXT_TELEMETRY_DISABLED=1
|
|
51
|
+
RUN npm run build
|
|
52
|
+
|
|
53
|
+
FROM base AS runner
|
|
54
|
+
WORKDIR /app
|
|
55
|
+
ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1
|
|
56
|
+
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
|
|
57
|
+
COPY --from=builder /app/public ./public
|
|
58
|
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
|
59
|
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
|
60
|
+
USER nextjs
|
|
61
|
+
EXPOSE 3000
|
|
62
|
+
ENV PORT=3000 HOSTNAME="0.0.0.0"
|
|
63
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
|
64
|
+
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1
|
|
65
|
+
CMD ["node", "server.js"]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Required: `output: "standalone"` in `next.config.ts`.
|
|
69
|
+
|
|
70
|
+
## .dockerignore
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
node_modules
|
|
74
|
+
.next
|
|
75
|
+
.git
|
|
76
|
+
.env*
|
|
77
|
+
*.md
|
|
78
|
+
.vscode
|
|
79
|
+
coverage
|
|
80
|
+
test
|
|
81
|
+
__tests__
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## EasyPanel Service Config
|
|
85
|
+
|
|
86
|
+
| Setting | .NET Backend | Next.js Frontend |
|
|
87
|
+
|---------|-------------|-----------------|
|
|
88
|
+
| Source | GitHub | GitHub |
|
|
89
|
+
| Build method | Dockerfile | Dockerfile |
|
|
90
|
+
| Dockerfile path | `./backend/Dockerfile` | `./frontend/Dockerfile` |
|
|
91
|
+
| Port | 8080 | 3000 |
|
|
92
|
+
| Domain | api.example.com | app.example.com |
|
|
93
|
+
|
|
94
|
+
### GitHub Integration
|
|
95
|
+
|
|
96
|
+
1. Generate GitHub PAT with `repo` scope
|
|
97
|
+
2. EasyPanel: Settings > GitHub > Add token
|
|
98
|
+
3. Select repository and branch
|
|
99
|
+
4. Enable "Auto Deploy" for webhook-triggered deploys
|
|
100
|
+
|
|
101
|
+
### Domain and SSL
|
|
102
|
+
|
|
103
|
+
DNS setup (CNAME to EasyPanel server):
|
|
104
|
+
```
|
|
105
|
+
app.example.com CNAME your-server.easypanel.host
|
|
106
|
+
api.example.com CNAME your-server.easypanel.host
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
SSL via Let's Encrypt is automatic. Force HTTPS enabled by default.
|
|
110
|
+
|
|
111
|
+
## Environment Variables
|
|
112
|
+
|
|
113
|
+
### .NET Backend
|
|
114
|
+
|
|
115
|
+
```env
|
|
116
|
+
ASPNETCORE_ENVIRONMENT=Production
|
|
117
|
+
ASPNETCORE_URLS=http://+:8080
|
|
118
|
+
ConnectionStrings__DefaultConnection=Host=...;Database=...;Username=...;Password=...
|
|
119
|
+
Supabase__Url=https://xxx.supabase.co
|
|
120
|
+
Supabase__ServiceRoleKey=eyJ...
|
|
121
|
+
Supabase__JwtSecret=your-jwt-secret
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Next.js Frontend
|
|
125
|
+
|
|
126
|
+
```env
|
|
127
|
+
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
|
|
128
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
|
|
129
|
+
NEXT_PUBLIC_API_URL=https://api.example.com
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Health Check Endpoints
|
|
133
|
+
|
|
134
|
+
```csharp
|
|
135
|
+
// .NET: Program.cs
|
|
136
|
+
builder.Services.AddHealthChecks()
|
|
137
|
+
.AddNpgSql(connectionString, name: "database");
|
|
138
|
+
app.MapHealthChecks("/health");
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
// Next.js: app/api/health/route.ts
|
|
143
|
+
export async function GET() {
|
|
144
|
+
return Response.json({ status: "healthy", timestamp: new Date().toISOString() });
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Zero-Downtime Deploys
|
|
149
|
+
|
|
150
|
+
EasyPanel uses Docker HEALTHCHECK to determine container readiness:
|
|
151
|
+
|
|
152
|
+
1. New container starts alongside old container
|
|
153
|
+
2. Health check passes after start-period + retries
|
|
154
|
+
3. Traffic shifts to new container
|
|
155
|
+
4. Old container stopped
|
|
156
|
+
|
|
157
|
+
| HEALTHCHECK Param | Value | Purpose |
|
|
158
|
+
|-------------------|-------|---------|
|
|
159
|
+
| `--interval` | 30s | Time between checks |
|
|
160
|
+
| `--timeout` | 5s | Max response wait |
|
|
161
|
+
| `--start-period` | 10s | Startup grace period |
|
|
162
|
+
| `--retries` | 3 | Failures before unhealthy |
|
|
163
|
+
|
|
164
|
+
## Monitoring
|
|
165
|
+
|
|
166
|
+
- **Logs**: Real-time in EasyPanel UI
|
|
167
|
+
- **Metrics**: CPU, memory, network via dashboard
|
|
168
|
+
- **Restart**: Auto-restart on crash (default)
|
|
169
|
+
|
|
170
|
+
Structured logging:
|
|
171
|
+
|
|
172
|
+
```csharp
|
|
173
|
+
// .NET: Serilog with JSON output
|
|
174
|
+
builder.Host.UseSerilog((ctx, cfg) => cfg
|
|
175
|
+
.ReadFrom.Configuration(ctx.Configuration)
|
|
176
|
+
.WriteTo.Console(new JsonFormatter()));
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
// Next.js: pino
|
|
181
|
+
import pino from "pino";
|
|
182
|
+
const logger = pino({ level: process.env.LOG_LEVEL ?? "info" });
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Deployment Checklist
|
|
186
|
+
|
|
187
|
+
| Step | Action |
|
|
188
|
+
|------|--------|
|
|
189
|
+
| 1 | Verify `output: "standalone"` in next.config.ts |
|
|
190
|
+
| 2 | Test Docker build locally |
|
|
191
|
+
| 3 | Test health endpoint |
|
|
192
|
+
| 4 | Configure env vars in EasyPanel |
|
|
193
|
+
| 5 | Set up custom domain + DNS |
|
|
194
|
+
| 6 | Verify SSL certificate |
|
|
195
|
+
| 7 | Enable auto-deploy from GitHub |
|
|
196
|
+
| 8 | Push to main, verify deployment |
|