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 |
| Step 2 | WITH CHECK | NEW | Assigning the owner role requires the |
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:
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
PostgreSQL RLS infinite recursion error (42P17)
When querying a table with RLS policies applied, the following error may be printed:
If an RLS policy references its own table, the same RLS policy is evaluated again on that reference, causing infinite recursion.
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());
- If a policy is already deployed, remove it first:
DROP POLICY "정책명" ON 테이블; - The
SECURITY DEFINERfunction 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
- PostgreSQL: Documentation: 15: 5.8. Row Security Policies
- Row-security - PostgreSQL wiki
- CREATE POLICY