{"id":35451405,"url":"https://github.com/sectsect/payload-supabase-rls","last_synced_at":"2026-01-13T21:01:30.194Z","repository":{"id":331207450,"uuid":"1125670180","full_name":"sectsect/payload-supabase-rls","owner":"sectsect","description":"Automated Row-Level Security (RLS) management for Supabase PostgreSQL, designed for PayloadCMS and other ORMs.","archived":false,"fork":false,"pushed_at":"2026-01-03T01:09:11.000Z","size":11038,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-04T11:21:56.179Z","etag":null,"topics":["drizzle-orm","payloadcms","postgresql","rls","supabase"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sectsect.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-31T06:15:09.000Z","updated_at":"2026-01-03T01:07:33.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/sectsect/payload-supabase-rls","commit_stats":null,"previous_names":["sectsect/payload-supabase-rls"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/sectsect/payload-supabase-rls","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sectsect%2Fpayload-supabase-rls","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sectsect%2Fpayload-supabase-rls/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sectsect%2Fpayload-supabase-rls/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sectsect%2Fpayload-supabase-rls/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sectsect","download_url":"https://codeload.github.com/sectsect/payload-supabase-rls/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sectsect%2Fpayload-supabase-rls/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28398971,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-13T14:36:09.778Z","status":"ssl_error","status_checked_at":"2026-01-13T14:35:19.697Z","response_time":56,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["drizzle-orm","payloadcms","postgresql","rls","supabase"],"created_at":"2026-01-03T03:14:51.697Z","updated_at":"2026-01-13T21:01:30.188Z","avatar_url":"https://github.com/sectsect.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @sect/payload-supabase-rls\n\n[![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)\n\nAutomated Row-Level Security (RLS) management for Supabase PostgreSQL, designed for PayloadCMS and other ORMs.\n\n## ⚠️ Experimental Status\n\n\u003e [!IMPORTANT]\n\u003e 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.\n\n## Features\n\n- 🔒 **Automatic RLS enablement** - Restore RLS after ORM schema pushes\n- 🔍 **Verification tools** - Detailed RLS status reporting\n- 🎯 **Framework-agnostic** - Works with any PostgreSQL/Supabase project\n- 🔌 **PayloadCMS integration** - Drop-in plugin for automatic management\n- 📦 **CLI \u0026 Programmatic API** - Use as command-line tool or import in code\n- 🛡️ **Security-first** - Deny-all policies for anon/authenticated roles\n\n## Installation\n\n```bash\nnpm install @sect/payload-supabase-rls\n```\n\n## Why This Package?\n\n### Two Approaches to Schema Management\n\nPayloadCMS's `postgresAdapter` offers a `push` option that controls automatic schema synchronization:\n\n**Option 1: Disable Auto-Sync (`push: false`)**\n\n- Requires manual migration management\n- Full control over schema changes\n- No RLS conflicts during development\n\n```typescript\ndb: postgresAdapter({\n  pool: { connectionString: process.env.DATABASE_URI },\n  push: false,  // Disable automatic schema sync\n}),\n```\n\nLearn more: [PayloadCMS Postgres Documentation](https://payloadcms.com/docs/database/postgres)\n\n**Option 2: Enable Auto-Sync with RLS Protection (`push: true`)**\n\n- Automatic schema synchronization during development\n- Faster iteration cycle\n- Requires this plugin to maintain RLS security\n\n### The Problem with Auto-Sync\n\nWhen `push: true` (the default) **in development mode**, PayloadCMS uses Drizzle ORM's `pushDevSchema()` which **explicitly disables RLS** on all tables:\n\n```sql\n-- What Drizzle's pushDevSchema() does\nALTER TABLE your_table DISABLE ROW LEVEL SECURITY;\n```\n\nThis leaves your database vulnerable to direct PostgREST API access.\n\n### The Solution\n\nThis package automatically re-enables RLS after schema sync:\n\n```sql\n-- What this package does\nALTER TABLE your_table ENABLE ROW LEVEL SECURITY;\nCREATE POLICY \"deny_all_select\" ON your_table FOR SELECT TO anon, authenticated USING (false);\nCREATE POLICY \"deny_all_insert\" ON your_table FOR INSERT TO anon, authenticated WITH CHECK (false);\nCREATE POLICY \"deny_all_update\" ON your_table FOR UPDATE TO anon, authenticated USING (false);\nCREATE POLICY \"deny_all_delete\" ON your_table FOR DELETE TO anon, authenticated USING (false);\n```\n\n**What this means**:\n\n- **All tables**: RLS is enabled on every table in your database\n- **All operations**: SELECT, INSERT, UPDATE, DELETE are all protected\n- **Target roles**: `anon` and `authenticated` roles are completely blocked\n- **Bypass roles**: `postgres` and `service_role` bypass RLS (PayloadCMS uses these)\n\n## Get Started\n\n### For PayloadCMS Users (Recommended)\n\nAdd the plugin to your PayloadCMS configuration:\n\n```typescript\n// src/payload.config.ts\nimport { buildConfig } from 'payload';\nimport { payloadSupabaseRLS } from '@sect/payload-supabase-rls/integrations/payload';\n\nexport default buildConfig({\n  // ... other config\n  plugins: [\n    payloadSupabaseRLS({\n      autoEnable: true,\n      environments: ['development'],\n    }),\n  ],\n});\n```\n\n**What happens when you run `pnpm dev` and access a page that triggers a database query:**\n\n1. Next.js starts the development server\n2. You navigate to a page that triggers a database query (e.g., `/admin`, `/posts`, etc.)\n3. PayloadCMS initializes and runs `pushDevSchema()` (disabling RLS)\n4. The plugin automatically re-enables RLS\n\n**Expected console output:**\n\n```\n🔄 PayloadSupabaseRLS: Re-enabling RLS...\n✅ PayloadSupabaseRLS: RLS enabled on X tables with Y policies\n```\n\nThis happens once per server startup when you first access a page that triggers a database query. Your database is now secure! 🎉\n\n## Current Limitations\n\n### What This Implementation Provides\n\n✅ **Enabled Features**:\n\n- **PayloadCMS Admin**: Full access via service role (bypasses RLS)\n- **PayloadCMS REST API**: Works normally (uses service role)\n- **PayloadCMS GraphQL API**: Works normally (uses service role)\n- **Database Security**: All tables protected from direct PostgREST access\n- **Automatic Management**: RLS automatically restored after schema changes\n\n### What This Implementation Restricts\n\n❌ **Restricted Supabase Features**:\n\n**1. Supabase Auth Integration**\n\n- `auth.uid()` returns NULL for all policies\n- Frontend authentication via Supabase Auth is not functional\n- JWT-based row-level access control unavailable\n\n```typescript\n// ❌ This won't work with current RLS policies\nconst { data } = await supabase.auth.signIn({ email, password });\n// User can authenticate, but database access is denied\n```\n\n**2. PostgREST API (Supabase Client)**\n\n- All direct database queries from frontend are blocked\n- `supabase.from('table')` operations will fail with permission errors\n\n```typescript\n// ❌ All these queries are denied by RLS\nawait supabase.from('posts').select('*');        // Blocked\nawait supabase.from('media').insert({...});      // Blocked\nawait supabase.from('users').update({...});      // Blocked\n```\n\n**3. Realtime Subscriptions**\n\n- Real-time database change notifications unavailable\n- WebSocket subscriptions blocked by RLS policies\n\n```typescript\n// ❌ Realtime subscriptions don't work\nsupabase\n  .channel('posts')\n  .on(\n    'postgres_changes',\n    {\n      event: '*',\n      schema: 'public',\n      table: 'posts',\n    },\n    payload =\u003e {\n      // This callback will never fire\n    },\n  )\n  .subscribe();\n```\n\n**4. Public Content Access**\n\n- Cannot implement public read-only content (e.g., published blog posts)\n- All content requires service role access\n- Frontend must proxy all queries through PayloadCMS API\n\n### Use Case Suitability\n\n**✅ Ideal For**:\n\n- Using PayloadCMS exclusively as your CMS\n- All database access goes through PayloadCMS APIs\n- No need for direct Supabase client access from frontend\n- Prioritizing maximum database security\n\n**⚠️ Not Suitable For**:\n\n- Building frontend apps with Supabase Auth + direct database access\n- Real-time collaborative features\n- Public content with mixed authentication (some public, some private)\n- Leveraging Supabase's PostgREST API features\n\n### Future Improvements\n\nWe are exploring more flexible RLS strategies that would enable:\n\n- Gradual RLS with table-specific policies\n- Supabase Auth integration\n- Public content with authenticated user access\n- Real-time subscriptions for allowed content\n\nSee [GitHub Issues](https://github.com/sectsect/payload-supabase-rls/issues) for roadmap and discussions.\n\n## Advanced Usage\n\n### CLI Tool\n\nUse the command-line interface for manual RLS management or in CI/CD pipelines:\n\n```bash\n# Enable RLS on all tables\nnpx payload-supabase-rls enable\n\n# Verify RLS status (detailed report)\nnpx payload-supabase-rls verify\n\n# Quick status check\nnpx payload-supabase-rls status\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eExample output for \u003ccode\u003estatus\u003c/code\u003e command\u003c/summary\u003e\n\n```bash\n$ npx payload-supabase-rls status\n\n┌─────────────────────────────────┬──────────┐\n│ Table                           │ RLS      │\n├─────────────────────────────────┼──────────┤\n│ posts                           │ ✓ Enabled│\n│ posts_rels                      │ ✓ Enabled│\n│ media                           │ ✓ Enabled│\n│ users                           │ ✓ Enabled│\n│ payload_preferences             │ ✓ Enabled│\n│ payload_migrations              │ ✓ Enabled│\n└─────────────────────────────────┴──────────┘\n\nSummary: 26/26 tables have RLS enabled ✓\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eExample output for \u003ccode\u003everify\u003c/code\u003e command\u003c/summary\u003e\n\n```bash\n$ npx payload-supabase-rls verify\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n  RLS Verification Report\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n📊 Summary:\n   Total tables:       26\n   ✅ RLS enabled:      26\n   ❌ RLS disabled:     0\n\n📋 Detailed Table Status:\n   ┌─────────────────────────────────────┬─────────┬──────────┐\n   │ Table Name                          │ RLS     │ Policies │\n   ├─────────────────────────────────────┼─────────┼──────────┤\n   │ posts                               │ ✅ Yes  │ ✅ 4     │\n   │ posts_page                          │ ✅ Yes  │ ✅ 4     │\n   │ media                               │ ✅ Yes  │ ✅ 4     │\n   │ users                               │ ✅ Yes  │ ✅ 4     │\n   │ payload_preferences                 │ ✅ Yes  │ ✅ 4     │\n   │ payload_migrations                  │ ✅ Yes  │ ✅ 4     │\n   ... (26 tables total)\n   └─────────────────────────────────────┴─────────┴──────────┘\n\n✅ All tables have RLS enabled!\n✅ All tables with policies have the correct deny-all configuration (4 policies each).\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n```\n\n\u003c/details\u003e\n\n**Optional**: Add npm scripts to your project for convenience:\n\n```json\n{\n  \"scripts\": {\n    \"rls:enable\": \"payload-supabase-rls enable --verbose\",\n    \"rls:verify\": \"payload-supabase-rls verify\",\n    \"rls:status\": \"payload-supabase-rls status\"\n  }\n}\n```\n\n### Programmatic API\n\nUse the programmatic API for custom integrations or other frameworks:\n\n```typescript\nimport { enableRLS, verifyRLS } from '@sect/payload-supabase-rls';\n\n// Enable RLS\nconst result = await enableRLS({\n  connectionString: process.env.DATABASE_URI,\n  verbose: true,\n});\n\nif (result.success) {\n  console.log(`Processed ${result.tablesProcessed} tables`);\n}\n\n// Verify RLS\nconst verification = await verifyRLS({\n  connectionString: process.env.DATABASE_URI,\n});\n```\n\n## Configuration\n\n### PayloadCMS Plugin Options\n\n```typescript\ninterface PayloadSupabaseRLSOptions {\n  autoEnable?: boolean; // default: true\n  rlsConfig?: Partial\u003cOmit\u003cRLSConfig, 'connectionString'\u003e\u003e;\n  environments?: string[]; // default: ['development']\n  logging?: boolean; // default: true\n}\n```\n\n### CLI Options\n\n```bash\npayload-supabase-rls enable --help\n\nOptions:\n  -c, --connection \u003cstring\u003e      Database connection string\n  -v, --verbose                  Verbose output\n  -s, --schema \u003cstring\u003e          Schema name (default: \"public\")\n  -r, --roles \u003croles\u003e            Target roles (default: \"anon,authenticated\")\n  -p, --policy-prefix \u003cstring\u003e   Policy name prefix (default: \"deny_all\")\n  -f, --policy-function \u003cstring\u003e Policy function name (default: \"deny_all\")\n```\n\n### Programmatic API Config\n\n```typescript\ninterface RLSConfig {\n  connectionString: string;\n  schema?: string; // default: 'public'\n  targetRoles?: string[]; // default: ['anon', 'authenticated']\n  policyPrefix?: string; // default: 'deny_all'\n  verbose?: boolean; // default: false\n  excludePatterns?: string[]; // default: ['pg_%', 'sql_%']\n  policyFunction?: string; // default: 'deny_all'\n}\n```\n\n## API Reference\n\n### `enableRLS(config: RLSConfig): Promise\u003cRLSOperationResult\u003e`\n\nEnable RLS on all tables in the database.\n\n**Returns**:\n\n```typescript\ninterface RLSOperationResult {\n  success: boolean;\n  tablesProcessed: number;\n  policiesCreated: number;\n  errors?: string[];\n}\n```\n\n### `verifyRLS(config): Promise\u003cVerificationResult\u003e`\n\nVerify RLS configuration on all tables.\n\n**Returns**:\n\n```typescript\ninterface VerificationResult {\n  totalTables: number;\n  protectedTables: number;\n  unprotectedTables: number;\n  tables: TableStatus[];\n}\n```\n\n### `getRLSStatus(config): Promise\u003cRLSStatusResult\u003e`\n\nGet quick RLS status for all tables.\n\n**Returns**:\n\n```typescript\ninterface RLSStatusResult {\n  tables: TableRLSStatus[];\n  totalTables: number;\n  enabledTables: number;\n  disabledTables: number;\n}\n```\n\n## Workflows and Supabase CLI Integration\n\nFor detailed information on environment-specific workflows and Supabase CLI integration, see [WORKFLOWS.md](./packages/payload-supabase-rls/docs/WORKFLOWS.md).\n\nTopics covered:\n\n- Development vs Staging/Production workflows\n- Supabase CLI integration strategies\n- Migration creation from plugin state\n- Environment configuration\n\n## Troubleshooting\n\nIf you encounter any issues, see [TROUBLESHOOTING.md](./packages/payload-supabase-rls/docs/TROUBLESHOOTING.md) for common problems and solutions.\n\nTopics covered:\n\n- Plugin not re-enabling RLS\n- RLS disabled after `supabase db reset`\n- Policy name conflicts\n- Connection issues\n- Verification failures\n- And more\n\n## Development\n\nThis is a Turborepo monorepo containing:\n\n- `packages/payload-supabase-rls` - Main package\n- `apps/supabase-payload` - Demo app showcasing PayloadCMS + Next.js 15 + Supabase integration with `@sect/payload-supabase-rls` plugin\n\n### Setup\n\n```bash\npnpm install\npnpm build\n```\n\n### Development Workflow\n\n```bash\n# Build all packages\npnpm build\n\n# Run tests\npnpm test\n\n# Type checking\npnpm type-check\n\n# Linting\npnpm lint\npnpm lint:fix\n\n# Run demo app\ncd apps/supabase-payload\npnpm dev\n```\n\n## License\n\nMIT\n\n\u003cp align=\"center\"\u003e✌️\u003c/p\u003e\n\u003cp align=\"center\"\u003e\n\u003csub\u003e\u003csup\u003eA little project by \u003ca href=\"https://github.com/sectsect\"\u003e@sectsect\u003c/a\u003e\u003c/sup\u003e\u003c/sub\u003e\n\u003c/p\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsectsect%2Fpayload-supabase-rls","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsectsect%2Fpayload-supabase-rls","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsectsect%2Fpayload-supabase-rls/lists"}