[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M74F4XNH38Z5DR4GB092XJCD":3,"skill-same:01M74F4XNH38Z5DR4GB092XJCD":52},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":19,"linkOnly":20,"markdownContent":21,"markdownBody":22,"frontMatter":23,"refName":24,"commitSha":25,"skillDirectory":26,"forksCount":14,"repositoryHtmlUrl":27,"repositoryHomepageUrl":6,"ownerHtmlUrl":28,"ownerBlogUrl":29,"ownerDescription":30,"updatedAt":31,"install":32,"resources":45},"01M74F4XNH38Z5DR4GB092XJCD","supabase-rls-policies",null,"Supabase Row-Level Security policy patterns and failure modes -- policy bypass via service-role key in client code, USING-vs-WITH-CHECK confusion, missing INSERT policies that leave write paths unprotected. References GDPR Article 32 (Security of processing) and SOC 2 Trust Services Criteria CC6.1 (Logical and Physical Access Controls) for the least-privilege alignment. Triggers on Supabase table policy authoring (`CREATE POLICY`, `ALTER TABLE ... ENABLE ROW LEVEL SECURITY` references). OWASP ASI03 (Identity & Privilege Abuse) defense.","MILTONADINA\u002FDevOPs","MILTONADINA","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F106546372?v=4","01M74F3R2PP4Z04DQMMBHSQAT1","Milton Adina","01M74F3R3CHTKPXJV4RP7RR3W8",0,"MIT","https:\u002F\u002Fgithub.com\u002FMILTONADINA\u002FDevOPs\u002Fblob\u002Fmain\u002Fskills\u002Fstack-specific\u002Fsupabase\u002Frls-policies\u002FSKILL.md",[],[],"2026-11-11 15:11:14",false,"---\nname: supabase-rls-policies\ndescription: Supabase Row-Level Security policy patterns and failure modes -- policy bypass via service-role key in client code, USING-vs-WITH-CHECK confusion, missing INSERT policies that leave write paths unprotected. References GDPR Article 32 (Security of processing) and SOC 2 Trust Services Criteria CC6.1 (Logical and Physical Access Controls) for the least-privilege alignment. Triggers on Supabase table policy authoring (`CREATE POLICY`, `ALTER TABLE ... ENABLE ROW LEVEL SECURITY` references). OWASP ASI03 (Identity & Privilege Abuse) defense.\n---\n\n# Supabase Row-Level Security Policies\n\n> Defends against **OWASP ASI03 (Identity & Privilege Abuse)** in\n> Supabase-backed applications by enforcing least-privilege at the\n> Postgres row level. Supabase's auth model produces a JWT carrying the\n> authenticated user's UUID; PostgREST (Supabase's API layer) propagates\n> that JWT into the `auth.uid()` function inside the database, which\n> RLS policies use to scope queries. Done right, the database refuses\n> to return rows the user shouldn't see -- even if the application\n> layer has a bug. Done wrong, RLS is a false sense of security: the\n> service-role key bypasses RLS entirely, missing policies leave gaps,\n> and USING-vs-WITH-CHECK confusion ships read-only-looking restrictions\n> that allow writes.\n\n**Tradeoff:** Per-row policy enforcement adds Postgres planner overhead\nand shifts authorization logic from the application layer into SQL\npolicies. Worth it: RLS is the database-layer expression of\nleast-privilege that keeps working when the application is compromised,\nwhen a service-role-key bypass leaks, or when a future code path\nforgets to apply the equivalent application-layer check.\n\n## Why this matters\n\nRLS aligns with **GDPR Article 32 (Security of Processing)** --\nspecifically the \"appropriate technical and organisational measures to\nensure a level of security appropriate to the risk\" obligation -- and\n**SOC 2 Trust Services Criterion CC6.1 (Logical and Physical Access\nControls)** -- \"the entity implements logical access security\nsoftware, infrastructure, and architectures over protected information\nassets.\" Postgres RLS is the canonical defense-in-depth layer for both\ncontrols: even if the application is compromised, the database enforces\nrow-level authorization for any identity-scoped data.\n\n---\n\n## Pattern 1 -- Enable RLS on every table holding user-scoped data\n\nThe first move on any table that holds PII, financial data, tenant-\nscoped records, or anything else identity-bound:\n\n```sql\nALTER TABLE public.posts ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.orders ENABLE ROW LEVEL SECURITY;\n```\n\nAfter enabling, the table is inaccessible through the Data API (the\n`anon` and `authenticated` roles) until at least one policy is created.\nThat's the safe default. The table owner (`postgres`, which the SQL\neditor uses) and BYPASSRLS roles such as `service_role` still see every\nrow, so a policy check run as either of them proves nothing. Adding the\n`ENABLE ROW LEVEL SECURITY` without immediately authoring policies is\nacceptable -- it fails closed.\n\nFor every policy you author, document the threat model in a comment\nabove the SQL: who is this policy for, what access does it grant, why\nis the scoping safe?\n\n---\n\n## Pattern 2 -- Authoring policies with USING (read) and WITH CHECK (write)\n\nThe two clauses serve different roles:\n\n- **USING** -- filters rows VISIBLE to the operation (applies to\n  SELECT, UPDATE, DELETE).\n- **WITH CHECK** -- validates rows being WRITTEN by the operation\n  (applies to INSERT, UPDATE).\n\nFor SELECT-only policies, only USING applies. For INSERT-only policies,\nonly WITH CHECK applies. For UPDATE, BOTH apply -- USING filters which\nrows the user can attempt to update (the row must be visible to be\nmodified), and WITH CHECK validates the row's post-update state.\n\n```sql\n-- Read your own posts.\nCREATE POLICY \"select_own_posts\"\nON public.posts\nFOR SELECT\nUSING (author_id = auth.uid());\n\n-- Insert posts where you are the author.\nCREATE POLICY \"insert_own_posts\"\nON public.posts\nFOR INSERT\nWITH CHECK (author_id = auth.uid());\n\n-- Update your own posts, and ensure you're still the author after the update.\nCREATE POLICY \"update_own_posts\"\nON public.posts\nFOR UPDATE\nUSING (author_id = auth.uid())\nWITH CHECK (author_id = auth.uid());\n\n-- Delete your own posts.\nCREATE POLICY \"delete_own_posts\"\nON public.posts\nFOR DELETE\nUSING (author_id = auth.uid());\n```\n\nPer-operation policies are clearer than `FOR ALL` policies because\neach operation's intent is reviewable in isolation. A new contributor\nmodifying one operation's authz can't accidentally widen authz on\nother operations.\n\n---\n\n## Pattern 3 -- Service-role-key usage is server-side only, never shipped to clients\n\nSupabase issues two API keys:\n\n- **Anon key** (`sb_publishable_*` or older `eyJ...` JWT with role `anon`) --\n  safe to ship in client code. Subject to RLS.\n- **Service-role key** (`sb_secret_*` or older `eyJ...` JWT with role\n  `service_role`) -- **BYPASSES RLS ENTIRELY**. Anything done with this\n  key has full database authority.\n\nThe service-role key belongs in server-side environment variables only.\nIt lives in the vault (1Password \u002F AWS Secrets Manager \u002F Doppler), is\nloaded at server boot, and never reaches a Next.js \u002F React \u002F Vue\nclient bundle. The `NEXT_PUBLIC_SUPABASE_*` env-var naming convention\n(`NEXT_PUBLIC_*` is shipped to the browser by Next.js) is exactly the\nnaming you must NOT use for the service-role key.\n\nDiscipline:\n\n```bash\n# .env.local (developer machine -- gitignored, not shipped)\nSUPABASE_SERVICE_ROLE_KEY=sb_secret_AbCdEf...   # NEVER NEXT_PUBLIC_\nNEXT_PUBLIC_SUPABASE_URL=https:\u002F\u002Fabc.supabase.co\nNEXT_PUBLIC_SUPABASE_ANON_KEY=sb_publishable_...\n```\n\nA grep of the client bundle for the service-role key should return\nzero results. Search for the key's value as well as the `sb_secret_`\nprefix, because a legacy service-role JWT starts with the same `eyJ` as\nthe anon key, so no prefix identifies it. Wire it into CI as a separate\nstep after the build, and pass the key as a secret to that step only,\nso the build itself never holds it:\n\n```bash\n: \"${SUPABASE_SERVICE_ROLE_KEY:?provide it as a CI secret so the bundle can be scanned for it}\"\ndirs=\".next\u002Fstatic\"; [ -d public ] && dirs=\"$dirs public\"\nrc=0; grep -rqF -e \"sb_secret_\" -e \"$SUPABASE_SERVICE_ROLE_KEY\" $dirs || rc=$?\n[ \"$rc\" -eq 1 ]  # 1 = not found (pass); 0 = key found or 2 = scan error, e.g. no build output (fail)\n```\n\n---\n\n## Anti-patterns\n\n### Anti-pattern 1 -- Service-role-key bypass shipped to client code\n\n```ts\n\u002F\u002F WRONG (in client-side code shipped to the browser)\nconst supabase = createClient(\n  process.env.NEXT_PUBLIC_SUPABASE_URL!,\n  process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY!,  \u002F\u002F exposes ALL data!\n);\n```\n\n`NEXT_PUBLIC_*` ships in the browser bundle. The service-role key\nbypasses RLS, so this client has full database authority. Every user\nof the application can read every row in every table. RLS is\nfully defeated.\n\nUse the anon key in clients (`NEXT_PUBLIC_SUPABASE_ANON_KEY`). Reserve\nthe service-role key for server-side code, ideally in a thin API layer\nthat enforces explicit authorization checks before invoking\nservice-role queries.\n\n### Anti-pattern 2 -- A WITH CHECK weaker than USING on UPDATE policies\n\n```sql\n-- WRONG\nCREATE POLICY \"update_own_posts\"\nON public.posts\nFOR UPDATE\nUSING (author_id = auth.uid())\nWITH CHECK (true);  -- the post-update row is unconstrained\n```\n\nUSING filters which rows the user can attempt to update. WITH CHECK\nvalidates the post-update state. A WITH CHECK weaker than USING lets\nthe user update their own post and, in the same update, set\n`author_id` to someone else's UUID, transferring ownership of the\npost. Omitting WITH CHECK is not this bug: Postgres then applies the\nUSING expression to the new row too. Write WITH CHECK explicitly on\nUPDATE policies, normally with the same predicate as USING, so the\npost-update rule is visible in review.\n\n### Anti-pattern 3 -- Missing INSERT policy after enabling RLS\n\n```sql\n-- WRONG: RLS enabled, SELECT policy authored, but no INSERT policy.\nALTER TABLE public.posts ENABLE ROW LEVEL SECURITY;\n\nCREATE POLICY \"select_own_posts\"\nON public.posts\nFOR SELECT USING (author_id = auth.uid());\n\n-- INSERT policy missing -- table is now INSERT-blocked for everyone\n-- except the service-role key. Production write traffic fails.\n```\n\nA common deployment foot-gun: enable RLS, author a SELECT policy, ship\nto staging, discover that no one can write to the table. The reverse\ndirection is worse: in development with the service-role key (which\nbypasses RLS), inserts succeed; in production where the application\nuses the anon key with a per-user JWT, inserts fail because no INSERT\npolicy authorizes them.\n\nAlways author INSERT, UPDATE, DELETE policies alongside SELECT if the\ntable needs writes from non-service-role identities.\n\n---\n\n## ASI\u002FAST mapping\n\nThis skill addresses **canonical OWASP ASI03 (Identity & Privilege\nAbuse)** from `governance\u002Fowasp-asi-2026\u002Fthreats.md`. RLS is the\ndatabase-layer expression of least-privilege per user identity; the\npatterns above ensure that the application's auth.uid() propagation\ninto RLS policies actually scopes data correctly, and the\nanti-patterns enumerate the common ways that scoping leaks.\n\nCompliance alignment:\n\n- **GDPR Article 32 (Security of processing)** -- \"appropriate\n  technical and organisational measures.\" Per-user RLS is canonical.\n- **SOC 2 TSC CC6.1 (Logical and Physical Access Controls)** --\n  \"implements logical access security software... over protected\n  information assets.\" RLS is exactly this control at the database\n  tier.\n\nThese citations are at the article \u002F criterion level which has been\nstable across recent revisions of both standards. Operators verifying\nagainst current text should reference the latest GDPR consolidated\nversion (eur-lex.europa.eu) and the current SOC 2 TSP-100 framework.\n\n---\n\n## Testing\n\nFor every table with RLS enabled, write integration tests that exercise\nboth authorized and unauthorized access:\n\n```sql\n-- Act as an API user: RLS does not apply to the table owner or BYPASSRLS roles.\nSET ROLE authenticated;\n\n-- Connect as user A's JWT and INSERT a post.\nSET request.jwt.claim.sub = 'user-a-uuid';\nINSERT INTO posts (author_id, body) VALUES ('user-a-uuid', 'hello');\n\n-- Connect as user B's JWT and SELECT all posts -- should see ZERO of A's posts.\nSET request.jwt.claim.sub = 'user-b-uuid';\nSELECT count(*) FROM posts;  -- expect 0\n\n-- User B attempts to update A's post -- should fail (USING filters it out).\nUPDATE posts SET body = 'compromised' WHERE author_id = 'user-a-uuid';\n-- 0 rows affected\n```\n\nSupabase's pgTAP testing framework or a custom pytest harness with\n`supabase-py` are both viable. The minimum bar: each table has\npositive (authorized identity can do the operation) and negative\n(unauthorized identity cannot) tests per supported operation.\n\n---\n\n## See also\n\n- `governance\u002Fowasp-asi-2026\u002Fthreats.md` -- ASI03 full text\n- Supabase RLS guide:\n  https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fdatabase\u002Fpostgres\u002Frow-level-security\n- `skills\u002Fstack-specific\u002Fsupabase\u002Frpc-functions\u002FSKILL.md` -- the RPC\n  companion skill (`SECURITY DEFINER` functions can bypass RLS; the\n  RPC skill covers that surface)\n- GDPR Article 32: https:\u002F\u002Fgdpr-info.eu\u002Fart-32-gdpr\u002F\n- SOC 2 TSC framework: https:\u002F\u002Fwww.aicpa-cima.com\u002Ftopic\u002Faudit-assurance\u002Faudit-and-assurance-greater-than-soc-2\n","# Supabase Row-Level Security Policies\n\n> Defends against **OWASP ASI03 (Identity & Privilege Abuse)** in\n> Supabase-backed applications by enforcing least-privilege at the\n> Postgres row level. Supabase's auth model produces a JWT carrying the\n> authenticated user's UUID; PostgREST (Supabase's API layer) propagates\n> that JWT into the `auth.uid()` function inside the database, which\n> RLS policies use to scope queries. Done right, the database refuses\n> to return rows the user shouldn't see -- even if the application\n> layer has a bug. Done wrong, RLS is a false sense of security: the\n> service-role key bypasses RLS entirely, missing policies leave gaps,\n> and USING-vs-WITH-CHECK confusion ships read-only-looking restrictions\n> that allow writes.\n\n**Tradeoff:** Per-row policy enforcement adds Postgres planner overhead\nand shifts authorization logic from the application layer into SQL\npolicies. Worth it: RLS is the database-layer expression of\nleast-privilege that keeps working when the application is compromised,\nwhen a service-role-key bypass leaks, or when a future code path\nforgets to apply the equivalent application-layer check.\n\n## Why this matters\n\nRLS aligns with **GDPR Article 32 (Security of Processing)** --\nspecifically the \"appropriate technical and organisational measures to\nensure a level of security appropriate to the risk\" obligation -- and\n**SOC 2 Trust Services Criterion CC6.1 (Logical and Physical Access\nControls)** -- \"the entity implements logical access security\nsoftware, infrastructure, and architectures over protected information\nassets.\" Postgres RLS is the canonical defense-in-depth layer for both\ncontrols: even if the application is compromised, the database enforces\nrow-level authorization for any identity-scoped data.\n\n---\n\n## Pattern 1 -- Enable RLS on every table holding user-scoped data\n\nThe first move on any table that holds PII, financial data, tenant-\nscoped records, or anything else identity-bound:\n\n```sql\nALTER TABLE public.posts ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.orders ENABLE ROW LEVEL SECURITY;\n```\n\nAfter enabling, the table is inaccessible through the Data API (the\n`anon` and `authenticated` roles) until at least one policy is created.\nThat's the safe default. The table owner (`postgres`, which the SQL\neditor uses) and BYPASSRLS roles such as `service_role` still see every\nrow, so a policy check run as either of them proves nothing. Adding the\n`ENABLE ROW LEVEL SECURITY` without immediately authoring policies is\nacceptable -- it fails closed.\n\nFor every policy you author, document the threat model in a comment\nabove the SQL: who is this policy for, what access does it grant, why\nis the scoping safe?\n\n---\n\n## Pattern 2 -- Authoring policies with USING (read) and WITH CHECK (write)\n\nThe two clauses serve different roles:\n\n- **USING** -- filters rows VISIBLE to the operation (applies to\n  SELECT, UPDATE, DELETE).\n- **WITH CHECK** -- validates rows being WRITTEN by the operation\n  (applies to INSERT, UPDATE).\n\nFor SELECT-only policies, only USING applies. For INSERT-only policies,\nonly WITH CHECK applies. For UPDATE, BOTH apply -- USING filters which\nrows the user can attempt to update (the row must be visible to be\nmodified), and WITH CHECK validates the row's post-update state.\n\n```sql\n-- Read your own posts.\nCREATE POLICY \"select_own_posts\"\nON public.posts\nFOR SELECT\nUSING (author_id = auth.uid());\n\n-- Insert posts where you are the author.\nCREATE POLICY \"insert_own_posts\"\nON public.posts\nFOR INSERT\nWITH CHECK (author_id = auth.uid());\n\n-- Update your own posts, and ensure you're still the author after the update.\nCREATE POLICY \"update_own_posts\"\nON public.posts\nFOR UPDATE\nUSING (author_id = auth.uid())\nWITH CHECK (author_id = auth.uid());\n\n-- Delete your own posts.\nCREATE POLICY \"delete_own_posts\"\nON public.posts\nFOR DELETE\nUSING (author_id = auth.uid());\n```\n\nPer-operation policies are clearer than `FOR ALL` policies because\neach operation's intent is reviewable in isolation. A new contributor\nmodifying one operation's authz can't accidentally widen authz on\nother operations.\n\n---\n\n## Pattern 3 -- Service-role-key usage is server-side only, never shipped to clients\n\nSupabase issues two API keys:\n\n- **Anon key** (`sb_publishable_*` or older `eyJ...` JWT with role `anon`) --\n  safe to ship in client code. Subject to RLS.\n- **Service-role key** (`sb_secret_*` or older `eyJ...` JWT with role\n  `service_role`) -- **BYPASSES RLS ENTIRELY**. Anything done with this\n  key has full database authority.\n\nThe service-role key belongs in server-side environment variables only.\nIt lives in the vault (1Password \u002F AWS Secrets Manager \u002F Doppler), is\nloaded at server boot, and never reaches a Next.js \u002F React \u002F Vue\nclient bundle. The `NEXT_PUBLIC_SUPABASE_*` env-var naming convention\n(`NEXT_PUBLIC_*` is shipped to the browser by Next.js) is exactly the\nnaming you must NOT use for the service-role key.\n\nDiscipline:\n\n```bash\n# .env.local (developer machine -- gitignored, not shipped)\nSUPABASE_SERVICE_ROLE_KEY=sb_secret_AbCdEf...   # NEVER NEXT_PUBLIC_\nNEXT_PUBLIC_SUPABASE_URL=https:\u002F\u002Fabc.supabase.co\nNEXT_PUBLIC_SUPABASE_ANON_KEY=sb_publishable_...\n```\n\nA grep of the client bundle for the service-role key should return\nzero results. Search for the key's value as well as the `sb_secret_`\nprefix, because a legacy service-role JWT starts with the same `eyJ` as\nthe anon key, so no prefix identifies it. Wire it into CI as a separate\nstep after the build, and pass the key as a secret to that step only,\nso the build itself never holds it:\n\n```bash\n: \"${SUPABASE_SERVICE_ROLE_KEY:?provide it as a CI secret so the bundle can be scanned for it}\"\ndirs=\".next\u002Fstatic\"; [ -d public ] && dirs=\"$dirs public\"\nrc=0; grep -rqF -e \"sb_secret_\" -e \"$SUPABASE_SERVICE_ROLE_KEY\" $dirs || rc=$?\n[ \"$rc\" -eq 1 ]  # 1 = not found (pass); 0 = key found or 2 = scan error, e.g. no build output (fail)\n```\n\n---\n\n## Anti-patterns\n\n### Anti-pattern 1 -- Service-role-key bypass shipped to client code\n\n```ts\n\u002F\u002F WRONG (in client-side code shipped to the browser)\nconst supabase = createClient(\n  process.env.NEXT_PUBLIC_SUPABASE_URL!,\n  process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY!,  \u002F\u002F exposes ALL data!\n);\n```\n\n`NEXT_PUBLIC_*` ships in the browser bundle. The service-role key\nbypasses RLS, so this client has full database authority. Every user\nof the application can read every row in every table. RLS is\nfully defeated.\n\nUse the anon key in clients (`NEXT_PUBLIC_SUPABASE_ANON_KEY`). Reserve\nthe service-role key for server-side code, ideally in a thin API layer\nthat enforces explicit authorization checks before invoking\nservice-role queries.\n\n### Anti-pattern 2 -- A WITH CHECK weaker than USING on UPDATE policies\n\n```sql\n-- WRONG\nCREATE POLICY \"update_own_posts\"\nON public.posts\nFOR UPDATE\nUSING (author_id = auth.uid())\nWITH CHECK (true);  -- the post-update row is unconstrained\n```\n\nUSING filters which rows the user can attempt to update. WITH CHECK\nvalidates the post-update state. A WITH CHECK weaker than USING lets\nthe user update their own post and, in the same update, set\n`author_id` to someone else's UUID, transferring ownership of the\npost. Omitting WITH CHECK is not this bug: Postgres then applies the\nUSING expression to the new row too. Write WITH CHECK explicitly on\nUPDATE policies, normally with the same predicate as USING, so the\npost-update rule is visible in review.\n\n### Anti-pattern 3 -- Missing INSERT policy after enabling RLS\n\n```sql\n-- WRONG: RLS enabled, SELECT policy authored, but no INSERT policy.\nALTER TABLE public.posts ENABLE ROW LEVEL SECURITY;\n\nCREATE POLICY \"select_own_posts\"\nON public.posts\nFOR SELECT USING (author_id = auth.uid());\n\n-- INSERT policy missing -- table is now INSERT-blocked for everyone\n-- except the service-role key. Production write traffic fails.\n```\n\nA common deployment foot-gun: enable RLS, author a SELECT policy, ship\nto staging, discover that no one can write to the table. The reverse\ndirection is worse: in development with the service-role key (which\nbypasses RLS), inserts succeed; in production where the application\nuses the anon key with a per-user JWT, inserts fail because no INSERT\npolicy authorizes them.\n\nAlways author INSERT, UPDATE, DELETE policies alongside SELECT if the\ntable needs writes from non-service-role identities.\n\n---\n\n## ASI\u002FAST mapping\n\nThis skill addresses **canonical OWASP ASI03 (Identity & Privilege\nAbuse)** from `governance\u002Fowasp-asi-2026\u002Fthreats.md`. RLS is the\ndatabase-layer expression of least-privilege per user identity; the\npatterns above ensure that the application's auth.uid() propagation\ninto RLS policies actually scopes data correctly, and the\nanti-patterns enumerate the common ways that scoping leaks.\n\nCompliance alignment:\n\n- **GDPR Article 32 (Security of processing)** -- \"appropriate\n  technical and organisational measures.\" Per-user RLS is canonical.\n- **SOC 2 TSC CC6.1 (Logical and Physical Access Controls)** --\n  \"implements logical access security software... over protected\n  information assets.\" RLS is exactly this control at the database\n  tier.\n\nThese citations are at the article \u002F criterion level which has been\nstable across recent revisions of both standards. Operators verifying\nagainst current text should reference the latest GDPR consolidated\nversion (eur-lex.europa.eu) and the current SOC 2 TSP-100 framework.\n\n---\n\n## Testing\n\nFor every table with RLS enabled, write integration tests that exercise\nboth authorized and unauthorized access:\n\n```sql\n-- Act as an API user: RLS does not apply to the table owner or BYPASSRLS roles.\nSET ROLE authenticated;\n\n-- Connect as user A's JWT and INSERT a post.\nSET request.jwt.claim.sub = 'user-a-uuid';\nINSERT INTO posts (author_id, body) VALUES ('user-a-uuid', 'hello');\n\n-- Connect as user B's JWT and SELECT all posts -- should see ZERO of A's posts.\nSET request.jwt.claim.sub = 'user-b-uuid';\nSELECT count(*) FROM posts;  -- expect 0\n\n-- User B attempts to update A's post -- should fail (USING filters it out).\nUPDATE posts SET body = 'compromised' WHERE author_id = 'user-a-uuid';\n-- 0 rows affected\n```\n\nSupabase's pgTAP testing framework or a custom pytest harness with\n`supabase-py` are both viable. The minimum bar: each table has\npositive (authorized identity can do the operation) and negative\n(unauthorized identity cannot) tests per supported operation.\n\n---\n\n## See also\n\n- `governance\u002Fowasp-asi-2026\u002Fthreats.md` -- ASI03 full text\n- Supabase RLS guide:\n  https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fdatabase\u002Fpostgres\u002Frow-level-security\n- `skills\u002Fstack-specific\u002Fsupabase\u002Frpc-functions\u002FSKILL.md` -- the RPC\n  companion skill (`SECURITY DEFINER` functions can bypass RLS; the\n  RPC skill covers that surface)\n- GDPR Article 32: https:\u002F\u002Fgdpr-info.eu\u002Fart-32-gdpr\u002F\n- SOC 2 TSC framework: https:\u002F\u002Fwww.aicpa-cima.com\u002Ftopic\u002Faudit-assurance\u002Faudit-and-assurance-greater-than-soc-2\n",{"name":5,"description":7},"main","5a590e92c8f62b54c96a6928215ac2145b671e03","skills\u002Fstack-specific\u002Fsupabase\u002Frls-policies","https:\u002F\u002Fgithub.com\u002FMILTONADINA\u002FDevOPs","https:\u002F\u002Fgithub.com\u002FMILTONADINA","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fmiltonadina","CS\u002FCybersecurity SWE & former Epidemiologist. Building secure, test-driven full-stack & mobile apps (TS\u002FReact\u002FNode, Java\u002FSpring, Flutter).","2026-11-14 20:03:36",{"source":8,"skillName":5,"commands":33,"prompt":43,"zipAvailable":44},[34,37,40],{"tool":35,"command":36},"npx","npx skills add MILTONADINA\u002FDevOPs --skill supabase-rls-policies",{"tool":38,"command":39},"bunx","bunx skills add MILTONADINA\u002FDevOPs --skill supabase-rls-policies",{"tool":41,"command":42},"pnpm","pnpm dlx skills add MILTONADINA\u002FDevOPs --skill supabase-rls-policies","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FMILTONADINA\u002FDevOPs\u002Fblob\u002Fmain\u002Fskills\u002Fstack-specific\u002Fsupabase\u002Frls-policies\u002FSKILL.md",true,[46],{"path":47,"resourceType":48,"resourceTypeDesc":49,"sizeBytes":50,"sourceHtmlUrl":51},"SKILL.md.bundle","OTHER","其他",9607,"https:\u002F\u002Fgithub.com\u002FMILTONADINA\u002FDevOPs\u002Fblob\u002Fmain\u002Fskills\u002Fstack-specific\u002Fsupabase\u002Frls-policies\u002FSKILL.md.bundle",[53,60,67,74,79,86],{"skillNo":54,"name":55,"nameZh":6,"description":56,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":57,"categories":58,"keywords":59,"publishedAt":19},"01M74F7F3HN6J7CSFHBVQAE9S5","karpathy-guidelines","Behavioral guidelines that reduce common LLM coding mistakes. Apply whenever writing, modifying, or reviewing code. Don't assume requirements, don't overcomplicate, don't refactor unrelated code, and define success criteria before implementing. Use this skill on every non-trivial coding task to avoid bloat, silent assumptions, and completion-bias.","https:\u002F\u002Fgithub.com\u002FMILTONADINA\u002FDevOPs\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fsecurity\u002Fmodified-skill\u002FSKILL.md",[],[],{"skillNo":61,"name":62,"nameZh":6,"description":63,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":64,"categories":65,"keywords":66,"publishedAt":19},"01M74F7D60N9FRWWWHCGZMMB41","unsigned-missing-sig-fixture","Test fixture (F.09) — SKILL.md without an accompanying .sig file. Default install rejects with exit 2; --allow-unsigned --rationale=\"...\" proceeds.","https:\u002F\u002Fgithub.com\u002FMILTONADINA\u002FDevOPs\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Finstall\u002Fsource-skills\u002Funsigned-missing-sig\u002FSKILL.md",[],[],{"skillNo":68,"name":69,"nameZh":6,"description":70,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":71,"categories":72,"keywords":73,"publishedAt":19},"01M74F7BKGX67Q606QPKZV7TQV","unsigned-mismatched-sig-fixture","Test fixture (F.09) — SKILL.md whose accompanying .sig signs different content. NFR-F3 hash check catches this even with --allow-unsigned.","https:\u002F\u002Fgithub.com\u002FMILTONADINA\u002FDevOPs\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Finstall\u002Fsource-skills\u002Funsigned-mismatched-sig\u002FSKILL.md",[],[],{"skillNo":75,"name":55,"nameZh":6,"description":56,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":76,"categories":77,"keywords":78,"publishedAt":19},"01M74F793BYMX98EQC5XP4CDZK","https:\u002F\u002Fgithub.com\u002FMILTONADINA\u002FDevOPs\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Finstall\u002Fsource-skills\u002Fsigned\u002FSKILL.md",[],[],{"skillNo":80,"name":81,"nameZh":6,"description":82,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":83,"categories":84,"keywords":85,"publishedAt":19},"01M74F7481EBD7G5471ZC4S9YB","webhook-idempotency","Defend against double-processing of webhook events across every provider (Stripe, GitHub, Slack, Twilio, SendGrid). Idempotency-key pattern + replay-window enforcement + dedupe-storage strategy. Triggers on any webhook handler authoring (route matching \u002Fwebhook(s)?\u002F, Stripe-Signature \u002F X-Hub-Signature-256 header references). Defense against OWASP ASI02 (Tool Misuse) -- a replayed webhook that re-triggers a destructive tool is the canonical control-flow-level replay attack. Universal across providers; stack-specific signature verification lives in stack-specific skills.","https:\u002F\u002Fgithub.com\u002FMILTONADINA\u002FDevOPs\u002Fblob\u002Fmain\u002Fskills\u002Funiversal\u002Fsecurity\u002Fwebhook-idempotency\u002FSKILL.md",[],[],{"skillNo":87,"name":88,"nameZh":6,"description":89,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":90,"categories":91,"keywords":92,"publishedAt":19},"01M74F71Q6JKRFJ66G5T98KE8H","semgrep-scan","Run Semgrep static analysis to detect insecure code patterns - SQL injection, XSS, path traversal, weak crypto, broken auth, OWASP Top 10 issues. Use on every file change touching security-sensitive code and as part of the tiered security gate. Complements gitleaks (which catches secrets) by catching insecure logic.","https:\u002F\u002Fgithub.com\u002FMILTONADINA\u002FDevOPs\u002Fblob\u002Fmain\u002Fskills\u002Funiversal\u002Fsecurity\u002Fsemgrep-scan\u002FSKILL.md",[],[]]