@polymorphism-tech/morph-spec 4.2.0 → 4.3.1
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 +108 -946
- package/bin/morph-spec.js +284 -9
- package/bin/task-manager.cjs +102 -14
- package/bin/validate.js +4 -4
- package/docs/{v3.0 → next-generation}/AGENTS.md +1 -1
- package/docs/next-generation/CONTEXT-OPTIMIZATION.md +267 -0
- package/docs/next-generation/EXECUTION-FLOW.md +274 -0
- package/docs/next-generation/META-PROMPTS.md +235 -0
- package/docs/next-generation/MIGRATION-GUIDE.md +253 -0
- package/docs/next-generation/THREAD-MANAGEMENT.md +240 -0
- package/package.json +5 -5
- package/src/commands/agents/agents-fuse.js +97 -0
- package/src/commands/agents/micro-agent.js +112 -0
- package/src/commands/agents/spawn-team.js +69 -4
- package/src/commands/agents/squad-template.js +146 -0
- package/src/commands/analytics/analytics.js +176 -0
- package/src/commands/context/context-prime.js +63 -0
- package/src/commands/context/core-four.js +54 -0
- package/src/commands/mcp/mcp.js +102 -0
- package/src/commands/project/detect-agents.js +32 -2
- package/src/commands/project/detect.js +11 -1
- package/src/commands/project/doctor.js +573 -356
- package/src/commands/project/init.js +9 -2
- package/src/commands/project/update.js +13 -3
- package/src/commands/state/advance-phase.js +448 -416
- package/src/commands/state/state.js +14 -12
- package/src/commands/tasks/task.js +1 -1
- package/src/commands/templates/template-render.js +80 -1
- package/src/commands/threads/thread-template.js +103 -0
- package/src/commands/threads/threads.js +261 -0
- package/src/commands/trust/trust.js +205 -0
- package/src/{orchestrator.js → core/orchestrator.js} +8 -8
- package/src/core/state/state-manager.js +37 -17
- package/src/core/workflows/workflow-detector.js +114 -3
- package/src/lib/agents/micro-agent-factory.js +161 -0
- package/src/lib/analytics/analytics-engine.js +345 -0
- package/src/lib/checkpoints/checkpoint-hooks.js +298 -258
- package/src/lib/context/context-bundler.js +240 -0
- package/src/lib/context/context-optimizer.js +212 -0
- package/src/lib/context/context-tracker.js +273 -0
- package/src/lib/context/core-four-tracker.js +201 -0
- package/src/lib/context/mcp-optimizer.js +200 -0
- package/src/lib/detectors/index.js +1 -1
- package/src/lib/detectors/standards-generator.js +77 -17
- package/src/lib/detectors/structure-detector.js +67 -39
- package/src/lib/execution/fusion-executor.js +304 -0
- package/src/lib/execution/parallel-executor.js +270 -0
- package/src/lib/generators/context-generator.js +3 -3
- package/src/lib/generators/recap-generator.js +32 -12
- package/src/lib/hooks/hook-executor.js +169 -0
- package/src/lib/hooks/stop-hook-executor.js +286 -0
- package/src/lib/hops/hop-composer.js +221 -0
- package/src/lib/threads/thread-coordinator.js +238 -0
- package/src/lib/threads/thread-manager.js +317 -0
- package/src/lib/tracking/artifact-trail.js +202 -0
- package/src/lib/trust/trust-manager.js +269 -0
- package/src/lib/validators/design-system/design-system-validator.js +2 -2
- package/src/lib/validators/validation-runner.js +14 -30
- package/src/utils/hooks-installer.js +69 -0
- package/stacks/blazor-azure/.morph/config/agents.json +72 -3
- package/stacks/nextjs-supabase/.morph/config/agents.json +3 -3
- package/docs/llm-interaction-config.md +0 -735
- package/docs/v3.0/EXECUTION-FLOW.md +0 -1304
- package/src/commands/utils/migrate-state.js +0 -158
- package/src/commands/utils/upgrade.js +0 -346
- package/src/lib/validators/architecture-validator.js +0 -60
- package/src/lib/validators/content-validator.js +0 -164
- package/src/lib/validators/package-validator.js +0 -61
- package/src/lib/validators/ui-contrast-validator.js +0 -44
- package/stacks/blazor-azure/.claude/commands/morph-apply.md +0 -221
- package/stacks/blazor-azure/.claude/commands/morph-archive.md +0 -79
- package/stacks/blazor-azure/.claude/commands/morph-deploy.md +0 -529
- package/stacks/blazor-azure/.claude/commands/morph-infra.md +0 -209
- package/stacks/blazor-azure/.claude/commands/morph-preflight.md +0 -227
- package/stacks/blazor-azure/.claude/commands/morph-proposal.md +0 -122
- package/stacks/blazor-azure/.claude/commands/morph-status.md +0 -86
- package/stacks/blazor-azure/.claude/commands/morph-troubleshoot.md +0 -122
- package/stacks/blazor-azure/.claude/skills/level-0-meta/README.md +0 -7
- package/stacks/blazor-azure/.claude/skills/level-0-meta/code-review.md +0 -226
- package/stacks/blazor-azure/.claude/skills/level-0-meta/morph-checklist.md +0 -117
- package/stacks/blazor-azure/.claude/skills/level-0-meta/simulation-checklist.md +0 -77
- package/stacks/blazor-azure/.claude/skills/level-1-workflows/README.md +0 -7
- package/stacks/blazor-azure/.claude/skills/level-1-workflows/morph-replicate.md +0 -213
- package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-clarify.md +0 -131
- package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-design.md +0 -213
- package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-setup.md +0 -106
- package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-tasks.md +0 -164
- package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-uiux.md +0 -169
- package/stacks/blazor-azure/.claude/skills/level-2-domains/README.md +0 -14
- package/stacks/blazor-azure/.claude/skills/level-2-domains/ai-agents/ai-system-architect.md +0 -192
- package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/po-pm-advisor.md +0 -197
- package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/prompt-engineer.md +0 -189
- package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/seo-growth-hacker.md +0 -320
- package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/standards-architect.md +0 -156
- package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/api-designer.md +0 -59
- package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/dotnet-senior.md +0 -77
- package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/ef-modeler.md +0 -58
- package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/hangfire-orchestrator.md +0 -126
- package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/ms-agent-expert.md +0 -45
- package/stacks/blazor-azure/.claude/skills/level-2-domains/frontend/blazor-builder.md +0 -210
- package/stacks/blazor-azure/.claude/skills/level-2-domains/frontend/nextjs-expert.md +0 -154
- package/stacks/blazor-azure/.claude/skills/level-2-domains/frontend/ui-ux-designer.md +0 -191
- package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/azure-architect.md +0 -142
- package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/azure-deploy-specialist.md +0 -699
- package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/bicep-architect.md +0 -126
- package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/container-specialist.md +0 -131
- package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/devops-engineer.md +0 -119
- package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/asaas-financial.md +0 -130
- package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/azure-identity.md +0 -142
- package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/clerk-auth.md +0 -108
- package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/hangfire-orchestrator.md +0 -64
- package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/resend-email.md +0 -119
- package/stacks/blazor-azure/.claude/skills/level-2-domains/quality/code-analyzer.md +0 -235
- package/stacks/blazor-azure/.claude/skills/level-2-domains/quality/testing-specialist.md +0 -126
- package/stacks/blazor-azure/.claude/skills/level-3-technologies/README.md +0 -7
- package/stacks/blazor-azure/.claude/skills/level-4-patterns/README.md +0 -7
- package/stacks/blazor-azure/.morph/archive/.gitkeep +0 -25
- package/stacks/blazor-azure/.morph/features/.gitkeep +0 -25
- package/stacks/blazor-azure/.morph/schemas/agent.schema.json +0 -296
- package/stacks/blazor-azure/.morph/schemas/tasks.schema.json +0 -220
- package/stacks/blazor-azure/.morph/specs/.gitkeep +0 -20
- package/stacks/blazor-azure/.morph/test-infra/example.bicep +0 -59
- package/stacks/nextjs-supabase/.claude/commands/morph-apply.md +0 -221
- package/stacks/nextjs-supabase/.claude/commands/morph-archive.md +0 -79
- package/stacks/nextjs-supabase/.claude/commands/morph-deploy.md +0 -529
- package/stacks/nextjs-supabase/.claude/commands/morph-infra.md +0 -209
- package/stacks/nextjs-supabase/.claude/commands/morph-preflight.md +0 -227
- package/stacks/nextjs-supabase/.claude/commands/morph-proposal.md +0 -122
- package/stacks/nextjs-supabase/.claude/commands/morph-status.md +0 -86
- package/stacks/nextjs-supabase/.claude/commands/morph-troubleshoot.md +0 -122
- package/stacks/nextjs-supabase/.claude/settings.local.json +0 -6
- package/stacks/nextjs-supabase/.claude/skills/level-2-domains/backend/dotnet-supabase.md +0 -244
- package/stacks/nextjs-supabase/.claude/skills/level-2-domains/frontend/nextjs-supabase.md +0 -335
- package/stacks/nextjs-supabase/.claude/skills/level-2-domains/infrastructure/easypanel-deployer.md +0 -189
- package/stacks/nextjs-supabase/.claude/skills/level-2-domains/integrations/supabase-expert.md +0 -50
- /package/docs/{v3.0 → next-generation}/ANALYSIS.md +0 -0
- /package/docs/{v3.0 → next-generation}/ARCHITECTURE.md +0 -0
- /package/docs/{v3.0 → next-generation}/FEATURES.md +0 -0
- /package/docs/{v3.0 → next-generation}/README.md +0 -0
- /package/docs/{v3.0 → next-generation}/ROADMAP.md +0 -0
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
# .NET + Supabase Backend
|
|
2
|
-
|
|
3
|
-
> **Layer:** 2 | **Load:** on-keyword | **Keywords:** dotnet, supabase, npgsql, dapper, jwt, minimal-api, postgresql
|
|
4
|
-
|
|
5
|
-
## Identity
|
|
6
|
-
|
|
7
|
-
Backend specialist for .NET Minimal API with Supabase (PostgreSQL). Uses Npgsql for connections, Dapper for data access with records, JWT middleware for Supabase Auth token validation, and typed Minimal API endpoints. No EF Core -- direct SQL with Dapper for maximum control over Supabase's PostgreSQL.
|
|
8
|
-
|
|
9
|
-
## Domains
|
|
10
|
-
|
|
11
|
-
- data-access
|
|
12
|
-
- auth-middleware
|
|
13
|
-
- api-endpoints
|
|
14
|
-
|
|
15
|
-
## Standards
|
|
16
|
-
|
|
17
|
-
- coding.md -- C# naming, sealed classes, CancellationToken, Result pattern
|
|
18
|
-
- architecture.md -- Layer boundaries (API -> Application -> Domain)
|
|
19
|
-
|
|
20
|
-
## Patterns
|
|
21
|
-
|
|
22
|
-
### Npgsql Connection Setup
|
|
23
|
-
|
|
24
|
-
```csharp
|
|
25
|
-
// Program.cs
|
|
26
|
-
builder.Services.AddSingleton<NpgsqlDataSource>(sp =>
|
|
27
|
-
{
|
|
28
|
-
var connectionString = builder.Configuration.GetConnectionString("Supabase")
|
|
29
|
-
?? throw new InvalidOperationException("Supabase connection string not configured");
|
|
30
|
-
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);
|
|
31
|
-
dataSourceBuilder.UseVector(); // pgvector support
|
|
32
|
-
return dataSourceBuilder.Build();
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
// appsettings.json
|
|
36
|
-
{
|
|
37
|
-
"ConnectionStrings": {
|
|
38
|
-
"Supabase": "Host=db.xxx.supabase.co;Port=5432;Database=postgres;Username=postgres;Password=${DB_PASSWORD};SSL Mode=Require;Trust Server Certificate=true"
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
### Dapper Queries with Records
|
|
44
|
-
|
|
45
|
-
```csharp
|
|
46
|
-
namespace MyApp.Infrastructure.Repositories;
|
|
47
|
-
|
|
48
|
-
public sealed class DocumentRepository(NpgsqlDataSource dataSource) : IDocumentRepository
|
|
49
|
-
{
|
|
50
|
-
public async Task<DocumentDto?> GetByIdAsync(Guid id, CancellationToken ct = default)
|
|
51
|
-
{
|
|
52
|
-
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
|
53
|
-
return await connection.QueryFirstOrDefaultAsync<DocumentDto>(
|
|
54
|
-
"""
|
|
55
|
-
SELECT id, title, content, user_id AS UserId, created_at AS CreatedAt
|
|
56
|
-
FROM documents
|
|
57
|
-
WHERE id = @Id
|
|
58
|
-
""",
|
|
59
|
-
new { Id = id });
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
public async Task<PagedResult<DocumentDto>> GetPagedAsync(
|
|
63
|
-
PaginationQuery query,
|
|
64
|
-
Guid userId,
|
|
65
|
-
CancellationToken ct = default)
|
|
66
|
-
{
|
|
67
|
-
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
|
68
|
-
|
|
69
|
-
var count = await connection.ExecuteScalarAsync<int>(
|
|
70
|
-
"SELECT count(*) FROM documents WHERE user_id = @UserId",
|
|
71
|
-
new { UserId = userId });
|
|
72
|
-
|
|
73
|
-
var items = await connection.QueryAsync<DocumentDto>(
|
|
74
|
-
"""
|
|
75
|
-
SELECT id, title, content, user_id AS UserId, created_at AS CreatedAt
|
|
76
|
-
FROM documents
|
|
77
|
-
WHERE user_id = @UserId
|
|
78
|
-
ORDER BY created_at DESC
|
|
79
|
-
LIMIT @PageSize OFFSET @Offset
|
|
80
|
-
""",
|
|
81
|
-
new { UserId = userId, query.PageSize, Offset = (query.Page - 1) * query.PageSize });
|
|
82
|
-
|
|
83
|
-
return new PagedResult<DocumentDto>(items.ToList(), count, query.Page, query.PageSize);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
public async Task<Guid> CreateAsync(CreateDocumentRequest request, Guid userId, CancellationToken ct = default)
|
|
87
|
-
{
|
|
88
|
-
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
|
89
|
-
return await connection.ExecuteScalarAsync<Guid>(
|
|
90
|
-
"""
|
|
91
|
-
INSERT INTO documents (title, content, user_id)
|
|
92
|
-
VALUES (@Title, @Content, @UserId)
|
|
93
|
-
RETURNING id
|
|
94
|
-
""",
|
|
95
|
-
new { request.Title, request.Content, UserId = userId });
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
### JWT Middleware Setup (Supabase Auth)
|
|
101
|
-
|
|
102
|
-
```csharp
|
|
103
|
-
// Program.cs
|
|
104
|
-
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
|
105
|
-
.AddJwtBearer(options =>
|
|
106
|
-
{
|
|
107
|
-
var supabaseUrl = builder.Configuration["Supabase:Url"]
|
|
108
|
-
?? throw new InvalidOperationException("Supabase:Url not configured");
|
|
109
|
-
var jwtSecret = builder.Configuration["Supabase:JwtSecret"]
|
|
110
|
-
?? throw new InvalidOperationException("Supabase:JwtSecret not configured");
|
|
111
|
-
|
|
112
|
-
options.TokenValidationParameters = new TokenValidationParameters
|
|
113
|
-
{
|
|
114
|
-
ValidateIssuer = true,
|
|
115
|
-
ValidIssuer = $"{supabaseUrl}/auth/v1",
|
|
116
|
-
ValidateAudience = true,
|
|
117
|
-
ValidAudience = "authenticated",
|
|
118
|
-
ValidateIssuerSigningKey = true,
|
|
119
|
-
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret)),
|
|
120
|
-
ValidateLifetime = true,
|
|
121
|
-
ClockSkew = TimeSpan.FromSeconds(30)
|
|
122
|
-
};
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
builder.Services.AddAuthorization();
|
|
126
|
-
app.UseAuthentication();
|
|
127
|
-
app.UseAuthorization();
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
### User ID Extraction
|
|
131
|
-
|
|
132
|
-
```csharp
|
|
133
|
-
// Extension method for ClaimsPrincipal
|
|
134
|
-
public static class ClaimsPrincipalExtensions
|
|
135
|
-
{
|
|
136
|
-
public static Guid GetUserId(this ClaimsPrincipal user)
|
|
137
|
-
{
|
|
138
|
-
var sub = user.FindFirstValue(ClaimTypes.NameIdentifier)
|
|
139
|
-
?? throw new UnauthorizedAccessException("User ID claim not found");
|
|
140
|
-
return Guid.Parse(sub);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
### Minimal API Endpoints
|
|
146
|
-
|
|
147
|
-
```csharp
|
|
148
|
-
namespace MyApp.Api.Endpoints;
|
|
149
|
-
|
|
150
|
-
public static class DocumentEndpoints
|
|
151
|
-
{
|
|
152
|
-
public static void MapDocumentEndpoints(this WebApplication app)
|
|
153
|
-
{
|
|
154
|
-
var group = app.MapGroup("/api/documents")
|
|
155
|
-
.WithTags("Documents")
|
|
156
|
-
.RequireAuthorization();
|
|
157
|
-
|
|
158
|
-
group.MapGet("/", GetAllAsync);
|
|
159
|
-
group.MapGet("/{id:guid}", GetByIdAsync);
|
|
160
|
-
group.MapPost("/", CreateAsync);
|
|
161
|
-
group.MapPut("/{id:guid}", UpdateAsync);
|
|
162
|
-
group.MapDelete("/{id:guid}", DeleteAsync);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
private static async Task<IResult> GetAllAsync(
|
|
166
|
-
[AsParameters] PaginationQuery query,
|
|
167
|
-
ClaimsPrincipal user,
|
|
168
|
-
IDocumentService service,
|
|
169
|
-
CancellationToken ct)
|
|
170
|
-
{
|
|
171
|
-
var userId = user.GetUserId();
|
|
172
|
-
var result = await service.GetPagedAsync(query, userId, ct);
|
|
173
|
-
return Results.Ok(result);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
private static async Task<IResult> CreateAsync(
|
|
177
|
-
CreateDocumentRequest request,
|
|
178
|
-
ClaimsPrincipal user,
|
|
179
|
-
IDocumentService service,
|
|
180
|
-
CancellationToken ct)
|
|
181
|
-
{
|
|
182
|
-
var userId = user.GetUserId();
|
|
183
|
-
var result = await service.CreateAsync(request, userId, ct);
|
|
184
|
-
return result.IsSuccess
|
|
185
|
-
? Results.Created($"/api/documents/{result.Value.Id}", result.Value)
|
|
186
|
-
: Results.BadRequest(result.Error);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
### Supabase Client Wrapper (for Storage/Auth Admin)
|
|
192
|
-
|
|
193
|
-
```csharp
|
|
194
|
-
namespace MyApp.Infrastructure.Supabase;
|
|
195
|
-
|
|
196
|
-
public sealed class SupabaseAdmin(HttpClient httpClient, IOptions<SupabaseOptions> options)
|
|
197
|
-
{
|
|
198
|
-
private readonly string _serviceKey = options.Value.ServiceRoleKey;
|
|
199
|
-
|
|
200
|
-
public async Task<string> UploadFileAsync(
|
|
201
|
-
string bucket,
|
|
202
|
-
string path,
|
|
203
|
-
Stream content,
|
|
204
|
-
string contentType,
|
|
205
|
-
CancellationToken ct = default)
|
|
206
|
-
{
|
|
207
|
-
using var request = new HttpRequestMessage(HttpMethod.Post,
|
|
208
|
-
$"{options.Value.Url}/storage/v1/object/{bucket}/{path}");
|
|
209
|
-
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _serviceKey);
|
|
210
|
-
request.Headers.Add("apikey", _serviceKey);
|
|
211
|
-
request.Content = new StreamContent(content);
|
|
212
|
-
request.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType);
|
|
213
|
-
|
|
214
|
-
var response = await httpClient.SendAsync(request, ct);
|
|
215
|
-
response.EnsureSuccessStatusCode();
|
|
216
|
-
|
|
217
|
-
var result = await response.Content.ReadFromJsonAsync<StorageUploadResult>(ct);
|
|
218
|
-
return $"{options.Value.Url}/storage/v1/object/public/{bucket}/{result?.Key}";
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
public sealed record SupabaseOptions
|
|
223
|
-
{
|
|
224
|
-
public required string Url { get; init; }
|
|
225
|
-
public required string AnonKey { get; init; }
|
|
226
|
-
public required string ServiceRoleKey { get; init; }
|
|
227
|
-
public required string JwtSecret { get; init; }
|
|
228
|
-
}
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
## Checklist
|
|
232
|
-
|
|
233
|
-
- [ ] NpgsqlDataSource registered as Singleton (connection pooling)
|
|
234
|
-
- [ ] Dapper queries use parameterized SQL (never string interpolation)
|
|
235
|
-
- [ ] JWT middleware validates issuer, audience, signing key, and lifetime
|
|
236
|
-
- [ ] service_role key stored in environment variables (not appsettings)
|
|
237
|
-
- [ ] All async methods accept CancellationToken
|
|
238
|
-
- [ ] All DTOs are records (not classes)
|
|
239
|
-
- [ ] Endpoints use RequireAuthorization()
|
|
240
|
-
- [ ] User ID extracted from JWT claims (not request body)
|
|
241
|
-
|
|
242
|
-
---
|
|
243
|
-
|
|
244
|
-
*MORPH-SPEC by Polymorphism Tech*
|
|
@@ -1,335 +0,0 @@
|
|
|
1
|
-
# Next.js + Supabase Frontend
|
|
2
|
-
|
|
3
|
-
> **Layer:** 2 | **Load:** on-keyword | **Keywords:** nextjs, supabase, ssr, auth, react-query, shadcn, tailwind, middleware
|
|
4
|
-
|
|
5
|
-
## Identity
|
|
6
|
-
|
|
7
|
-
Frontend specialist for Next.js App Router with Supabase. Implements @supabase/ssr for server and browser clients, auth middleware for route protection, React Query for data fetching with Supabase, shadcn/ui components with Tailwind CSS, and Realtime subscriptions.
|
|
8
|
-
|
|
9
|
-
## Domains
|
|
10
|
-
|
|
11
|
-
- auth-ui
|
|
12
|
-
- data-fetching
|
|
13
|
-
- realtime
|
|
14
|
-
- components
|
|
15
|
-
|
|
16
|
-
## Standards
|
|
17
|
-
|
|
18
|
-
- css-naming.md -- Tailwind utility class ordering conventions
|
|
19
|
-
- architecture.md -- Server vs Client component boundaries
|
|
20
|
-
|
|
21
|
-
## Patterns
|
|
22
|
-
|
|
23
|
-
### @supabase/ssr Client Setup
|
|
24
|
-
|
|
25
|
-
```typescript
|
|
26
|
-
// lib/supabase/client.ts (Browser client)
|
|
27
|
-
import { createBrowserClient } from "@supabase/ssr";
|
|
28
|
-
import type { Database } from "@/types/database";
|
|
29
|
-
|
|
30
|
-
export function createClient() {
|
|
31
|
-
return createBrowserClient<Database>(
|
|
32
|
-
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
33
|
-
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
```typescript
|
|
39
|
-
// lib/supabase/server.ts (Server client)
|
|
40
|
-
import { createServerClient } from "@supabase/ssr";
|
|
41
|
-
import { cookies } from "next/headers";
|
|
42
|
-
import type { Database } from "@/types/database";
|
|
43
|
-
|
|
44
|
-
export async function createServerSupabaseClient() {
|
|
45
|
-
const cookieStore = await cookies();
|
|
46
|
-
|
|
47
|
-
return createServerClient<Database>(
|
|
48
|
-
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
49
|
-
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
50
|
-
{
|
|
51
|
-
cookies: {
|
|
52
|
-
getAll() {
|
|
53
|
-
return cookieStore.getAll();
|
|
54
|
-
},
|
|
55
|
-
setAll(cookiesToSet) {
|
|
56
|
-
try {
|
|
57
|
-
cookiesToSet.forEach(({ name, value, options }) =>
|
|
58
|
-
cookieStore.set(name, value, options)
|
|
59
|
-
);
|
|
60
|
-
} catch {
|
|
61
|
-
// The `setAll` method is called from a Server Component
|
|
62
|
-
// if middleware refreshes the session. Ignore in that context.
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
}
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
### Auth Middleware
|
|
72
|
-
|
|
73
|
-
```typescript
|
|
74
|
-
// middleware.ts
|
|
75
|
-
import { createServerClient } from "@supabase/ssr";
|
|
76
|
-
import { NextResponse, type NextRequest } from "next/server";
|
|
77
|
-
|
|
78
|
-
const publicRoutes = ["/", "/login", "/signup", "/auth/callback"];
|
|
79
|
-
|
|
80
|
-
export async function middleware(request: NextRequest) {
|
|
81
|
-
let response = NextResponse.next({ request });
|
|
82
|
-
|
|
83
|
-
const supabase = createServerClient(
|
|
84
|
-
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
85
|
-
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
86
|
-
{
|
|
87
|
-
cookies: {
|
|
88
|
-
getAll() {
|
|
89
|
-
return request.cookies.getAll();
|
|
90
|
-
},
|
|
91
|
-
setAll(cookiesToSet) {
|
|
92
|
-
cookiesToSet.forEach(({ name, value }) =>
|
|
93
|
-
request.cookies.set(name, value)
|
|
94
|
-
);
|
|
95
|
-
response = NextResponse.next({ request });
|
|
96
|
-
cookiesToSet.forEach(({ name, value, options }) =>
|
|
97
|
-
response.cookies.set(name, value, options)
|
|
98
|
-
);
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
}
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
const {
|
|
105
|
-
data: { user },
|
|
106
|
-
} = await supabase.auth.getUser();
|
|
107
|
-
|
|
108
|
-
const isPublicRoute = publicRoutes.some((route) =>
|
|
109
|
-
request.nextUrl.pathname.startsWith(route)
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
if (!user && !isPublicRoute) {
|
|
113
|
-
const url = request.nextUrl.clone();
|
|
114
|
-
url.pathname = "/login";
|
|
115
|
-
url.searchParams.set("redirectTo", request.nextUrl.pathname);
|
|
116
|
-
return NextResponse.redirect(url);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return response;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export const config = {
|
|
123
|
-
matcher: ["/((?!_next/static|_next/image|favicon.ico|api/health).*)"],
|
|
124
|
-
};
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
### Auth Callback Route
|
|
128
|
-
|
|
129
|
-
```typescript
|
|
130
|
-
// app/auth/callback/route.ts
|
|
131
|
-
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
|
132
|
-
import { NextResponse } from "next/server";
|
|
133
|
-
|
|
134
|
-
export async function GET(request: Request) {
|
|
135
|
-
const { searchParams, origin } = new URL(request.url);
|
|
136
|
-
const code = searchParams.get("code");
|
|
137
|
-
const next = searchParams.get("next") ?? "/dashboard";
|
|
138
|
-
|
|
139
|
-
if (code) {
|
|
140
|
-
const supabase = await createServerSupabaseClient();
|
|
141
|
-
const { error } = await supabase.auth.exchangeCodeForSession(code);
|
|
142
|
-
if (!error) {
|
|
143
|
-
return NextResponse.redirect(`${origin}${next}`);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return NextResponse.redirect(`${origin}/login?error=auth_failed`);
|
|
148
|
-
}
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
### React Query + Supabase
|
|
152
|
-
|
|
153
|
-
```typescript
|
|
154
|
-
// hooks/use-documents.ts
|
|
155
|
-
"use client";
|
|
156
|
-
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
157
|
-
import { createClient } from "@/lib/supabase/client";
|
|
158
|
-
import type { Document, CreateDocumentInput } from "@/types/database";
|
|
159
|
-
|
|
160
|
-
const supabase = createClient();
|
|
161
|
-
|
|
162
|
-
export function useDocuments(page = 1, pageSize = 10) {
|
|
163
|
-
return useQuery({
|
|
164
|
-
queryKey: ["documents", page, pageSize],
|
|
165
|
-
queryFn: async () => {
|
|
166
|
-
const from = (page - 1) * pageSize;
|
|
167
|
-
const to = from + pageSize - 1;
|
|
168
|
-
|
|
169
|
-
const { data, error, count } = await supabase
|
|
170
|
-
.from("documents")
|
|
171
|
-
.select("*", { count: "exact" })
|
|
172
|
-
.order("created_at", { ascending: false })
|
|
173
|
-
.range(from, to);
|
|
174
|
-
|
|
175
|
-
if (error) throw error;
|
|
176
|
-
return { items: data as Document[], totalCount: count ?? 0, page, pageSize };
|
|
177
|
-
},
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export function useCreateDocument() {
|
|
182
|
-
const queryClient = useQueryClient();
|
|
183
|
-
|
|
184
|
-
return useMutation({
|
|
185
|
-
mutationFn: async (input: CreateDocumentInput) => {
|
|
186
|
-
const { data, error } = await supabase
|
|
187
|
-
.from("documents")
|
|
188
|
-
.insert(input)
|
|
189
|
-
.select()
|
|
190
|
-
.single();
|
|
191
|
-
|
|
192
|
-
if (error) throw error;
|
|
193
|
-
return data as Document;
|
|
194
|
-
},
|
|
195
|
-
onSuccess: () => {
|
|
196
|
-
queryClient.invalidateQueries({ queryKey: ["documents"] });
|
|
197
|
-
},
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
### Form with react-hook-form + zod
|
|
203
|
-
|
|
204
|
-
```typescript
|
|
205
|
-
// components/document-form.tsx
|
|
206
|
-
"use client";
|
|
207
|
-
import { useForm } from "react-hook-form";
|
|
208
|
-
import { zodResolver } from "@hookform/resolvers/zod";
|
|
209
|
-
import { z } from "zod";
|
|
210
|
-
import { Button } from "@/components/ui/button";
|
|
211
|
-
import { Input } from "@/components/ui/input";
|
|
212
|
-
import { Textarea } from "@/components/ui/textarea";
|
|
213
|
-
import { useCreateDocument } from "@/hooks/use-documents";
|
|
214
|
-
|
|
215
|
-
const schema = z.object({
|
|
216
|
-
title: z.string().min(1, "Title is required").max(200),
|
|
217
|
-
content: z.string().min(1, "Content is required"),
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
type FormData = z.infer<typeof schema>;
|
|
221
|
-
|
|
222
|
-
export function DocumentForm({ onSuccess }: { onSuccess?: () => void }) {
|
|
223
|
-
const { register, handleSubmit, formState: { errors }, reset } = useForm<FormData>({
|
|
224
|
-
resolver: zodResolver(schema),
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
const createDocument = useCreateDocument();
|
|
228
|
-
|
|
229
|
-
const onSubmit = async (data: FormData) => {
|
|
230
|
-
await createDocument.mutateAsync(data);
|
|
231
|
-
reset();
|
|
232
|
-
onSuccess?.();
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
return (
|
|
236
|
-
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
237
|
-
<div>
|
|
238
|
-
<Input placeholder="Document title" {...register("title")} />
|
|
239
|
-
{errors.title && <p className="text-sm text-destructive mt-1">{errors.title.message}</p>}
|
|
240
|
-
</div>
|
|
241
|
-
<div>
|
|
242
|
-
<Textarea placeholder="Content..." rows={6} {...register("content")} />
|
|
243
|
-
{errors.content && <p className="text-sm text-destructive mt-1">{errors.content.message}</p>}
|
|
244
|
-
</div>
|
|
245
|
-
<Button type="submit" disabled={createDocument.isPending}>
|
|
246
|
-
{createDocument.isPending ? "Creating..." : "Create Document"}
|
|
247
|
-
</Button>
|
|
248
|
-
</form>
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
### Realtime Subscription Hook
|
|
254
|
-
|
|
255
|
-
```typescript
|
|
256
|
-
// hooks/use-realtime.ts
|
|
257
|
-
"use client";
|
|
258
|
-
import { useEffect } from "react";
|
|
259
|
-
import { useQueryClient } from "@tanstack/react-query";
|
|
260
|
-
import { createClient } from "@/lib/supabase/client";
|
|
261
|
-
import type { RealtimePostgresChangesPayload } from "@supabase/supabase-js";
|
|
262
|
-
|
|
263
|
-
export function useRealtimeSubscription<T extends Record<string, unknown>>(
|
|
264
|
-
table: string,
|
|
265
|
-
queryKey: string[]
|
|
266
|
-
) {
|
|
267
|
-
const queryClient = useQueryClient();
|
|
268
|
-
const supabase = createClient();
|
|
269
|
-
|
|
270
|
-
useEffect(() => {
|
|
271
|
-
const channel = supabase
|
|
272
|
-
.channel(`${table}-changes`)
|
|
273
|
-
.on<T>(
|
|
274
|
-
"postgres_changes",
|
|
275
|
-
{ event: "*", schema: "public", table },
|
|
276
|
-
(payload: RealtimePostgresChangesPayload<T>) => {
|
|
277
|
-
queryClient.invalidateQueries({ queryKey });
|
|
278
|
-
}
|
|
279
|
-
)
|
|
280
|
-
.subscribe();
|
|
281
|
-
|
|
282
|
-
return () => {
|
|
283
|
-
supabase.removeChannel(channel);
|
|
284
|
-
};
|
|
285
|
-
}, [table, queryKey, queryClient, supabase]);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Usage in component:
|
|
289
|
-
// useRealtimeSubscription<Document>("documents", ["documents"]);
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
### Server Component Data Fetching
|
|
293
|
-
|
|
294
|
-
```typescript
|
|
295
|
-
// app/dashboard/page.tsx
|
|
296
|
-
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
|
297
|
-
import { DocumentList } from "@/components/document-list";
|
|
298
|
-
|
|
299
|
-
export default async function DashboardPage() {
|
|
300
|
-
const supabase = await createServerSupabaseClient();
|
|
301
|
-
const { data: user } = await supabase.auth.getUser();
|
|
302
|
-
|
|
303
|
-
const { data: documents } = await supabase
|
|
304
|
-
.from("documents")
|
|
305
|
-
.select("*")
|
|
306
|
-
.order("created_at", { ascending: false })
|
|
307
|
-
.limit(20);
|
|
308
|
-
|
|
309
|
-
return (
|
|
310
|
-
<main className="container mx-auto py-8">
|
|
311
|
-
<h1 className="text-2xl font-bold mb-6">
|
|
312
|
-
Welcome, {user.user?.email}
|
|
313
|
-
</h1>
|
|
314
|
-
<DocumentList initialDocuments={documents ?? []} />
|
|
315
|
-
</main>
|
|
316
|
-
);
|
|
317
|
-
}
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
## Checklist
|
|
321
|
-
|
|
322
|
-
- [ ] @supabase/ssr used (not @supabase/auth-helpers-nextjs)
|
|
323
|
-
- [ ] Server and browser clients in separate files
|
|
324
|
-
- [ ] Middleware refreshes session on every request
|
|
325
|
-
- [ ] Public routes excluded from auth check
|
|
326
|
-
- [ ] Auth callback route handles code exchange
|
|
327
|
-
- [ ] React Query configured with QueryClientProvider
|
|
328
|
-
- [ ] Forms validated with zod schemas
|
|
329
|
-
- [ ] Realtime subscriptions clean up on unmount
|
|
330
|
-
- [ ] Server Components fetch data directly (no useEffect)
|
|
331
|
-
- [ ] Environment variables: NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY
|
|
332
|
-
|
|
333
|
-
---
|
|
334
|
-
|
|
335
|
-
*MORPH-SPEC by Polymorphism Tech*
|