Extension Tables, Expo Configs, and Static MCP Scanners in supabase-rls-check 0.6.3
After shipping supabase-rls-check 0.6.0 on November 11, 2026, I ran into three edge cases across PostgreSQL system catalogs, React Native build files, and Model Context Protocol (MCP) directory scanners. Versions 0.6.1 through 0.6.3 address those three cases without adding network calls to the analyzer. 1. Extension-owned tables in public where ENABLE ROW LEVEL SECURITY fails Rule rls_disabled flags tables in the public schema that have Row Level Security turned off, because PostgREST exposes the public schema to the anon and authenticated roles by default. For ordinary application tables, the remediation template in fix.sql suggests enabling RLS: ALTER TABLE public.my_table ENABLE ROW LEVEL SECURITY; That remediation fails when a developer enables an extension such as PostGIS inside the public schema. PostGIS creates public.spatial_ref_sys owned by the bootstrap superuser (supabase_admin). When a project owner runs ALTER TABLE public.spatial_ref_sys ENABLE ROW LEVEL SECURITY in the Supabase SQL Editor, PostgreSQL rejects the statement with ERROR: must be owner of table spatial_ref_sys (discussed in Supabase GitHub Discussion #52767). PostgreSQL records which objects belong to an extension in pg_catalog.pg_depend with deptype = 'e'. In version 0.6.1, I updated the rls_disabled branch of check.sql to join pg_depend and pg_extension through a LEFT JOIN LATERAL subquery and populate the basis column when a table belongs to an extension: SELECT 'rls_disabled'::text AS rule, n.nspname::text AS schema_name, c.relname::text AS object_name, ''::text AS policy_name, coalesce('owned by extension ' || ext.extname, '')::text AS basis FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace LEFT JOIN LATERAL ( SELECT e.extname FROM pg_catalog.pg_depend d JOIN pg_catalog.pg_extension e ON e.oid = d.refobjid WHERE d.classid = 'pg_catalog.pg_class'::pg_catalog.regclass AND d.objid = c.oid AND d.objsubid = 0 AND d.refclassid = 'pg_catalog.pg_extension'::pg_catalog.regclass AND d.deptype = 'e' ORDER BY e.extname LIMIT 1 ) ext ON true WHERE n.nspname = 'public' AND c.relkind IN ('r', 'p') AND NOT c.relrowsecurity When basis starts with owned by extension, fix.sql and the HTML and Markdown reports skip the ENABLE ROW LEVEL SECURITY statement. Instead, they recommend moving the extension into a dedicated extensions schema (CREATE SCHEMA IF NOT EXISTS extensions; ALTER EXTENSION postgis SET SCHEMA extensions;) or revoking API role grants if the extension does not support SET SCHEMA. Because basis is part of the 5-tuple identity key used by supabase-rls-check diff, a baseline exported before 0.6.1 records spatial_ref_sys with an empty basis. Re-exporting the baseline once updates the key so subsequent CI runs stay quiet. 2. Scanning Expo config files and skipping password placeholders Mobile teams using Supabase with React Native and Expo often store configuration values inside app.json, app.config.js, app.config.ts, or eas.json build profiles. Every key placed under expo.extra in app.json or env in eas.json can end up bundled into the client binary. In version 0.6.1, I expanded supabase-rls-check scan-env to check app.json, app.config.*, and eas.json for hardcoded service_role JWTs and sb_secret_ keys under service_role_in_client_code. I also added the Critical rule db_url_in_client_code, which checks client source files and mobile config files for postgresql:// or postgres:// connection strings containing a password. The scanner skips build output directories such as web-build, android/app/build, and ios/build, and it never flags anon or sb_publishable_ keys. Adding connection string checks exposed a false positive in coding agent configuration files (.cursor/mcp.json, .mcp.json, and .claude/settings.json). Developers who commit shared MCP configurations often pass environment variable placeholders in the URL: { "mcpServers": { "postgres": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-postgres", "postgresql://postgres:${DB_PASSWORD}@db.example.supabase.co:5432/postgres"] } } } Version 0.6.1 checks the extracted password segment before reporting a finding. If the password is an unexpanded placeholder such as ${DB_PASSWORD} or $DB_PASSWORD, the scanner ignores it, while real inline passwords stay masked and reported as Critical findings. 3. Making MCP tools visible to static directory scanners supabase-rls-check ships an optional stdio MCP server (pip install "supabase-rls-check[mcp]") with four local tools: analyze_sql_export, explain_rule, scan_public_env, and diff_results. None of these tools accept database credentials or make network requests. When I submitted the server to MCP directories and ran static trust checkers (such as MCP Trust Checker on mcprush), two packaging issues surfaced: Directories using the newer MCP specification check every tool for four boolean safety hints (readOnlyHint, destructiveHint, idempotentHint, and openWorldHint) alongside a human-readable title. Tools missing those annotations receive warnings or lower trust scores because clients cannot tell whether a tool mutates external state. Static AST scanners look for direct server.add_tool(...) calls or @server.tool() decorators. Earlier versions registered tools in a loop using the curried call server.tool()(fn), which caused static analyzers to report zero discovered tools even though the server worked at runtime. In versions 0.6.2 and 0.6.3, I bumped the optional mcp extra to mcp>=1.10, attached ToolAnnotations to all four tools, and switched registration to server.add_tool: def _tool_annotations(title: str) -> Any: """All four tools only read local input: read-only, non-destructive, idempotent, closed-world.""" hints = dict( title=title, readOnlyHint=True, destructiveHint=False, idempotentHint=True, openWorldHint=False, ) try: from mcp.types import ToolAnnotations except ImportError: return hints return ToolAnnotations(**hints) def create_mcp_server(fastmcp_cls: Any = None) -> Any: # ... server = fastmcp_cls('supabase-rls-check') for fn in (analyze_sql_export, explain_rule, scan_public_env, diff_results): title = MCP_TOOL_TITLES[fn.__name__] server.add_tool(fn, title=title, annotations=_tool_annotations(title)) return server A unit test in tests/test_mcp_server.py now inspects the registered tools and asserts all four boolean hints on each one. Boundaries of the checks Extension detection in check.sql only covers tables directly registered in pg_depend with deptype = 'e', and some PostgreSQL extensions do not support ALTER EXTENSION ... SET SCHEMA. Client and config file secret scanning relies on regular expressions over file text, so it will miss keys assembled dynamically at runtime or split across multiple variables. Static catalog and config checks help catch common mistakes early, and they should be paired with integration tests that sign in as two separate tenants and verify row isolation against a real database. Trying version 0.6.3 You can install or upgrade supabase-rls-check from PyPI: pipx install supabase-rls-check supabase-rls-check --version supabase-rls-check sql --print-sql supabase-rls-check scan-env . The source code, PostgreSQL 15 test fixtures (32 catalog findings and 133 unit tests), and changelog are on GitHub at github.com/Tthanh2k6/supabase-rls-check.
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to