An open API service indexing awesome lists of open source software.

https://github.com/sectsect/payload-supabase-rls

Automated Row-Level Security (RLS) management for Supabase PostgreSQL, designed for PayloadCMS and other ORMs.
https://github.com/sectsect/payload-supabase-rls

drizzle-orm payloadcms postgresql rls supabase

Last synced: 2 months ago
JSON representation

Automated Row-Level Security (RLS) management for Supabase PostgreSQL, designed for PayloadCMS and other ORMs.

Awesome Lists containing this project

README

          

# @sect/payload-supabase-rls

[![Release](https://github.com/sectsect/payload-supabase-rls/actions/workflows/release.yml/badge.svg)](https://github.com/sectsect/payload-supabase-rls/actions/workflows/release.yml) [![codecov](https://codecov.io/gh/sectsect/payload-supabase-rls/graph/badge.svg?token=ovMJyAl0aY)](https://codecov.io/gh/sectsect/payload-supabase-rls) [![CodeQL](https://github.com/sectsect/payload-supabase-rls/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/sectsect/payload-supabase-rls/actions/workflows/github-code-scanning/codeql)

Automated Row-Level Security (RLS) management for Supabase PostgreSQL, designed for PayloadCMS and other ORMs.

## ⚠️ Experimental Status

> [!IMPORTANT]
> This package is currently in experimental stage (v0.x). The current implementation applies a "Deny All" security policy suitable for specific use cases. Please review the limitations section carefully before use.

## Features

- πŸ”’ **Automatic RLS enablement** - Restore RLS after ORM schema pushes
- πŸ” **Verification tools** - Detailed RLS status reporting
- 🎯 **Framework-agnostic** - Works with any PostgreSQL/Supabase project
- πŸ”Œ **PayloadCMS integration** - Drop-in plugin for automatic management
- πŸ“¦ **CLI & Programmatic API** - Use as command-line tool or import in code
- πŸ›‘οΈ **Security-first** - Deny-all policies for anon/authenticated roles

## Installation

```bash
npm install @sect/payload-supabase-rls
```

## Why This Package?

### Two Approaches to Schema Management

PayloadCMS's `postgresAdapter` offers a `push` option that controls automatic schema synchronization:

**Option 1: Disable Auto-Sync (`push: false`)**

- Requires manual migration management
- Full control over schema changes
- No RLS conflicts during development

```typescript
db: postgresAdapter({
pool: { connectionString: process.env.DATABASE_URI },
push: false, // Disable automatic schema sync
}),
```

Learn more: [PayloadCMS Postgres Documentation](https://payloadcms.com/docs/database/postgres)

**Option 2: Enable Auto-Sync with RLS Protection (`push: true`)**

- Automatic schema synchronization during development
- Faster iteration cycle
- Requires this plugin to maintain RLS security

### The Problem with Auto-Sync

When `push: true` (the default) **in development mode**, PayloadCMS uses Drizzle ORM's `pushDevSchema()` which **explicitly disables RLS** on all tables:

```sql
-- What Drizzle's pushDevSchema() does
ALTER TABLE your_table DISABLE ROW LEVEL SECURITY;
```

This leaves your database vulnerable to direct PostgREST API access.

### The Solution

This package automatically re-enables RLS after schema sync:

```sql
-- What this package does
ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;
CREATE POLICY "deny_all_select" ON your_table FOR SELECT TO anon, authenticated USING (false);
CREATE POLICY "deny_all_insert" ON your_table FOR INSERT TO anon, authenticated WITH CHECK (false);
CREATE POLICY "deny_all_update" ON your_table FOR UPDATE TO anon, authenticated USING (false);
CREATE POLICY "deny_all_delete" ON your_table FOR DELETE TO anon, authenticated USING (false);
```

**What this means**:

- **All tables**: RLS is enabled on every table in your database
- **All operations**: SELECT, INSERT, UPDATE, DELETE are all protected
- **Target roles**: `anon` and `authenticated` roles are completely blocked
- **Bypass roles**: `postgres` and `service_role` bypass RLS (PayloadCMS uses these)

## Get Started

### For PayloadCMS Users (Recommended)

Add the plugin to your PayloadCMS configuration:

```typescript
// src/payload.config.ts
import { buildConfig } from 'payload';
import { payloadSupabaseRLS } from '@sect/payload-supabase-rls/integrations/payload';

export default buildConfig({
// ... other config
plugins: [
payloadSupabaseRLS({
autoEnable: true,
environments: ['development'],
}),
],
});
```

**What happens when you run `pnpm dev` and access a page that triggers a database query:**

1. Next.js starts the development server
2. You navigate to a page that triggers a database query (e.g., `/admin`, `/posts`, etc.)
3. PayloadCMS initializes and runs `pushDevSchema()` (disabling RLS)
4. The plugin automatically re-enables RLS

**Expected console output:**

```
πŸ”„ PayloadSupabaseRLS: Re-enabling RLS...
βœ… PayloadSupabaseRLS: RLS enabled on X tables with Y policies
```

This happens once per server startup when you first access a page that triggers a database query. Your database is now secure! πŸŽ‰

## Current Limitations

### What This Implementation Provides

βœ… **Enabled Features**:

- **PayloadCMS Admin**: Full access via service role (bypasses RLS)
- **PayloadCMS REST API**: Works normally (uses service role)
- **PayloadCMS GraphQL API**: Works normally (uses service role)
- **Database Security**: All tables protected from direct PostgREST access
- **Automatic Management**: RLS automatically restored after schema changes

### What This Implementation Restricts

❌ **Restricted Supabase Features**:

**1. Supabase Auth Integration**

- `auth.uid()` returns NULL for all policies
- Frontend authentication via Supabase Auth is not functional
- JWT-based row-level access control unavailable

```typescript
// ❌ This won't work with current RLS policies
const { data } = await supabase.auth.signIn({ email, password });
// User can authenticate, but database access is denied
```

**2. PostgREST API (Supabase Client)**

- All direct database queries from frontend are blocked
- `supabase.from('table')` operations will fail with permission errors

```typescript
// ❌ All these queries are denied by RLS
await supabase.from('posts').select('*'); // Blocked
await supabase.from('media').insert({...}); // Blocked
await supabase.from('users').update({...}); // Blocked
```

**3. Realtime Subscriptions**

- Real-time database change notifications unavailable
- WebSocket subscriptions blocked by RLS policies

```typescript
// ❌ Realtime subscriptions don't work
supabase
.channel('posts')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'posts',
},
payload => {
// This callback will never fire
},
)
.subscribe();
```

**4. Public Content Access**

- Cannot implement public read-only content (e.g., published blog posts)
- All content requires service role access
- Frontend must proxy all queries through PayloadCMS API

### Use Case Suitability

**βœ… Ideal For**:

- Using PayloadCMS exclusively as your CMS
- All database access goes through PayloadCMS APIs
- No need for direct Supabase client access from frontend
- Prioritizing maximum database security

**⚠️ Not Suitable For**:

- Building frontend apps with Supabase Auth + direct database access
- Real-time collaborative features
- Public content with mixed authentication (some public, some private)
- Leveraging Supabase's PostgREST API features

### Future Improvements

We are exploring more flexible RLS strategies that would enable:

- Gradual RLS with table-specific policies
- Supabase Auth integration
- Public content with authenticated user access
- Real-time subscriptions for allowed content

See [GitHub Issues](https://github.com/sectsect/payload-supabase-rls/issues) for roadmap and discussions.

## Advanced Usage

### CLI Tool

Use the command-line interface for manual RLS management or in CI/CD pipelines:

```bash
# Enable RLS on all tables
npx payload-supabase-rls enable

# Verify RLS status (detailed report)
npx payload-supabase-rls verify

# Quick status check
npx payload-supabase-rls status
```

Example output for status command

```bash
$ npx payload-supabase-rls status

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Table β”‚ RLS β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ posts β”‚ βœ“ Enabledβ”‚
β”‚ posts_rels β”‚ βœ“ Enabledβ”‚
β”‚ media β”‚ βœ“ Enabledβ”‚
β”‚ users β”‚ βœ“ Enabledβ”‚
β”‚ payload_preferences β”‚ βœ“ Enabledβ”‚
β”‚ payload_migrations β”‚ βœ“ Enabledβ”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Summary: 26/26 tables have RLS enabled βœ“
```

Example output for verify command

```bash
$ npx payload-supabase-rls verify

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
RLS Verification Report
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

πŸ“Š Summary:
Total tables: 26
βœ… RLS enabled: 26
❌ RLS disabled: 0

πŸ“‹ Detailed Table Status:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Table Name β”‚ RLS β”‚ Policies β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ posts β”‚ βœ… Yes β”‚ βœ… 4 β”‚
β”‚ posts_page β”‚ βœ… Yes β”‚ βœ… 4 β”‚
β”‚ media β”‚ βœ… Yes β”‚ βœ… 4 β”‚
β”‚ users β”‚ βœ… Yes β”‚ βœ… 4 β”‚
β”‚ payload_preferences β”‚ βœ… Yes β”‚ βœ… 4 β”‚
β”‚ payload_migrations β”‚ βœ… Yes β”‚ βœ… 4 β”‚
... (26 tables total)
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

βœ… All tables have RLS enabled!
βœ… All tables with policies have the correct deny-all configuration (4 policies each).

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```

**Optional**: Add npm scripts to your project for convenience:

```json
{
"scripts": {
"rls:enable": "payload-supabase-rls enable --verbose",
"rls:verify": "payload-supabase-rls verify",
"rls:status": "payload-supabase-rls status"
}
}
```

### Programmatic API

Use the programmatic API for custom integrations or other frameworks:

```typescript
import { enableRLS, verifyRLS } from '@sect/payload-supabase-rls';

// Enable RLS
const result = await enableRLS({
connectionString: process.env.DATABASE_URI,
verbose: true,
});

if (result.success) {
console.log(`Processed ${result.tablesProcessed} tables`);
}

// Verify RLS
const verification = await verifyRLS({
connectionString: process.env.DATABASE_URI,
});
```

## Configuration

### PayloadCMS Plugin Options

```typescript
interface PayloadSupabaseRLSOptions {
autoEnable?: boolean; // default: true
rlsConfig?: Partial>;
environments?: string[]; // default: ['development']
logging?: boolean; // default: true
}
```

### CLI Options

```bash
payload-supabase-rls enable --help

Options:
-c, --connection Database connection string
-v, --verbose Verbose output
-s, --schema Schema name (default: "public")
-r, --roles Target roles (default: "anon,authenticated")
-p, --policy-prefix Policy name prefix (default: "deny_all")
-f, --policy-function Policy function name (default: "deny_all")
```

### Programmatic API Config

```typescript
interface RLSConfig {
connectionString: string;
schema?: string; // default: 'public'
targetRoles?: string[]; // default: ['anon', 'authenticated']
policyPrefix?: string; // default: 'deny_all'
verbose?: boolean; // default: false
excludePatterns?: string[]; // default: ['pg_%', 'sql_%']
policyFunction?: string; // default: 'deny_all'
}
```

## API Reference

### `enableRLS(config: RLSConfig): Promise`

Enable RLS on all tables in the database.

**Returns**:

```typescript
interface RLSOperationResult {
success: boolean;
tablesProcessed: number;
policiesCreated: number;
errors?: string[];
}
```

### `verifyRLS(config): Promise`

Verify RLS configuration on all tables.

**Returns**:

```typescript
interface VerificationResult {
totalTables: number;
protectedTables: number;
unprotectedTables: number;
tables: TableStatus[];
}
```

### `getRLSStatus(config): Promise`

Get quick RLS status for all tables.

**Returns**:

```typescript
interface RLSStatusResult {
tables: TableRLSStatus[];
totalTables: number;
enabledTables: number;
disabledTables: number;
}
```

## Workflows and Supabase CLI Integration

For detailed information on environment-specific workflows and Supabase CLI integration, see [WORKFLOWS.md](./packages/payload-supabase-rls/docs/WORKFLOWS.md).

Topics covered:

- Development vs Staging/Production workflows
- Supabase CLI integration strategies
- Migration creation from plugin state
- Environment configuration

## Troubleshooting

If you encounter any issues, see [TROUBLESHOOTING.md](./packages/payload-supabase-rls/docs/TROUBLESHOOTING.md) for common problems and solutions.

Topics covered:

- Plugin not re-enabling RLS
- RLS disabled after `supabase db reset`
- Policy name conflicts
- Connection issues
- Verification failures
- And more

## Development

This is a Turborepo monorepo containing:

- `packages/payload-supabase-rls` - Main package
- `apps/supabase-payload` - Demo app showcasing PayloadCMS + Next.js 15 + Supabase integration with `@sect/payload-supabase-rls` plugin

### Setup

```bash
pnpm install
pnpm build
```

### Development Workflow

```bash
# Build all packages
pnpm build

# Run tests
pnpm test

# Type checking
pnpm type-check

# Linting
pnpm lint
pnpm lint:fix

# Run demo app
cd apps/supabase-payload
pnpm dev
```

## License

MIT

✌️



A little project by @sectsect