Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,15 @@ POSTGRES_DB=postgres
## Supabase config
###
SUPABASE_URL=https://example.supabase.co
SUPABASE_PK=example-key
SUPABASE_AUTH_JWT_SECRET=abcdefghijklmnopqrstuvwxzyz1234567890

# Supabase Keys
# Get from: supabase status (local) or Supabase dashboard (cloud)
# - Publishable key: For auth operations (getClaims, signIn, signUp)
# - Secret key: For admin operations (auth.admin.*) - optional
SUPABASE_PUBLISHABLE_KEY=sb_publishable_xxx
SUPABASE_SECRET_KEY=sb_secret_xxx

###
# Feature Flags
###
FEATURE_FLAG_SENTRY_DEVELOPMENT=true
FEATURE_FLAG_SENTRY_DEVELOPMENT=true
78 changes: 78 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,84 @@ pnpm token-test --token=<jwt-token> --decode-only

Be sure to update the seeds as new migrations are added.

## Security

### Row Level Security (RLS)

This project uses **RLS with deny-all policy** as defense-in-depth:

```
┌─────────────────────────────────────────────────────────────┐
│ Data Queries: API → Drizzle ORM → PostgreSQL │
│ (Bypasses RLS via DATABASE_URL) │
├─────────────────────────────────────────────────────────────┤
│ Auth: API uses supabase.auth.getClaims() for JWT verify │
│ (Works with publishable key, not affected by RLS) │
├─────────────────────────────────────────────────────────────┤
│ Direct Access: Blocked by RLS (no policies = deny all) │
│ If publishable key leaks, queries return [] │
└─────────────────────────────────────────────────────────────┘
```

**How it works:**

- ✅ RLS enabled on all data tables with **no policies** (deny all)
- ✅ API uses `DATABASE_URL` (Drizzle ORM) which bypasses RLS
- ✅ Auth endpoints (`/auth/v1/*`) are not affected by RLS
- ✅ If publishable key leaks, direct data queries return empty results

### Supabase Keys

| Variable | Purpose | Used By |
|----------|---------|---------|
| `DATABASE_URL` | Drizzle ORM data queries | API |
| `SUPABASE_PUBLISHABLE_KEY` | Auth operations (getClaims, signIn) | API |
| `SUPABASE_SECRET_KEY` | Admin operations (optional) | API |

**Key formats (2025+):**

- `sb_publishable_*` - Publishable/anon key for client operations
- `sb_secret_*` - Secret/service role key for admin operations

Legacy key names (`SUPABASE_PK`, `SUPABASE_ANON_KEY`) are still supported for backward compatibility.

### Token Verification

This project uses `supabase.auth.getClaims()` for JWT verification instead of the legacy `jwt.verify()` approach.

**Why getClaims()?**

| Old Approach | New Approach |
|--------------|--------------|
| `jwt.verify(token, JWT_SECRET)` | `supabase.auth.getClaims(token)` |
| Required `SUPABASE_AUTH_JWT_SECRET` env var | No secret needed |
| Manual dependency on `jsonwebtoken` | Built into Supabase SDK |

**How getClaims() works:**

```
┌─────────────────────────────────────────────────────────────┐
│ getClaims(token) │
│ │
│ 1. Decode JWT header → check algorithm │
│ │
│ 2. If asymmetric (RS256, new sb_publishable_* keys): │
│ → Fetch JWKS (public keys) from Supabase │
│ → Verify signature locally with Web Crypto API │
│ → Fast, no network call for verification │
│ │
│ 3. If symmetric (HS256, old eyJhbG... keys): │
│ → Falls back to getUser() server call │
│ → Supabase server verifies the token │
│ → Still works, just slower │
└─────────────────────────────────────────────────────────────┘
```

**Benefits:**
- ✅ No `JWT_SECRET` environment variable needed
- ✅ Local verification with asymmetric keys (faster, no network call)
- ✅ Uses new `SUPABASE_PUBLISHABLE_KEY` format (`sb_publishable_*`)

## Build with docker

```bash
Expand Down
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@types/jsonwebtoken": "^9.0.6",
"@types/swagger-ui-express": "^4.1.8",
"@typescript-eslint/eslint-plugin": "^8.29.0",
"@typescript-eslint/parser": "^8.29.0",
Expand Down Expand Up @@ -76,7 +75,6 @@
"express": "^4.19.2",
"express-rate-limit": "^8.0.1",
"helmet": "^8.0.0",
"jsonwebtoken": "^9.0.2",
"pg": "^8.14.1",
"pino": "^9.6.0",
"pino-http": "^10.4.0",
Expand Down
100 changes: 0 additions & 100 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading