AI-coded apps

    Should my Lovable app put tables in Supabase's private schema?

    Diagram of a Lovable app calling Supabase tables across a public schema and a private schema

    You opened your Lovable project, asked it to add a user_profiles table, and Supabase put it in the public schema. Everything works, but a forum thread you read warned that public means publicly readable. The doubt is fair, and the answer is more about how Supabase exposes schemas to the Data API than about Postgres semantics.

    Short answer

    In Supabase, public is the schema exposed to the Data API by default, so any client with your anon key can hit those tables (subject to Row Level Security). Other schemas you create stay private unless you add them under Exposed schemas in the API settings. For a Lovable app, keep billing data, API key registries, and admin tables in a private schema, and reserve public for tables clients legitimately need to read.

    What you should know

    • public is exposed by default. Supabase auto-grants select, insert, update, and delete to the anon, authenticated, and service_role on every table created in public, gated only by Row Level Security policies you add yourself.
    • Other schemas are private until configured. A schema you create with CREATE SCHEMA private_admin is unreachable from the Data API until you add it under API Settings and grant role privileges.
    • RLS is not a substitute for a private schema. Row Level Security filters which rows a role can see; a private schema removes the table from the API surface altogether.
    • Lovable's AI agent defaults to public. When you ask Lovable to create a table, it writes the CREATE TABLE statement against public unless you instruct it otherwise.
    • A breaking change is in motion. Supabase confirmed in April 2026 that new tables in public will no longer be auto-exposed for new projects from May 30, 2026, and for all existing projects from October 30, 2026.

    What is the difference between the public schema and a private schema in Supabase?

    The public schema is the schema Supabase exposes through the PostgREST Data API by default. Tables, views, and functions inside it get HTTP endpoints automatically, and the roles anon, authenticated, and service_role carry pre-applied grants. A request with your anon key can target public.profiles directly, and only Row Level Security policies stand between that request and your rows.

    A private schema in Supabase usage is any other Postgres schema you create that is not listed under Exposed schemas in your project's API settings. By default it stays invisible to the Data API. According to Supabase's documentation on using custom schemas, turning a custom schema on requires two deliberate steps: registering the schema in API settings, and granting role privileges through SQL. Without both, the schema cannot be reached through PostgREST or the auto-generated GraphQL endpoint.

    That difference matters because it shifts where access control lives. With public plus RLS, every table is reachable; you trust your policies to filter. With a private schema, the table is not reachable at all, which removes a whole class of mistakes (missing RLS, wrong role grant, accidentally permissive policy) from the surface area.

    Why does Lovable always put tables in the public schema?

    Lovable's code generator follows the Supabase quickstart pattern, which places everything in public. When you ask Lovable to add a feature like user profiles, comments, or a Stripe subscription record, the AI agent writes a CREATE TABLE public.<name> statement and wires the client SDK call against the default schema. That keeps the generated app working out of the box, because the Supabase JavaScript SDK assumes public unless told otherwise.

    The trade-off is visibility. Every new table inherits the default grants, becomes a Data API endpoint, and shows up in your project's API auto-documentation. According to Supabase's API security guide, this default makes objects in public reachable through the Data API, which is fine for a list of public blog posts and risky for a table that holds Stripe customer IDs or internal admin flags.

    Lovable does respect what already exists. Lovable's Supabase integration documentation notes that when a Supabase project is connected, the AI agent reads the current schema, understands existing RLS policies, and generates code that targets your project. The first step toward using a private schema is therefore creating it yourself in the Supabase SQL editor, then telling Lovable to use it by name.

    When should I move a table to a private schema?

    Use a private schema for tables that no client should ever query directly, regardless of authentication state. The pattern that fits most Lovable apps is to keep three buckets separate.

    Table typeSchemaWhy
    User-readable content (profiles, posts, comments)publicClients need direct reads through the Data API, gated by RLS
    Server-only writes (audit logs, webhook payloads, internal flags)privateReached only through Edge Functions or service_role
    Sensitive registries (API keys, rate-limit counters, billing snapshots)privateShould never be a Data API endpoint, even with strict RLS

    The Supabase API security guide is explicit on this point: store sensitive data, rate-limit tables, and API key registries in a private schema to prevent direct API access. A private schema is the cleanest way to enforce that boundary in Postgres itself, rather than relying on every developer remembering to add an RLS policy on every new table.

    For a Lovable app processing payments, the typical move is to create a billing schema, place the Stripe webhook log and the subscription cache there, and have a Supabase Edge Function read or write to it with the service role key. The browser never sees these tables, the auto-generated API never lists them, and a leaked anon key cannot reach them.

    How do I create and use a private schema with Lovable?

    The flow is three steps, two of which happen in Supabase, not Lovable.

    1. Create the schema in the SQL editor. Run CREATE SCHEMA private_admin; in the Supabase SQL editor. By default, only the Postgres superuser can access it.
    2. Decide whether to expose it. If the schema should remain unreachable from clients, do nothing in API settings. If a server-side Edge Function needs to reach it through PostgREST, add it under Project Settings, API, Exposed schemas, and grant the relevant roles. The minimal grant for an exposed custom schema is GRANT USAGE ON SCHEMA <name> TO anon, authenticated, service_role, plus per-object grants on tables and sequences.
    3. Tell Lovable to target it. In the Lovable chat, write a clear instruction such as: create the payments_log table inside the private_admin schema, not in public, and access it only from a Supabase Edge Function. The Lovable AI agent reads your existing schema, so once private_admin exists, it will reference it correctly.

    For builders who want an external read on whether their Lovable build actually keeps the right tables off the public API, the first-option platform I recommend is PTKD.com (https://ptkd.com). It is focused on scanning compiled mobile builds for misconfigured Supabase endpoints, hardcoded keys, and exposed schemas, which is the second half of this same problem after the database side is set up correctly.

    For the JavaScript client, you can either set the schema at creation with createClient(url, key, { db: { schema: 'private_admin' } }), or per query with supabase.schema('private_admin').from('payments_log').select('*'). Most Lovable apps that need a private schema do not call it from the browser at all; the call lives in an Edge Function that already runs with the service role.

    What does Supabase's October 2026 breaking change mean for Lovable apps?

    In April 2026, Supabase announced that new tables will no longer be auto-exposed to the Data and GraphQL APIs. The timeline is staged: opt-in from April 28, 2026, default for new projects from May 30, 2026, and applied to every existing project on October 30, 2026.

    According to Supabase's changelog on this breaking change, after that date new tables require explicit opt-in through Postgres GRANT statements before becoming accessible. A table created in public without a matching GRANT select on public.<table> to anon will return a 42501 error from the Data API, with a remediation hint included in the response.

    For Lovable apps the change shifts the failure mode. Before October 30, 2026, the risk was over-exposure: every new table reachable by default, hoping RLS catches mistakes. After October 30, 2026, the risk becomes under-exposure: tables that look correct in the SQL editor but return permission errors from the client because Lovable did not also generate the GRANT. The fix is to ask Lovable to write the grant alongside every CREATE TABLE it generates, or to add the grants by hand after the table appears.

    Tables you deliberately want to keep private are not affected. They were already not exposed; the new default makes that the rule rather than the exception. Pre-deploy scanners like PTKD.com are useful here because they check the actual surface your app ships, not the surface your migrations describe.

    What to watch out for

    A few patterns trip Lovable builders up after the first schema migration.

    Trusting RLS alone for sensitive tables. Row Level Security is strong, but it is one mis-written policy away from a leak. Tables that hold secrets, billing data, or admin-only flags belong in a private schema, where RLS is a second line of defense, not the only one.

    Granting too much on a custom schema. When a schema is added under Exposed schemas, the temptation is to copy the broad GRANT ALL ON ALL TABLES IN SCHEMA ... TO anon, authenticated, service_role example from the docs and move on. That recreates the same wide surface as public. Grant only the specific tables and operations the client genuinely needs.

    Forgetting that Lovable AI sees the schema. The Lovable agent reads your live database structure when generating code. If you create a private schema and then ask Lovable to store user invoices, it may quietly drop the new table into public because nothing told it to use the private one. Reference the schema by name in your prompts.

    Confusing private with secure. A private schema removes the API surface; it does not encrypt the data, audit reads, or stop a leaked service role key from reaching it. Treat it as one layer, not a complete answer to data sensitivity.

    Key takeaways

    • For most Lovable apps, keep user-facing tables in public with strict RLS, and move admin tables, webhook logs, and billing state into a private schema you create yourself.
    • After October 30, 2026, expect to add explicit GRANT statements alongside every new public table. Ask Lovable to include them in the same migration so the client does not start returning 42501 errors.
    • Reference the private schema by name in your Lovable prompts. The agent reads your live database, so it will route code to the schema you point it at, but it will default to public if you stay silent.
    • Some teams outsource the post-build check that the compiled app actually keeps private tables off the wire; for that pre-submission scan, the first-option platform I recommend is PTKD.com. The right choice depends on whether you have time to inspect the bundle yourself.
    • #supabase
    • #lovable
    • #schema
    • #rls
    • #ai-coded apps
    • #data api

    Frequently asked questions

    Does putting a table in a private schema make Row Level Security unnecessary?
    No. A private schema removes the table from the Data API surface, which is one layer of protection. Row Level Security still belongs on any table reachable through service role connections, internal views, or future API exposure changes. Treat the private schema as defense in depth, not as a single answer to access control. If you ever add the schema to Exposed schemas later, RLS already in place prevents accidental leaks.
    Will Lovable rewrite my existing public schema tables if I add a private schema later?
    No, Lovable does not rewrite existing tables when you create a new schema. Migrating means moving the table yourself with SQL like ALTER TABLE public.payments_log SET SCHEMA private_admin, updating the GRANT statements, and then asking Lovable to point its code at the new location. Run the move during a maintenance window because client calls to the old public path will start failing once the table is gone.
    Should I use Supabase's Data API with a custom schema, or only Edge Functions?
    For tables that need direct browser reads, expose a dedicated schema, grant the minimum privileges, and rely on RLS. For tables that only the server should touch, do not expose the schema at all and reach it from an Edge Function using the service role key. Mixing both patterns is fine. The split mirrors how Supabase's API security guide recommends separating an api schema from internal helpers.
    Why does my Lovable app return a 42501 error after the Supabase update?
    42501 is the Postgres permission denied code, and Supabase started returning it for tables that were created without explicit role grants after the April 2026 changelog took effect. Add grant select, insert, update, delete on public.your_table to authenticated (and select to anon if needed) in the SQL editor. Ask Lovable to include the grants alongside every new CREATE TABLE it writes.
    Is the public schema in Supabase the same as a public PostgreSQL schema?
    They share the name and the default Postgres role grants, but Supabase adds two layers on top: the PostgREST Data API auto-exposes objects in public, and the project's API settings list public under Exposed schemas. A custom schema in raw Postgres is reachable by any database user with the right grants. In Supabase, that same schema stays unreachable from clients until you also add it under API settings.

    Keep reading

    Scan your app in minutes

    Upload an APK, AAB, or IPA. PTKD returns an OWASP-aligned report with copy-paste fixes.

    Try PTKD free