auth-implementation · git:20260807.975fe85 · 2026-08-07 · sha256 a3e7cdf850e1928e
auth-implementation git:20260807.975fe85A
Immutable. This exact content never changes and is served at /api/v1/blob/a3e7cdf850e1928e.
---
name: auth-implementation
description: Implement authentication, user sync, authorization, data isolation, session state, and protected routes. Use when adding or repairing app identity and access.
license: MIT
---
# Authentication implementation
Implement authentication and authorization end to end: provider, user lifecycle, database mapping, server checks, data isolation, frontend state, and recovery. Prefer the project's existing provider; ask before introducing a new identity system.
## Phase 1: Detect the Stack
Before writing anything, figure out what exists.
### 1.1 Auth Provider
Check for existing auth:
- `@clerk/nextjs` in package.json → Clerk
- `next-auth` in package.json → NextAuth.js
- `@supabase/auth-helpers-nextjs` or `@supabase/ssr` → Supabase Auth
- `firebase/auth` → Firebase Auth
- None → ask the user which to set up
### 1.2 Database
- `@supabase/supabase-js` → Supabase (Postgres)
- `@prisma/client` → Prisma (check schema.prisma for provider)
- `drizzle-orm` → Drizzle
- `mongoose` → MongoDB
- Raw `pg` or `postgres` → Direct Postgres
### 1.3 Frontend Framework
- Next.js App Router vs Pages Router (check for `app/` vs `pages/`)
- React SPA (Vite)
- Other
### 1.4 What's Already Done
- Is there a users table? Read the schema.
- Are there any auth-related API routes?
- Any middleware files?
- Any existing auth hooks or context providers?
Tell the user:
```
Detected: [auth provider] + [database] + [framework]
Users table: [exists / missing]
I'll wire: [list of what needs to be created]
```
## Phase 2: Database Schema
### If no users table exists
**Supabase (SQL):**
```sql
create table users (
id uuid primary key default gen_random_uuid(),
auth_id text unique not null,
email text unique,
name text,
avatar_url text,
created_at timestamptz default now(),
updated_at timestamptz default now()
);
create index idx_users_auth_id on users(auth_id);
create index idx_users_email on users(email);
```
**Prisma:**
```prisma
model User {
id String @id @default(uuid())
authId String @unique @map("auth_id")
email String? @unique
name String?
avatarUrl String? @map("avatar_url")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("users")
}
```
**Drizzle:**
```typescript
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
authId: text('auth_id').unique().notNull(),
email: text('email').unique(),
name: text('name'),
avatarUrl: text('avatar_url'),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
```
### If users table exists
Add `auth_id` column if missing:
```sql
alter table users add column if not exists auth_id text unique;
create index if not exists idx_users_auth_id on users(auth_id);
```
## Phase 3: Row-Level Security (RLS)
**This is the part everyone skips.** Without RLS, any authenticated user can read/write any other user's data if they guess the right ID. RLS makes the database enforce access rules.
### For Supabase
Enable RLS on every table that contains user data:
```sql
-- Enable RLS
alter table users enable row level security;
-- Users can only read their own row
create policy "users_read_own"
on users for select
using (auth_id = current_setting('request.jwt.claims')::json->>'sub');
-- Users can only update their own row
create policy "users_update_own"
on users for update
using (auth_id = current_setting('request.jwt.claims')::json->>'sub');
-- Backend service role bypasses RLS for admin operations
create policy "service_role_all"
on users for all
using (current_setting('role') = 'service_role');
```
For any other user-owned table (e.g., posts, projects, settings), apply the same pattern:
```sql
alter table [table_name] enable row level security;
create policy "[table]_read_own"
on [table_name] for select
using (user_id = (
select id from users where auth_id = current_setting('request.jwt.claims')::json->>'sub'
));
create policy "[table]_write_own"
on [table_name] for insert
with check (user_id = (
select id from users where auth_id = current_setting('request.jwt.claims')::json->>'sub'
));
create policy "[table]_update_own"
on [table_name] for update
using (user_id = (
select id from users where auth_id = current_setting('request.jwt.claims')::json->>'sub'
));
create policy "[table]_delete_own"
on [table_name] for delete
using (user_id = (
select id from users where auth_id = current_setting('request.jwt.claims')::json->>'sub'
));
```
**Tell the user:** "RLS is enabled. Even if someone bypasses your frontend, the database itself rejects unauthorized access."
### For Prisma / Drizzle (no built-in RLS)
Add a middleware or helper that filters every query by the current user:
```typescript
// lib/auth-filter.ts
export function forUser(userId: string) {
return { where: { userId } };
}
// Usage in any route:
const posts = await prisma.post.findMany(forUser(session.user.id));
```
This isn't true RLS but it's the equivalent pattern. Flag to the user that Prisma doesn't enforce this at the database level — it's application-level security.
## Phase 4: Implement the Detected Provider
Read exactly one provider reference before writing auth integration code:
- **Clerk with Next.js and Supabase:** Read [references/clerk.md](references/clerk.md) for database sync, the React hook, middleware, API protection, and environment variables.
- **NextAuth with Next.js and Prisma:** Read [references/nextauth.md](references/nextauth.md) for adapter sync, session fields, the React hook, middleware, API protection, and environment variables.
- **Supabase Auth:** Read [references/supabase-auth.md](references/supabase-auth.md) for the public-user trigger and environment variables.
Do not combine provider implementations. Adapt the selected reference to the schema and routing pattern detected in Phase 1.
## Phase 5: Cross-Provider Security Checks
After the selected provider path is implemented:
- Protect private pages and API routes on the server, not only in client components.
- Keep provider secrets and Supabase service-role keys in server-only environment variables.
- Add `.env` and `.env.local` to `.gitignore` if either could be committed.
- Preserve the database isolation rules from Phase 3.
- Prevent duplicate user creation during repeated login or React Strict Mode.
- Reconcile an existing database user by verified identity before creating another row.
## Phase 6: Verify
Walk through these flows after wiring:
```
Flow 1: New User Signup
[ ] User signs up via auth provider
[ ] Sync endpoint creates user in database
[ ] Frontend hook reflects authenticated state
[ ] isNewUser flag is true (can show onboarding)
Flow 2: Returning User Login
[ ] User signs in
[ ] Sync endpoint finds existing user, updates fields
[ ] Frontend hook loads user data
[ ] isNewUser flag is false
Flow 3: Protected Routes
[ ] Unauthenticated user visiting /dashboard → redirected to sign-in
[ ] Authenticated user visiting /dashboard → sees content
[ ] API route without auth → returns 401
Flow 4: Data Isolation (if RLS enabled)
[ ] User A creates data → only User A can read it
[ ] User B cannot access User A's data even with a direct API call
[ ] Service role (backend) can access all data for admin operations
Flow 5: Edge Cases
[ ] User signs up with email, then signs in with OAuth (same email) → accounts linked
[ ] Token expires → user re-authenticates without data loss
[ ] Multiple tabs open → auth state syncs across tabs
```
Tell the user which flows are wired and which need manual testing.