Skip to content
Search

PostgreSQL:RowSecurityPolicies

lastmod 2026-03-20created 2023-07-0412 edits

CREATE POLICY syntax

CREATE POLICY 이름 ON 테이블이름
    [ AS { PERMISSIVE | RESTRICTIVE } ]
    [ FOR { ALL | SELECT | INSERT | UPDATE | DELETE } ]
    [ TO { 롤이름 | PUBLIC | CURRENT_USER | SESSION_USER } [, ...] ]
    [ USING ( 사용식 ) ]
    [ WITH CHECK ( 검사식 ) ]

The CREATE POLICY command defines a new row-level security policy for a table.

To use this command, row-level security policies must first be enabled among the table's attributes.

A policy allows select, insert, update, and delete operations only on the rows that pass the policy expression check.

  • Checks on data already in the table (SELECT, UPDATE, DELETE) use the expression given in the USING option,
  • and checks on data newly stored by an INSERT or UPDATE command use the expression given in the WITH CHECK option.
  • If the USING expression evaluates to true, the row is shown to the user; if false or null, it is not shown.
  • If the WITH CHECK expression evaluates to true, the row is inserted or updated; if false or null, the operation is treated as an error.

When processing an INSERT or UPDATE statement, the WITH CHECK expression runs after BEFORE triggers are processed and before the actual data is changed. That is, if a BEFORE ROW trigger modifies the data, the result of the check expression may change. WITH CHECK expressions are evaluated before any constraint checks.

Policy names are scoped per table. The same policy name can be used on multiple tables, and each one defines the policy for its own table.

A policy can be defined for specific commands or specific roles. If nothing is specified, it applies to all commands and all roles. Multiple policies can be defined at once with a single command.

Because a single policy can specify both USING and WITH CHECK clauses for ALL or UPDATE commands, when there is no WITH CHECK clause, the condition given in USING is used both when reading data and when modifying data.

If a table has row-level security enabled but no policies defined, the table's default security policy is "deny all". Data can be neither read nor modified.

USING vs WITH CHECK

USING

WITH CHECK

Target

Existing row (OLD)

New row (NEW)

Role

Whether this row can be seen

Whether this row may be written

Applies to

SELECT, UPDATE, DELETE

INSERT, UPDATE

On failure

Row is hidden (silent)

Error raised (exception)

Application by command

Command

USING

WITH CHECK

SELECT

Filters rows that can be read

—

INSERT

—

Validates the row to insert

UPDATE

Filters rows to update (OLD)

Validates the updated row (NEW)

DELETE

Filters rows to delete

—

Why UPDATE uses both

UPDATE is a two-step operation: "read the existing row → write the new row".

Example: org_members UPDATE policy

CREATE POLICY "org_members_update_permission"
    ON public.org_members
    FOR UPDATE
    -- 1단계: 이 row를 수정할 수 있는가? (OLD 기준)
    USING (public.has_org_perm(org_id, 'members:update'))
    -- 2단계: 수정 결과가 허용되는가? (NEW 기준)
    WITH CHECK (
        role_id != (SELECT id FROM public.org_roles WHERE name = 'owner')
        OR public.has_org_perm(org_id, 'delete')
    );

Step

Clause

Row referenced

What is checked

Step 1

USING

OLD

Whether the user has the members:update permission

Step 2

WITH CHECK

NEW

Assigning the owner role requires the delete permission (= owner level)

FOR ALL policies

FOR ALL applies to all of SELECT, INSERT, UPDATE, and DELETE. Even if the condition is the same, you must specify both.

CREATE POLICY "org_perms_manage_admin"
    ON public.org_perms
    FOR ALL
    USING ((SELECT public.is_admin()))
    WITH CHECK ((SELECT public.is_admin()));

If omitted

Result

Only USING written

No WITH CHECK for INSERT → inserts blocked

Only WITH CHECK written

No USING for SELECT/DELETE → reads/deletes blocked

Behavior when WITH CHECK is omitted

If WITH CHECK is omitted, the USING condition is automatically applied as WITH CHECK.

-- 아래 두 정책은 동일하게 동작한다
CREATE POLICY "explicit"
    ON public.orgs FOR UPDATE
    USING (public.has_org_perm(id, 'update'))
    WITH CHECK (public.has_org_perm(id, 'update'));

CREATE POLICY "implicit"
    ON public.orgs FOR UPDATE
    USING (public.has_org_perm(id, 'update'));
    -- WITH CHECK 생략 → USING 조건이 자동 적용

However, to apply different conditions to OLD and NEW in UPDATE, you must write them separately.

Supabase example

If you created a table like this:

create table public.profiles (
  id uuid not null references auth.users on delete cascade,
  first_name text,
  last_name text,

  primary key (id)
);

RLS can be enabled like this:

alter table public.profiles enable row level security;

Set up RLS policies like this:

create policy "Public profiles are viewable by everyone."
  on profiles for select
  using ( true );

create policy "Users can insert their own profile."
  on profiles for insert
  with check ( auth.uid() = id );

create policy "Users can update own profile."
  on profiles for update
  using ( auth.uid() = id );

For private access (only the user who owns the data should be able to read it), you only need to change the select query above:

create policy "Profiles are viewable by users who created them."
  on profiles for select
  using ( auth.uid() = id );

If you also want owners to be able to delete their data:

create policy "Individuals can delete their own posts."
  on posts for delete
  using ( auth.uid() = id );

What's nice about this pattern? You can now query this table through the API without including data filters in your API queries. The policies take care of it.

// 사용자가 로그아웃한 동안에는 아무것도 반환하지 않습니다.
const { data } = await supabase.from('profiles').select('id, username, avatar_url, website')

// 사용자가 로그인한 후에는 로그인한 사용자의 데이터만 반환됩니다. 위 경우 단일 행만 반환.
const { error } = await supabase.auth.signIn({ email })
const { data: profile } = await supabase
  .from('profiles')
  .select('id, username, avatar_url, website')

INFORMATION

Security is handled at the database level by RLS policies. Since the policies restrict which rows are returned, no filter on ID is needed. Depending on table size and query, you may still add filters for performance. Postgres can use the filters to build more efficient queries.

Limiting the number of rows

Removing a policy

DROP POLICY "Admin can control admin records" ON public.admins;

PostgreSQL RLS infinite recursion error (42P17)

When querying a table with RLS policies applied, the following error may be printed:

[42P17] ERROR: infinite recursion detected in policy for relation "admins"

If an RLS policy references its own table, the same RLS policy is evaluated again on that reference, causing infinite recursion.

SELECT from admins → RLS 평가 → SELECT from admins → RLS 평가 → ...

The problematic policy:

CREATE POLICY "Admin can control admin records" ON public.admins
    FOR ALL
    USING (
        EXISTS (
            SELECT 1 FROM public.admins  -- 자기 자신을 참조 → 무한 재귀
            WHERE user_id = auth.uid()
        )
    );

The fix is to create a SECURITY DEFINER function and use it in the policy. A SECURITY DEFINER function runs with the privileges of the function owner (superuser), so it bypasses RLS.

-- 1. SECURITY DEFINER 함수 생성
CREATE OR REPLACE FUNCTION public.is_admin()
    RETURNS BOOLEAN
    LANGUAGE plpgsql
    SECURITY DEFINER
AS $$
BEGIN
    RETURN EXISTS (
        SELECT 1 FROM public.admins
        WHERE user_id = auth.uid()
    );
END;
$$;

-- 2. 정책에서 함수 호출
CREATE POLICY "Admin can control admin records" ON public.admins
    FOR ALL
    USING (public.is_admin())
    WITH CHECK (public.is_admin());
SELECT from admins → RLS 평가 → is_admin() (SECURITY DEFINER, RLS 우회) → 정상 반환

  • If a policy is already deployed, remove it first: DROP POLICY "정책명" ON 테이블;
  • The SECURITY DEFINER function must be created before the policy.

Using security definer functions

You can use security definer functions inside Policies. This is useful in a many-to-many relationships, and important for performance. Following the teams and members example from above, this example shows how you can use the security definer function in combination with a policy to control access to the members table.

-- 1. Create a table of teams
create table teams (
  id serial primary key,
  name text
);

-- 2. Create many to many join
create table members (
  team_id bigint references teams,
  user_id uuid references auth.users
);

-- 2.  Enable RLS
alter table teams enable row level security;
alter table members enable row level security;

-- 3.  Create security definer function, which should be run as "postgres"
create function private.get_teams_for_authenticated_user()
returns setof bigint
language sql
security definer -- 실행 권한을 실행한 측이 아닌 함수 정의한 측(definer)로 지정.
set search_path = public -- 검색 경로 'public' schema 로 제한
stable -- 내용을 수정하지 않는다.
as $$
  select team_id
  from members
  where user_id = auth.uid()
$$;

-- 4. Create Policy
create policy "Team members can update team members if they belong to the team."
on members
for all using (
  team_id in (
    select private.get_teams_for_authenticated_user()
  )
);

Auto-enable RLS for new tables

If you want RLS enabled automatically for new tables, you can create an event trigger that runs after table creation. This uses a Postgres event trigger to call ALTER TABLE ... ENABLE ROW LEVEL SECURITY on each newly created table.

CREATE OR REPLACE FUNCTION rls_auto_enable()
RETURNS EVENT_TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = pg_catalog
AS $$
DECLARE
  cmd record;
BEGIN
  FOR cmd IN
    SELECT *
    FROM pg_event_trigger_ddl_commands()
    WHERE command_tag IN ('CREATE TABLE', 'CREATE TABLE AS', 'SELECT INTO')
      AND object_type IN ('table','partitioned table')
  LOOP
     IF cmd.schema_name IS NOT NULL AND cmd.schema_name IN ('public') AND cmd.schema_name NOT IN ('pg_catalog','information_schema') AND cmd.schema_name NOT LIKE 'pg_toast%' AND cmd.schema_name NOT LIKE 'pg_temp%' THEN
      BEGIN
        EXECUTE format('alter table if exists %s enable row level security', cmd.object_identity);
        RAISE LOG 'rls_auto_enable: enabled RLS on %', cmd.object_identity;
      EXCEPTION
        WHEN OTHERS THEN
          RAISE LOG 'rls_auto_enable: failed to enable RLS on %', cmd.object_identity;
      END;
     ELSE
        RAISE LOG 'rls_auto_enable: skip % (either system schema or not in enforced list: %.)', cmd.object_identity, cmd.schema_name;
     END IF;
  END LOOP;
END;
$$;

DROP EVENT TRIGGER IF EXISTS ensure_rls;
CREATE EVENT TRIGGER ensure_rls
ON ddl_command_end
WHEN TAG IN ('CREATE TABLE', 'CREATE TABLE AS', 'SELECT INTO')
EXECUTE FUNCTION rls_auto_enable();

See also

Favorite site

author Zer0 sitemap