@polymorphism-tech/morph-spec 4.17.0 → 4.18.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 +36 -0
- package/README.md +2 -2
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/agents/backend/neon-expert.md +9 -27
- package/framework/agents/integrations/neon-auth.md +151 -0
- package/framework/agents.json +21 -13
- package/framework/rules/neon-auth-standards.md +13 -0
- package/framework/rules/neon-standards.md +1 -0
- package/framework/skills/level-0-meta/morph-agent-teams/SKILL.md +3 -3
- package/framework/skills/level-0-meta/morph-frontend-review/SKILL.md +1 -1
- package/framework/skills/level-0-meta/morph-init/SKILL.md +4 -4
- package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +13 -9
- package/framework/standards/backend/database/postgresql/database.md +5 -5
- package/framework/standards/backend/integrations/asaas/asaas-api.md +1 -1
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +364 -0
- package/framework/standards/frontend/nextjs/nextjs-patterns.md +9 -9
- package/framework/templates/README.md +2 -2
- package/framework/templates/REGISTRY.json +5 -5
- package/framework/templates/code/sql/postgresql-migration.sql +10 -10
- package/framework/templates/code/sql/postgresql-migration.template.sql +12 -14
- package/framework/templates/code/sql/postgresql-rls-policy.sql +6 -9
- package/framework/templates/feature/decisions.md +8 -7
- package/framework/templates/integrations/neon-auth-config.cs +133 -0
- package/framework/workflows/docs/full-morph.md +1 -1
- package/package.json +1 -1
- package/src/lib/detectors/index.js +2 -2
- package/src/lib/generators/architecture-scanner.js +2 -3
- package/framework/agents/integrations/clerk-auth.md +0 -109
- package/framework/rules/clerk-standards.md +0 -13
- package/framework/standards/backend/integrations/clerk/clerk-auth.md +0 -292
- package/framework/templates/integrations/clerk-config.cs +0 -217
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
# Neon Auth (Better Auth) Integration Standard
|
|
2
|
+
|
|
3
|
+
> **Scope:** universal
|
|
4
|
+
> **Layer:** 2 (on keyword)
|
|
5
|
+
> **Keywords:** neon auth, better auth, authentication, nextjs, user management, session, JWT
|
|
6
|
+
> **Load When:** neon auth or authentication keywords detected
|
|
7
|
+
|
|
8
|
+
Managed authentication powered by Better Auth, with all auth data stored directly in your Neon database (`neon_auth` schema). No external auth providers, no webhooks, no sync delays.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
Neon Auth provides:
|
|
15
|
+
- Pre-built UI components (AuthView, UserButton, SignedIn)
|
|
16
|
+
- Social auth (Google, GitHub, etc.)
|
|
17
|
+
- Session management with HTTP-only cookies
|
|
18
|
+
- JWT tokens for Row-Level Security
|
|
19
|
+
- Branch-isolated auth data (perfect for preview environments)
|
|
20
|
+
- Database as single source of truth
|
|
21
|
+
|
|
22
|
+
**Primary Use Case:** Next.js 15 + App Router + Neon PostgreSQL
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Core Principles
|
|
27
|
+
|
|
28
|
+
1. **Database-First**: Auth data lives in `neon_auth` schema — no external service to sync
|
|
29
|
+
2. **Branch Isolation**: Each Neon branch gets isolated users, sessions, and auth config
|
|
30
|
+
3. **Server-First**: Use server-side auth checks with `auth.getSession()`
|
|
31
|
+
4. **JWT for RLS**: Tokens contain `sub` (user ID), `email`, `role: "authenticated"` for database policies
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Installation & Setup
|
|
36
|
+
|
|
37
|
+
### Install Packages
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install @neondatabase/auth@latest
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Environment Variables
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# .env.local
|
|
47
|
+
NEON_AUTH_BASE_URL=https://ep-xxx.neonauth.us-east-2.aws.neon.build/neondb/auth
|
|
48
|
+
NEON_AUTH_COOKIE_SECRET=your-secret-at-least-32-characters-long
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Generate cookie secret:
|
|
52
|
+
```bash
|
|
53
|
+
openssl rand -base64 32
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### SDK Initialization
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// lib/auth/client.ts
|
|
60
|
+
'use client';
|
|
61
|
+
import { createAuthClient } from '@neondatabase/auth/next';
|
|
62
|
+
export const authClient = createAuthClient();
|
|
63
|
+
|
|
64
|
+
// lib/auth/server.ts
|
|
65
|
+
import { createNeonAuth } from '@neondatabase/auth/next/server';
|
|
66
|
+
export const auth = createNeonAuth({
|
|
67
|
+
baseUrl: process.env.NEON_AUTH_BASE_URL!,
|
|
68
|
+
cookies: {
|
|
69
|
+
secret: process.env.NEON_AUTH_COOKIE_SECRET!,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Root Layout Provider
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
// app/providers.tsx
|
|
78
|
+
'use client';
|
|
79
|
+
import { NeonAuthUIProvider } from '@neondatabase/auth/react';
|
|
80
|
+
import '@neondatabase/auth/ui/css';
|
|
81
|
+
import Link from 'next/link';
|
|
82
|
+
import { useRouter } from 'next/navigation';
|
|
83
|
+
import { authClient } from '@/lib/auth/client';
|
|
84
|
+
|
|
85
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
86
|
+
const router = useRouter();
|
|
87
|
+
return (
|
|
88
|
+
<NeonAuthUIProvider
|
|
89
|
+
authClient={authClient}
|
|
90
|
+
navigate={router.push}
|
|
91
|
+
replace={router.replace}
|
|
92
|
+
onSessionChange={router.refresh}
|
|
93
|
+
Link={Link}
|
|
94
|
+
>
|
|
95
|
+
{children}
|
|
96
|
+
</NeonAuthUIProvider>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Auth API Route
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// app/api/auth/[...path]/route.ts
|
|
105
|
+
import { auth } from '@/lib/auth/server';
|
|
106
|
+
|
|
107
|
+
export const { GET, POST } = auth.handler();
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Common Patterns
|
|
113
|
+
|
|
114
|
+
### Sign In / Sign Up Pages
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
// app/auth/sign-in/page.tsx
|
|
118
|
+
import { AuthView } from '@neondatabase/auth/react';
|
|
119
|
+
|
|
120
|
+
export default function SignInPage() {
|
|
121
|
+
return (
|
|
122
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
123
|
+
<AuthView pathname="sign-in" />
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// app/auth/sign-up/page.tsx
|
|
129
|
+
import { AuthView } from '@neondatabase/auth/react';
|
|
130
|
+
|
|
131
|
+
export default function SignUpPage() {
|
|
132
|
+
return (
|
|
133
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
134
|
+
<AuthView pathname="sign-up" />
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Server-Side Auth Check
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
// app/dashboard/page.tsx
|
|
144
|
+
import { auth } from '@/lib/auth/server';
|
|
145
|
+
import { redirect } from 'next/navigation';
|
|
146
|
+
|
|
147
|
+
export const dynamic = 'force-dynamic';
|
|
148
|
+
|
|
149
|
+
export default async function DashboardPage() {
|
|
150
|
+
const { data: session } = await auth.getSession();
|
|
151
|
+
|
|
152
|
+
if (!session?.user) {
|
|
153
|
+
redirect('/auth/sign-in');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return <div>Dashboard for {session.user.name || session.user.email}</div>;
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Client Component with User Data
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
// components/UserProfile.tsx
|
|
164
|
+
'use client';
|
|
165
|
+
import { useSession } from '@/lib/auth/client';
|
|
166
|
+
|
|
167
|
+
export function UserProfile() {
|
|
168
|
+
const { data } = useSession();
|
|
169
|
+
const user = data?.user;
|
|
170
|
+
|
|
171
|
+
if (!user) return <div>Not signed in</div>;
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<div>
|
|
175
|
+
<p>Welcome, {user.name || user.email}!</p>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Route Protection (Middleware)
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
// middleware.ts
|
|
185
|
+
import { auth } from '@/lib/auth/server';
|
|
186
|
+
|
|
187
|
+
export default auth.middleware({
|
|
188
|
+
loginUrl: '/auth/sign-in',
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
export const config = {
|
|
192
|
+
matcher: [
|
|
193
|
+
'/dashboard/:path*',
|
|
194
|
+
'/settings/:path*',
|
|
195
|
+
'/((?!_next/static|_next/image|favicon.ico|auth).*)',
|
|
196
|
+
],
|
|
197
|
+
};
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Component-Level Protection
|
|
201
|
+
|
|
202
|
+
```tsx
|
|
203
|
+
import { SignedIn, RedirectToSignIn } from '@neondatabase/auth/react';
|
|
204
|
+
|
|
205
|
+
export default function ProtectedPage() {
|
|
206
|
+
return (
|
|
207
|
+
<>
|
|
208
|
+
<SignedIn>
|
|
209
|
+
<div>Protected content</div>
|
|
210
|
+
</SignedIn>
|
|
211
|
+
<RedirectToSignIn />
|
|
212
|
+
</>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## OAuth (Social Login)
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
// Trigger OAuth sign-in
|
|
223
|
+
await authClient.signIn.social({
|
|
224
|
+
provider: 'google',
|
|
225
|
+
callbackURL: 'http://localhost:3000/auth/callback',
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Configure OAuth providers in the Neon Console under Auth > Configuration.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## API Route Protection
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
// app/api/protected/route.ts
|
|
237
|
+
import { auth } from '@/lib/auth/server';
|
|
238
|
+
|
|
239
|
+
export const dynamic = 'force-dynamic';
|
|
240
|
+
|
|
241
|
+
export async function GET() {
|
|
242
|
+
const { data: session } = await auth.getSession();
|
|
243
|
+
|
|
244
|
+
if (!session?.user) {
|
|
245
|
+
return new Response('Unauthorized', { status: 401 });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return Response.json({ userId: session.user.id });
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## .NET Backend Integration
|
|
255
|
+
|
|
256
|
+
Neon Auth issues standard JWTs. The .NET API validates these tokens directly — no external auth SDK needed.
|
|
257
|
+
|
|
258
|
+
### JWT Bearer Configuration
|
|
259
|
+
|
|
260
|
+
```csharp
|
|
261
|
+
// Program.cs
|
|
262
|
+
builder.Services.AddAuthentication("NeonAuth")
|
|
263
|
+
.AddJwtBearer("NeonAuth", options =>
|
|
264
|
+
{
|
|
265
|
+
options.Authority = builder.Configuration["NeonAuth:BaseUrl"];
|
|
266
|
+
options.TokenValidationParameters = new()
|
|
267
|
+
{
|
|
268
|
+
ValidateIssuer = true,
|
|
269
|
+
ValidateAudience = false,
|
|
270
|
+
ValidateLifetime = true,
|
|
271
|
+
ValidateIssuerSigningKey = true,
|
|
272
|
+
NameClaimType = "sub"
|
|
273
|
+
};
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
builder.Services.AddAuthorization();
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Protected Endpoints
|
|
280
|
+
|
|
281
|
+
```csharp
|
|
282
|
+
app.MapGet("/api/profile", (ClaimsPrincipal user) =>
|
|
283
|
+
{
|
|
284
|
+
var userId = user.FindFirstValue("sub");
|
|
285
|
+
if (userId is null) return Results.Unauthorized();
|
|
286
|
+
return Results.Ok(new { UserId = userId });
|
|
287
|
+
}).RequireAuthorization();
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### appsettings.json
|
|
291
|
+
|
|
292
|
+
```json
|
|
293
|
+
{
|
|
294
|
+
"NeonAuth": {
|
|
295
|
+
"BaseUrl": "https://ep-xxx.neonauth.us-east-2.aws.neon.build/neondb/auth"
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## Row-Level Security Integration
|
|
303
|
+
|
|
304
|
+
Neon Auth JWT tokens are automatically validated by the Data API. The `auth.user_id()` function (via `pg_session_jwt` extension) exposes the authenticated user ID from the JWT `sub` claim.
|
|
305
|
+
|
|
306
|
+
```sql
|
|
307
|
+
-- Enable RLS
|
|
308
|
+
ALTER TABLE public.documents ENABLE ROW LEVEL SECURITY;
|
|
309
|
+
|
|
310
|
+
-- Users can only access their own rows
|
|
311
|
+
CREATE POLICY "Users can view own documents"
|
|
312
|
+
ON public.documents FOR SELECT TO authenticated
|
|
313
|
+
USING (user_id = auth.user_id());
|
|
314
|
+
|
|
315
|
+
CREATE POLICY "Users can insert own documents"
|
|
316
|
+
ON public.documents FOR INSERT TO authenticated
|
|
317
|
+
WITH CHECK (user_id = auth.user_id());
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## Database Schema
|
|
323
|
+
|
|
324
|
+
Neon Auth automatically manages the `neon_auth` schema with these tables:
|
|
325
|
+
- `neon_auth.user` — User profiles
|
|
326
|
+
- `neon_auth.session` — Active sessions
|
|
327
|
+
- `neon_auth.account` — Auth credentials (password hashes, OAuth tokens)
|
|
328
|
+
|
|
329
|
+
You can query these tables directly for user data — no webhook sync needed:
|
|
330
|
+
```sql
|
|
331
|
+
SELECT id, email, name FROM neon_auth.user WHERE id = auth.user_id();
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## Branch Isolation
|
|
337
|
+
|
|
338
|
+
Each Neon branch gets its own isolated auth data. This means:
|
|
339
|
+
- Preview branches have separate users and sessions
|
|
340
|
+
- Feature branches don't share auth state with production
|
|
341
|
+
- Perfect for PR preview environments
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## Best Practices
|
|
346
|
+
|
|
347
|
+
1. **Always use `force-dynamic`** on pages/routes that call `auth.getSession()`
|
|
348
|
+
2. **Cookie secret must be 32+ characters** — generate with `openssl rand -base64 32`
|
|
349
|
+
3. **Use `auth.user_id()` for RLS** — never extract JWT claims manually in SQL
|
|
350
|
+
4. **Configure OAuth in Neon Console** — not in code
|
|
351
|
+
5. **Use `SignedIn` / `RedirectToSignIn`** for client-side protection
|
|
352
|
+
6. **Use middleware** for broad route protection patterns
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## References
|
|
357
|
+
|
|
358
|
+
- [Neon Auth Documentation](https://neon.com/docs/auth)
|
|
359
|
+
- [Neon Auth Authentication Flow](https://neon.com/docs/auth/authentication-flow)
|
|
360
|
+
- [Migration from Legacy Auth](https://neon.com/docs/auth/migrate/from-legacy-auth)
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
*MORPH-SPEC by Polymorphism Tech*
|
|
@@ -65,10 +65,12 @@ export default function Error({ error, reset }: { error: Error; reset: () => voi
|
|
|
65
65
|
|
|
66
66
|
```ts
|
|
67
67
|
// app/api/documents/route.ts — BFF pattern (delegates to .NET API)
|
|
68
|
-
import { auth } from "
|
|
68
|
+
import { auth } from "@/lib/auth/server";
|
|
69
69
|
import { NextResponse } from "next/server";
|
|
70
70
|
import { z } from "zod";
|
|
71
71
|
|
|
72
|
+
export const dynamic = "force-dynamic";
|
|
73
|
+
|
|
72
74
|
const API_URL = process.env.API_URL!; // .NET backend
|
|
73
75
|
|
|
74
76
|
const CreateSchema = z.object({
|
|
@@ -77,27 +79,25 @@ const CreateSchema = z.object({
|
|
|
77
79
|
});
|
|
78
80
|
|
|
79
81
|
export async function GET() {
|
|
80
|
-
const {
|
|
81
|
-
if (!
|
|
82
|
+
const { data: session } = await auth.getSession();
|
|
83
|
+
if (!session?.user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
82
84
|
|
|
83
|
-
const token = await getToken();
|
|
84
85
|
const res = await fetch(`${API_URL}/api/documents`, {
|
|
85
|
-
headers: { Authorization: `Bearer ${
|
|
86
|
+
headers: { Authorization: `Bearer ${session.session.access_token}` },
|
|
86
87
|
});
|
|
87
88
|
return NextResponse.json(await res.json());
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
export async function POST(request: Request) {
|
|
91
|
-
const {
|
|
92
|
-
if (!
|
|
92
|
+
const { data: session } = await auth.getSession();
|
|
93
|
+
if (!session?.user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
93
94
|
|
|
94
95
|
const parsed = CreateSchema.safeParse(await request.json());
|
|
95
96
|
if (!parsed.success) return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
|
|
96
97
|
|
|
97
|
-
const token = await getToken();
|
|
98
98
|
const res = await fetch(`${API_URL}/api/documents`, {
|
|
99
99
|
method: "POST",
|
|
100
|
-
headers: { Authorization: `Bearer ${
|
|
100
|
+
headers: { Authorization: `Bearer ${session.session.access_token}`, "Content-Type": "application/json" },
|
|
101
101
|
body: JSON.stringify(parsed.data),
|
|
102
102
|
});
|
|
103
103
|
return NextResponse.json(await res.json(), { status: res.status });
|
|
@@ -77,7 +77,7 @@ framework/templates/
|
|
|
77
77
|
├── integrations/ # Platform-agnostic integrations
|
|
78
78
|
│ ├── asaas-client.cs # Asaas financial API
|
|
79
79
|
│ ├── asaas-webhook.cs # Asaas webhooks
|
|
80
|
-
│ └──
|
|
80
|
+
│ └── neon-auth-config.cs # Neon Auth (Better Auth)
|
|
81
81
|
│
|
|
82
82
|
├── ui/ # Design systems
|
|
83
83
|
│ └── design-system.css # CSS tokens
|
|
@@ -107,7 +107,7 @@ framework/templates/
|
|
|
107
107
|
**Future-proof:** When we create `vue-firebase` stack, we can reuse:
|
|
108
108
|
- `code/typescript/` templates
|
|
109
109
|
- `infrastructure/docker/` templates
|
|
110
|
-
- `integrations/` templates (
|
|
110
|
+
- `integrations/` templates (Neon Auth, etc.)
|
|
111
111
|
|
|
112
112
|
**Only create NEW templates for Vue-specific code.**
|
|
113
113
|
|
|
@@ -597,15 +597,15 @@
|
|
|
597
597
|
]
|
|
598
598
|
},
|
|
599
599
|
{
|
|
600
|
-
"id": "integration-
|
|
601
|
-
"name": "
|
|
602
|
-
"path": "integrations/
|
|
600
|
+
"id": "integration-neon-auth",
|
|
601
|
+
"name": "Neon Auth Configuration",
|
|
602
|
+
"path": "integrations/neon-auth-config.cs",
|
|
603
603
|
"location": "framework",
|
|
604
604
|
"category": "integration",
|
|
605
605
|
"phase": "implement",
|
|
606
606
|
"required": false,
|
|
607
|
-
"description": "
|
|
608
|
-
"outputName": "
|
|
607
|
+
"description": "Neon Auth (Better Auth) authentication configuration for .NET",
|
|
608
|
+
"outputName": "NeonAuthConfig.cs",
|
|
609
609
|
"placeholders": [
|
|
610
610
|
"NAMESPACE"
|
|
611
611
|
],
|
|
@@ -65,27 +65,27 @@ create trigger set_{{TABLE_NAME}}_updated_at
|
|
|
65
65
|
-- Enable RLS
|
|
66
66
|
alter table public.{{TABLE_NAME}} enable row level security;
|
|
67
67
|
|
|
68
|
-
-- Users can read their own rows (
|
|
68
|
+
-- Users can read their own rows (Neon Auth JWT via pg_session_jwt)
|
|
69
69
|
create policy "Users can read own {{TABLE_NAME}}"
|
|
70
70
|
on public.{{TABLE_NAME}}
|
|
71
|
-
for select
|
|
72
|
-
using (
|
|
71
|
+
for select to authenticated
|
|
72
|
+
using (auth.user_id() = user_id);
|
|
73
73
|
|
|
74
74
|
-- Users can insert rows for themselves
|
|
75
75
|
create policy "Users can insert own {{TABLE_NAME}}"
|
|
76
76
|
on public.{{TABLE_NAME}}
|
|
77
|
-
for insert
|
|
78
|
-
with check (
|
|
77
|
+
for insert to authenticated
|
|
78
|
+
with check (auth.user_id() = user_id);
|
|
79
79
|
|
|
80
80
|
-- Users can update their own rows
|
|
81
81
|
create policy "Users can update own {{TABLE_NAME}}"
|
|
82
82
|
on public.{{TABLE_NAME}}
|
|
83
|
-
for update
|
|
84
|
-
using (
|
|
85
|
-
with check (
|
|
83
|
+
for update to authenticated
|
|
84
|
+
using (auth.user_id() = user_id)
|
|
85
|
+
with check (auth.user_id() = user_id);
|
|
86
86
|
|
|
87
87
|
-- Users can delete their own rows
|
|
88
88
|
create policy "Users can delete own {{TABLE_NAME}}"
|
|
89
89
|
on public.{{TABLE_NAME}}
|
|
90
|
-
for delete
|
|
91
|
-
using (
|
|
90
|
+
for delete to authenticated
|
|
91
|
+
using (auth.user_id() = user_id);
|
|
@@ -70,28 +70,26 @@ create policy "{{TABLE_NAME}}_select_policy"
|
|
|
70
70
|
on public.{{TABLE_NAME}} for select
|
|
71
71
|
using (true);
|
|
72
72
|
|
|
73
|
-
-- Policy: Users can only insert their own records (
|
|
73
|
+
-- Policy: Users can only insert their own records (Neon Auth + pg_session_jwt)
|
|
74
74
|
-- create policy "{{TABLE_NAME}}_insert_policy"
|
|
75
|
-
-- on public.{{TABLE_NAME}} for insert
|
|
76
|
-
-- with check (
|
|
75
|
+
-- on public.{{TABLE_NAME}} for insert to authenticated
|
|
76
|
+
-- with check (auth.user_id() = user_id);
|
|
77
77
|
|
|
78
|
-
-- Policy: Users can only update their own records (
|
|
78
|
+
-- Policy: Users can only update their own records (Neon Auth + pg_session_jwt)
|
|
79
79
|
-- create policy "{{TABLE_NAME}}_update_policy"
|
|
80
|
-
-- on public.{{TABLE_NAME}} for update
|
|
81
|
-
-- using (
|
|
82
|
-
-- with check (
|
|
80
|
+
-- on public.{{TABLE_NAME}} for update to authenticated
|
|
81
|
+
-- using (auth.user_id() = user_id)
|
|
82
|
+
-- with check (auth.user_id() = user_id);
|
|
83
83
|
|
|
84
|
-
-- Policy: Users can only delete their own records (
|
|
84
|
+
-- Policy: Users can only delete their own records (Neon Auth + pg_session_jwt)
|
|
85
85
|
-- create policy "{{TABLE_NAME}}_delete_policy"
|
|
86
|
-
-- on public.{{TABLE_NAME}} for delete
|
|
87
|
-
-- using (
|
|
86
|
+
-- on public.{{TABLE_NAME}} for delete to authenticated
|
|
87
|
+
-- using (auth.user_id() = user_id);
|
|
88
88
|
|
|
89
|
-
-- Policy: Admins have full access (
|
|
89
|
+
-- Policy: Admins have full access (Neon Auth + pg_session_jwt)
|
|
90
90
|
-- create policy "{{TABLE_NAME}}_admin_policy"
|
|
91
91
|
-- on public.{{TABLE_NAME}} for all
|
|
92
|
-
-- using (
|
|
93
|
-
-- current_setting('request.jwt.claims')::json->>'role' = 'admin'
|
|
94
|
-
-- );
|
|
92
|
+
-- using (auth.jwt()->>'role' = 'admin');
|
|
95
93
|
|
|
96
94
|
-- ========================================
|
|
97
95
|
-- Seed Data (Optional)
|
|
@@ -40,18 +40,15 @@ create policy "{{POLICY_NAME}}_delete"
|
|
|
40
40
|
using ({{USING_EXPRESSION}});
|
|
41
41
|
|
|
42
42
|
-- ============================================================
|
|
43
|
-
-- Common USING / WITH CHECK expressions:
|
|
43
|
+
-- Common USING / WITH CHECK expressions (Neon Auth + pg_session_jwt):
|
|
44
44
|
--
|
|
45
|
-
-- Owner-based
|
|
46
|
-
--
|
|
45
|
+
-- Owner-based:
|
|
46
|
+
-- auth.user_id() = user_id
|
|
47
47
|
--
|
|
48
|
-
-- Role-based (
|
|
49
|
-
--
|
|
50
|
-
--
|
|
51
|
-
-- Organization-based (Clerk JWT):
|
|
52
|
-
-- org_id = (current_setting('request.jwt.claims')::json->>'org_id')
|
|
48
|
+
-- Role-based (extract from JWT claims):
|
|
49
|
+
-- auth.jwt()->>'role' = 'admin'
|
|
53
50
|
--
|
|
54
51
|
-- Public read, owner write:
|
|
55
52
|
-- SELECT using (true)
|
|
56
|
-
-- INSERT/UPDATE/DELETE using (
|
|
53
|
+
-- INSERT/UPDATE/DELETE using (auth.user_id() = user_id)
|
|
57
54
|
-- ============================================================
|
|
@@ -80,19 +80,20 @@ Need authentication for both Next.js frontend and .NET API.
|
|
|
80
80
|
|
|
81
81
|
### Decision
|
|
82
82
|
|
|
83
|
-
Use
|
|
83
|
+
Use Neon Auth (Better Auth) for authentication. Auth data lives in the Neon database. JWT validated by .NET API middleware.
|
|
84
84
|
|
|
85
85
|
### Consequences
|
|
86
86
|
|
|
87
87
|
**Pros:**
|
|
88
|
-
-
|
|
89
|
-
- RLS policies work with
|
|
90
|
-
- Built-in social login,
|
|
91
|
-
-
|
|
88
|
+
- Auth data in same database — no external provider, no sync delays
|
|
89
|
+
- RLS policies work with Neon Auth JWT via auth.user_id() / pg_session_jwt
|
|
90
|
+
- Built-in social login, email/password via @neondatabase/auth SDK
|
|
91
|
+
- Branch isolation — each Neon branch gets isolated auth data
|
|
92
|
+
- No webhooks needed — query neon_auth schema directly
|
|
92
93
|
|
|
93
94
|
**Cons:**
|
|
94
|
-
-
|
|
95
|
-
- JWT
|
|
95
|
+
- Neon Auth is currently in Beta
|
|
96
|
+
- JWT must be forwarded to .NET API for backend auth
|
|
96
97
|
{{/if}}
|
|
97
98
|
|
|
98
99
|
---
|