<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: kohei</title>
    <description>The latest articles on DEV Community by kohei (@0xkohe).</description>
    <link>https://dev.to/0xkohe</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3809660%2F22ed7afd-94fc-406b-bbfd-82d05bf7eaa8.png</url>
      <title>DEV Community: kohei</title>
      <link>https://dev.to/0xkohe</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/0xkohe"/>
    <language>en</language>
    <item>
      <title>Supabase Is Simple, but It Is Not Easy: Practical Patterns for Direct-to-Supabase Architectures</title>
      <dc:creator>kohei</dc:creator>
      <pubDate>Sat, 11 Jul 2026 15:57:36 +0000</pubDate>
      <link>https://dev.to/0xkohe/supabase-is-simple-but-it-is-not-easy-practical-patterns-for-direct-to-supabase-architectures-44ci</link>
      <guid>https://dev.to/0xkohe/supabase-is-simple-but-it-is-not-easy-practical-patterns-for-direct-to-supabase-architectures-44ci</guid>
      <description>&lt;p&gt;Hello, I’m 0xkohe.&lt;/p&gt;

&lt;p&gt;I run multiple workloads on Supabase. Supabase is simple, but it is not necessarily easy. There are also relatively few collections of established practices, so if you build something casually through vibe coding, you can quickly produce a working application while also introducing security risks.&lt;/p&gt;

&lt;p&gt;In this article, I use the term &lt;code&gt;Direct-to-Supabase&lt;/code&gt; to describe an architecture in which the client accesses the Supabase Data API directly. I personally prefer SPAs, so unless otherwise stated, this article assumes an SPA-based frontend.&lt;/p&gt;

&lt;p&gt;Using Supabase well requires a certain amount of computer science knowledge. If you are not confident in your ability to design the security model correctly, choosing a Backend for Frontend (BFF) is usually the safer option.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;My preferred stack is a React SPA on Cloudflare combined with Supabase. It can be operated at a very low cost.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I believe that roughly 80% of a typical service consists of straightforward authorization, writes, and reads. Supabase provides a very fast way to implement that 80% through Direct-to-Supabase access from the client using PostgREST and Row Level Security (RLS).&lt;/p&gt;

&lt;p&gt;When used well, this architecture significantly speeds up implementation and makes it easier to launch, modify, and improve a service. However, if you try to solve the remaining 20% using only the same mechanism, complexity grows rapidly. Extensibility declines, maintenance becomes harder, and the risk of accidentally leaking data increases.&lt;/p&gt;

&lt;p&gt;This article presents my personal collection of practices for avoiding those problems.&lt;/p&gt;

&lt;p&gt;Some of these practices are also useful when Supabase is used behind a BFF.&lt;/p&gt;

&lt;h1&gt;
  
  
  What Does “Simple, but Not Easy” Mean?
&lt;/h1&gt;

&lt;p&gt;A useful analogy is recursion. Recursive code can be concise and structurally simple, but understanding and extending it can still be difficult.&lt;/p&gt;

&lt;p&gt;RLS-based data protection looks simple at first. However, when you try to express too many business rules through RLS alone, complexity increases quickly and mistakes become more likely.&lt;/p&gt;

&lt;h1&gt;
  
  
  Core Principles
&lt;/h1&gt;

&lt;p&gt;In a Direct-to-Supabase architecture, use PostgREST and RLS for straightforward CRUD operations and row-level authorization.&lt;/p&gt;

&lt;p&gt;Do not attempt to express every complex use case entirely through RLS policy expressions.&lt;/p&gt;

&lt;p&gt;Move logic involving multiple tables, state transitions, or external services into RPC functions, views, or Edge Functions.&lt;/p&gt;

&lt;h1&gt;
  
  
  Local Development Environment
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://supabase.com/docs/guides/local-development" rel="noopener noreferrer"&gt;https://supabase.com/docs/guides/local-development&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Start by creating a local environment that you can destroy and recreate as often as necessary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;supabase init &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; supabase start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Seed Data
&lt;/h2&gt;

&lt;p&gt;If you write SQL in &lt;code&gt;./supabase/seed.sql&lt;/code&gt;, Supabase will load that data during the initial startup and whenever you reset the database.&lt;/p&gt;

&lt;h1&gt;
  
  
  Declarative Schemas
&lt;/h1&gt;

&lt;p&gt;I manage nearly the entire database structure through Declarative Schemas.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://supabase.com/docs/guides/local-development/declarative-database-schemas" rel="noopener noreferrer"&gt;https://supabase.com/docs/guides/local-development/declarative-database-schemas&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With Declarative Schemas, you describe the desired final state of the database under &lt;code&gt;supabase/schemas/&lt;/code&gt;, then use &lt;code&gt;supabase db diff&lt;/code&gt; to generate the migration required to reach that state.&lt;/p&gt;

&lt;p&gt;Schema files are evaluated in lexicographical order, so name them in a way that makes dependencies explicit.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tenants&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
    &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;char_length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;between&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

  &lt;span class="n"&gt;created_by&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
    &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="k"&gt;restrict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you create files like this, Supabase compares the desired schema with the current database state and generates a migration file containing the required differences.&lt;/p&gt;

&lt;p&gt;A simple directory structure looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;supabase/
├── config.toml
├── schemas/
│   ├── 00_types.sql
│   ├── 10_tables.sql
│   ├── 20_functions.sql
│   └── 30_policies.sql
├── migrations/
└── seed.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I usually operate projects with a more detailed structure like the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;supabase/schemas/
├── 00_setup/                        # Initial setup for extensions and shared functions
│   └── 001_set_updated_at.sql
├── 01_types/                        # Enums and custom types
│   ├── 004_gender_enum.sql
│   ├── 013_tenant_role_enum.sql
│   └── 015_application_status_enum.sql
├── 02_tables/                       # Table definitions
│   ├── 001_users.sql
│   ├── 007_tenants.sql
│   ├── 008_profiles.sql
│   ├── 020_message_rooms.sql
│   ├── 022_messages.sql
│   └── 023_job_applications.sql
├── 03_functions/                    # Functions and RPCs
│   ├── 001_handle_new_auth_user.sql
│   ├── 002_check_tenant_role.sql
│   ├── 003_check_is_admin.sql
│   └── 008_create_tenant_with_profile_and_staff.sql
├── 04_policies/                     # RLS policies
│   ├── 006_users.sql
│   ├── 007_profiles.sql
│   ├── 018_messages.sql
│   └── 020_job_applications.sql
├── 05_views/                        # Views
│   ├── 001_candidate_list_view.sql
│   ├── 002_profiles_for_tenant_view.sql
│   └── 004_user_messages_view.sql
├── 06_triggers/                     # Triggers
│   ├── 007_profiles.sql
│   └── 013_messages.sql
├── 07_indexes/                      # Indexes
│   ├── 001_profiles.sql
│   ├── 007_message_rooms.sql
│   └── 009_job_applications.sql
└── 08_storage_policies/             # Access control for Storage buckets
    ├── 001_user_icon_policies.sql
    ├── 002_user_resume_policies.sql
    └── 003_tenant_icon_policies.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When using nested directories, define &lt;code&gt;schema_paths&lt;/code&gt; in &lt;code&gt;supabase/config.toml&lt;/code&gt; to make the evaluation order explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[db.migrations]&lt;/span&gt;
&lt;span class="py"&gt;schema_paths&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s"&gt;"./schemas/00_setup/*.sql"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"./schemas/01_types/*.sql"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"./schemas/02_tables/*.sql"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"./schemas/03_functions/*.sql"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"./schemas/04_policies/*.sql"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"./schemas/05_views/*.sql"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"./schemas/06_triggers/*.sql"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"./schemas/07_indexes/*.sql"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"./schemas/08_storage_policies/*.sql"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When migrating an existing project, first dump the current database by following the official procedure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;supabase db dump &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; supabase/schemas/prod.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then split &lt;code&gt;prod.sql&lt;/code&gt; into files based on responsibility.&lt;/p&gt;

&lt;p&gt;AI can be useful as an assistant during this process. For example, you can provide the directory structure above and use a prompt such as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I want to migrate this project to Declarative Schemas. Review the existing migration files and create files under &lt;code&gt;schemas/&lt;/code&gt; until &lt;code&gt;supabase db diff&lt;/code&gt; produces no remaining differences.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;However, always review generated migrations manually.&lt;/p&gt;

&lt;p&gt;Declarative Schemas do not fully track every possible database change. DML, view ownership and privileges, column privileges, schema privileges, and some RLS changes may still require manually written migrations.&lt;/p&gt;

&lt;h1&gt;
  
  
  Generating Types
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://supabase.com/docs/guides/api/rest/generating-types" rel="noopener noreferrer"&gt;https://supabase.com/docs/guides/api/rest/generating-types&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Supabase can generate TypeScript types directly from the database. Import these types from both the frontend and Edge Functions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;supabase gen types typescript &lt;span class="nt"&gt;--local&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; app/database.types.ts

supabase gen types typescript &lt;span class="nt"&gt;--local&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; supabase/functions/_shared/database.types.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows the frontend and Edge Functions to understand the database structure through the type system.&lt;/p&gt;

&lt;p&gt;It also makes compiler-in-the-loop workflows with AI much more effective because references to nonexistent tables, columns, or values can be detected as type errors.&lt;/p&gt;

&lt;p&gt;In CI, verify that generated types are up to date:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;supabase gen types typescript &lt;span class="nt"&gt;--local&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; app/database.types.ts
git diff &lt;span class="nt"&gt;--exit-code&lt;/span&gt; app/database.types.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Database&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;~/database.types&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createClient&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Database&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;supabaseUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;supabaseAnonKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tenant_staffs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;single&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Inferred type of data:&lt;/span&gt;
&lt;span class="c1"&gt;// {&lt;/span&gt;
&lt;span class="c1"&gt;//   id: string;&lt;/span&gt;
&lt;span class="c1"&gt;//   tenant_id: string;&lt;/span&gt;
&lt;span class="c1"&gt;//   name: string;&lt;/span&gt;
&lt;span class="c1"&gt;//   email: string;&lt;/span&gt;
&lt;span class="c1"&gt;//   role: "owner" | "member";&lt;/span&gt;
&lt;span class="c1"&gt;// } | null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Row Level Security
&lt;/h1&gt;

&lt;p&gt;Do not attempt to control every authorization rule through RLS.&lt;/p&gt;

&lt;p&gt;Simple RLS policies can cover roughly 70–80% of the system. If you try to force the remaining 20–30% into RLS as well, complexity rises sharply. The policies become harder to understand, unintended mistakes become more likely, and the risk of data leakage increases.&lt;/p&gt;

&lt;p&gt;This is another example of the 80/20 rule.&lt;/p&gt;

&lt;p&gt;At the same time, the fact that a large part of the system can be protected with simple RLS policies is one of Supabase’s greatest strengths.&lt;/p&gt;

&lt;p&gt;For example, in a multi-tenant system, I limit RLS to rules such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users can operate on records containing their own user ID.&lt;/li&gt;
&lt;li&gt;Users can operate on records belonging to a tenant of which they are a member.&lt;/li&gt;
&lt;li&gt;Administrators can read the relevant records.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When requirements go beyond that level, consider views, RPC functions, or Edge Functions, as described later.&lt;/p&gt;

&lt;p&gt;Use Supabase’s built-in &lt;code&gt;auth.uid()&lt;/code&gt; function to identify the current user.&lt;/p&gt;

&lt;p&gt;The examples below assume that &lt;code&gt;private.current_tenant_id()&lt;/code&gt; and &lt;code&gt;private.is_admin()&lt;/code&gt; are custom helper functions that query membership-related tables.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current_tenant_id&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_admin&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where RLS Becomes Difficult
&lt;/h2&gt;

&lt;p&gt;Typical examples of requirements that make RLS policies complex include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Even within the same tenant, users may only view documents for projects they participate in.&lt;/li&gt;
&lt;li&gt;Confidential documents may only be viewed by specific groups.&lt;/li&gt;
&lt;li&gt;A document must not be shown to users blocked by its creator.&lt;/li&gt;
&lt;li&gt;Temporarily invited users may only access a document before the invitation expires.&lt;/li&gt;
&lt;li&gt;Project managers may view everything, while regular members may only view published content.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once write and delete permissions, additional requirements, and joins are added, readability deteriorates. Maintenance becomes difficult, and extending the policy becomes increasingly risky.&lt;/p&gt;

&lt;p&gt;A policy such as the following can certainly be written, but it is not something I would want to keep expanding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current_tenant_id&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;created_by&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="k"&gt;exists&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project_members&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;project_members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;
      &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;project_members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="k"&gt;exists&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;group_members&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;group_members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;group_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allowed_group_id&lt;/span&gt;
      &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;group_members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;exists&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;blocked_users&lt;/span&gt;
  &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;blocked_users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;blocked_user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;blocked_users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_by&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The situation becomes significantly harder once conditions spanning joins and write authorization are added.&lt;/p&gt;

&lt;p&gt;Suppose we introduce the following requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If a project has been archived, only managers may view its documents.&lt;/li&gt;
&lt;li&gt;Invited collaborators may view a document only while the invitation is valid and their role permits viewing.&lt;/li&gt;
&lt;li&gt;A confidential document may be viewed only if one of the user’s groups has sufficient clearance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;SELECT&lt;/code&gt; policy now effectively contains joins inside a series of existence checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- SELECT policy for documents&lt;/span&gt;
&lt;span class="n"&gt;tenant_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current_tenant_id&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_admin&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;exists&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project_members&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;
      &lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;projects&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;
      &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;
        &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;archived_at&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'manager'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="k"&gt;exists&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;invitations&lt;/span&gt; &lt;span class="n"&gt;inv&lt;/span&gt;
      &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
        &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;invited_user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;role&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'viewer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'editor'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_confidential&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
    &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="k"&gt;exists&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;group_members&lt;/span&gt; &lt;span class="n"&gt;gm&lt;/span&gt;
      &lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;group_clearances&lt;/span&gt; &lt;span class="n"&gt;gc&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;gc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;group_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;group_id&lt;/span&gt;
      &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;gm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;gc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clearance_level&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confidential_level&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;exists&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;blocked_users&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
  &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;blocked_user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_by&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Writes are even more difficult.&lt;/p&gt;

&lt;p&gt;Suppose the rule is: “A document may be edited only by its creator or by an invited collaborator with the &lt;code&gt;editor&lt;/code&gt; role, and only while the project is not archived.”&lt;/p&gt;

&lt;p&gt;An &lt;code&gt;UPDATE&lt;/code&gt; policy must carefully distinguish between &lt;code&gt;USING&lt;/code&gt; and &lt;code&gt;WITH CHECK&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"documents_update"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;documents&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt;
&lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;tenant_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current_tenant_id&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_admin&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="k"&gt;exists&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;projects&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
      &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;
        &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;archived_at&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;created_by&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="k"&gt;exists&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;invitations&lt;/span&gt; &lt;span class="n"&gt;inv&lt;/span&gt;
      &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
        &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;invited_user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'editor'&lt;/span&gt;
        &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;tenant_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current_tenant_id&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'draft'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'published'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even in this example, &lt;code&gt;status in ('draft', 'published')&lt;/code&gt; only prevents values other than &lt;code&gt;draft&lt;/code&gt; and &lt;code&gt;published&lt;/code&gt;. It does not control state transitions such as moving a document from &lt;code&gt;published&lt;/code&gt; back to &lt;code&gt;draft&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you also need to prevent changes to fields such as &lt;code&gt;created_by&lt;/code&gt;, &lt;code&gt;project_id&lt;/code&gt;, &lt;code&gt;allowed_group_id&lt;/code&gt;, or &lt;code&gt;confidential_level&lt;/code&gt;, you must compare the values before and after the update.&lt;/p&gt;

&lt;p&gt;These rules become difficult to read when implemented entirely through RLS. Consider using triggers or RPC functions instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits of RLS in Direct-to-Supabase Architectures
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;RLS controls rows, not individual columns.

&lt;ul&gt;
&lt;li&gt;Consider splitting tables or exposing a view.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Multiple table updates need to be executed as a single transaction.

&lt;ul&gt;
&lt;li&gt;Use an RPC function.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The operation involves external APIs, email, payments, webhooks, or other processing outside the database.

&lt;ul&gt;
&lt;li&gt;Use an Edge Function.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Choosing Between Views, RPC Functions, and Edge Functions
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Views
&lt;/h2&gt;

&lt;p&gt;Use views to create read-oriented representations of data.&lt;/p&gt;

&lt;p&gt;Typical use cases include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Limiting the columns exposed to clients.&lt;/li&gt;
&lt;li&gt;Building a read model that joins multiple tables.&lt;/li&gt;
&lt;li&gt;Intentionally creating a read API that bypasses the RLS behavior of the underlying tables.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By default, PostgreSQL views execute with the privileges of the view owner. In other words, unless a view is configured with &lt;code&gt;security_invoker = true&lt;/code&gt;, it does not simply inherit the RLS behavior of its underlying tables. Instead, data is read using the privileges of the view owner.&lt;/p&gt;

&lt;p&gt;When you rely on this behavior, a view should not be treated as something that accidentally bypasses RLS. It should be designed as an intentional exception API.&lt;/p&gt;

&lt;p&gt;For example, enable RLS on the underlying table and either avoid adding a direct &lt;code&gt;SELECT&lt;/code&gt; policy or keep it extremely restrictive. Then define the permitted columns, joins, and filtering conditions in the view, and grant only the required privilege on that view.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;profiles&lt;/span&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="k"&gt;security&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;view&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;public_profiles&lt;/span&gt;
&lt;span class="k"&gt;as&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;display_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;avatar_url&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;profiles&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="k"&gt;grant&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;public_profiles&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this design, a client querying the underlying &lt;code&gt;public.profiles&lt;/code&gt; table directly receives no rows if no &lt;code&gt;SELECT&lt;/code&gt; RLS policy exists.&lt;/p&gt;

&lt;p&gt;Meanwhile, &lt;code&gt;public.public_profiles&lt;/code&gt;, operating as an owner-privileged view, returns only the explicitly exposed columns and rows.&lt;/p&gt;

&lt;p&gt;If you want a view to inherit the underlying table’s RLS behavior, PostgreSQL 15 and later support &lt;code&gt;security_invoker = true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Personally, I generally do not use views for that case. If all I need is a join that continues to respect the underlying RLS policies, I usually find it simpler to construct the query directly with &lt;code&gt;supabase.js&lt;/code&gt;. In that situation, creating and maintaining a dedicated view provides limited benefit.&lt;/p&gt;

&lt;h2&gt;
  
  
  RPC Functions
&lt;/h2&gt;

&lt;p&gt;Use RPC functions for operations such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Updating multiple tables in a single transaction.&lt;/li&gt;
&lt;li&gt;Restricting which columns may be changed instead of allowing arbitrary table updates.&lt;/li&gt;
&lt;li&gt;Enforcing valid state transitions.&lt;/li&gt;
&lt;li&gt;Checking multiple authorization conditions before writing data.&lt;/li&gt;
&lt;li&gt;Updating columns that clients should not be allowed to modify directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When write operations are centralized in RPC functions, consider revoking direct &lt;code&gt;INSERT&lt;/code&gt; and &lt;code&gt;UPDATE&lt;/code&gt; privileges on the underlying tables and granting clients permission to execute only the relevant RPC functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edge Functions
&lt;/h2&gt;

&lt;p&gt;Use Edge Functions for integrations and processing outside the database, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sending email.&lt;/li&gt;
&lt;li&gt;Processing payments.&lt;/li&gt;
&lt;li&gt;Handling webhooks.&lt;/li&gt;
&lt;li&gt;Calling external APIs.&lt;/li&gt;
&lt;li&gt;Performing operations that require secret keys or the Service Role Key.&lt;/li&gt;
&lt;li&gt;Running workflows that cannot be completed within a database transaction alone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Operations using the Service Role Key can bypass RLS. Therefore, the Edge Function itself must authenticate and authorize the request, and the client must not be able to execute the same privileged operation directly.&lt;/p&gt;

&lt;p&gt;Where practical, I use a framework such as Hono to define multiple routes inside a single function so that one deployed function can handle several related operations.&lt;/p&gt;

&lt;h1&gt;
  
  
  Testing RLS and RPC Functions
&lt;/h1&gt;

&lt;p&gt;Once you have designed the security model, test it.&lt;/p&gt;

&lt;p&gt;Supabase documents the use of pgTAP for testing tables, functions, and RLS policies.&lt;/p&gt;

&lt;p&gt;At a minimum, test cases such as the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A user cannot &lt;code&gt;SELECT&lt;/code&gt; data belonging to another tenant.&lt;/li&gt;
&lt;li&gt;A user cannot &lt;code&gt;INSERT&lt;/code&gt; a row using another tenant’s ID.&lt;/li&gt;
&lt;li&gt;A user cannot change &lt;code&gt;tenant_id&lt;/code&gt; through an &lt;code&gt;UPDATE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Anonymous users cannot access protected resources.&lt;/li&gt;
&lt;li&gt;Regular users cannot execute administrator-only RPC functions.&lt;/li&gt;
&lt;li&gt;Views do not expose private columns.&lt;/li&gt;
&lt;li&gt;Expired invitations do not grant access.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Branches
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://supabase.com/docs/guides/deployment/branching" rel="noopener noreferrer"&gt;https://supabase.com/docs/guides/deployment/branching&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Supabase Branches require a paid plan, but they allow you to create an environment for each pull request.&lt;/p&gt;

&lt;p&gt;Because preview branches are deleted after merging, I use the &lt;code&gt;main&lt;/code&gt; branch as production and keep a persistent &lt;code&gt;develop&lt;/code&gt; branch as staging.&lt;/p&gt;

&lt;h1&gt;
  
  
  Official Supabase Skills
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://supabase.com/docs/guides/ai-tools/ai-skills" rel="noopener noreferrer"&gt;https://supabase.com/docs/guides/ai-tools/ai-skills&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is worth installing Supabase’s official Skills.&lt;/p&gt;

&lt;p&gt;They can help review RLS performance and check implementations against recommended practices.&lt;/p&gt;

&lt;p&gt;To be honest, I do not use MCP very much. If database types are generated from the schema, AI tools can already understand most of the database structure through those generated types.&lt;/p&gt;

&lt;h1&gt;
  
  
  Other Recommendations
&lt;/h1&gt;

&lt;h2&gt;
  
  
  BI Tools
&lt;/h2&gt;

&lt;p&gt;If you need a BI tool, I recommend connecting Google Looker Studio, formerly known as Google Data Studio. It can be used for analytics at no cost.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cloud.google.com/data-studio?hl=en" rel="noopener noreferrer"&gt;https://cloud.google.com/data-studio?hl=en&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Skills
&lt;/h2&gt;

&lt;p&gt;I have also turned the practices in this article into a Skill.&lt;/p&gt;

&lt;p&gt;You can install it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh skill &lt;span class="nb"&gt;install &lt;/span&gt;0xkohe/supabase-skills design-direct-to-supabase
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/0xkohe/supabase-skills" rel="noopener noreferrer"&gt;https://github.com/0xkohe/supabase-skills&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That’s all.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>rls</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Built tasuki — an AI CLI Orchestrator that Seamlessly Hands Off Between Tools</title>
      <dc:creator>kohei</dc:creator>
      <pubDate>Sun, 19 Apr 2026 08:49:32 +0000</pubDate>
      <link>https://dev.to/0xkohe/built-tasuki-an-ai-cli-orchestrator-that-seamlessly-hands-off-between-tools-519o</link>
      <guid>https://dev.to/0xkohe/built-tasuki-an-ai-cli-orchestrator-that-seamlessly-hands-off-between-tools-519o</guid>
      <description>&lt;p&gt;I built and open-sourced &lt;strong&gt;tasuki&lt;/strong&gt;, an AI CLI orchestrator that automatically rotates between &lt;strong&gt;Claude Code / Codex CLI / GitHub Copilot CLI&lt;/strong&gt; based on priority.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/0xkohe/tasuki" rel="noopener noreferrer"&gt;https://github.com/0xkohe/tasuki&lt;/a&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkcw64lvyid0gouvx7wpy.png" alt="Terminal screenshot showing tasuki orchestrating multiple AI CLIs and switching providers based on usage" width="800" height="367"&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Motivation
&lt;/h2&gt;

&lt;p&gt;I’m the kind of person who prefers subscribing to multiple AI tools at $20–$30 each rather than committing to a single $100–$200 plan. I want to explore different tools and continuously feel the differences between models.&lt;/p&gt;

&lt;p&gt;However, doing this means hitting usage limits fairly quickly, which interrupts workflow and requires manual switching — something I found annoying.&lt;/p&gt;

&lt;p&gt;Each AI has its strengths — Codex for review, Claude for conversation, etc. But unless you’re extremely opinionated, there’s value in constantly rotating through tools and experiencing their evolution. If you stick to one, you may miss when another becomes better at something.&lt;/p&gt;

&lt;p&gt;The switching decision itself is trivial labor — so I wanted to automate it.&lt;br&gt;
That’s why I built tasuki.&lt;/p&gt;

&lt;p&gt;Instead of treating rate limits as a constraint, I treat them as an opportunity to move to another model.&lt;/p&gt;




&lt;h2&gt;
  
  
  Intended Workflow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Prioritize Claude Code and Codex CLI (5-hour windows)&lt;/strong&gt;&lt;br&gt;
Since their reset cycles are short, it’s more cost-efficient to exhaust them first.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Once both 5-hour windows are exhausted, switch to GitHub Copilot CLI&lt;/strong&gt;&lt;br&gt;
Copilot operates on a monthly quota, so it works well as a “bridge.”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;When Claude / Codex reset, switch back automatically&lt;/strong&gt;&lt;br&gt;
Monthly quota should be preserved as much as possible.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Native UI stays intact&lt;/strong&gt;&lt;br&gt;
Each CLI runs inside a wrapped PTY, preserving the original interactive experience.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automatic switching based on usage&lt;/strong&gt;&lt;br&gt;
Adapters monitor CLI output streams and trigger a handoff when a threshold (default: 95%) is reached.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Context-aware handoff&lt;/strong&gt;&lt;br&gt;
Progress is written to &lt;code&gt;.tasuki/handoff.md&lt;/code&gt; and injected into the next provider.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Try It in 5 Minutes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/0xkohe/tasuki/cmd/tasuki@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  First Run
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;path/to/your/project
tasuki
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo7aybiohnxwcpd9zlzc7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo7aybiohnxwcpd9zlzc7.png" alt="Terminal output showing tasuki switching from Claude to Codex after hitting usage threshold" width="800" height="744"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fplufeba5hj9w73saebum.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fplufeba5hj9w73saebum.png" alt="Terminal output showing tasuki continuing execution seamlessly on another provider" width="800" height="744"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Use everything. Keep exploring.&lt;br&gt;
tasuki reduces the friction of doing that.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/0xkohe/tasuki" rel="noopener noreferrer"&gt;https://github.com/0xkohe/tasuki&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Issues: &lt;a href="https://github.com/0xkohe/tasuki/issues" rel="noopener noreferrer"&gt;https://github.com/0xkohe/tasuki/issues&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>cli</category>
      <category>go</category>
      <category>programming</category>
    </item>
    <item>
      <title>I built git-ai – generate Conventional Commit messages via Claude/Copilot/Codex CLI</title>
      <dc:creator>kohei</dc:creator>
      <pubDate>Fri, 06 Mar 2026 10:17:51 +0000</pubDate>
      <link>https://dev.to/0xkohe/i-built-git-ai-generate-conventional-commit-messages-via-claudecopilotcodex-cli-4bhk</link>
      <guid>https://dev.to/0xkohe/i-built-git-ai-generate-conventional-commit-messages-via-claudecopilotcodex-cli-4bhk</guid>
      <description>&lt;p&gt;I built a small  cli called git-ai that adds a &lt;code&gt;git ai commit&lt;/code&gt; command to your workflow.&lt;br&gt;
It reads &lt;code&gt;git diff --staged&lt;/code&gt;, sends the diff to your preferred AI CLI (Claude, GitHub Copilot, or OpenAI Codex), and generates a Conventional Commit message automatically. A spinner animation shows the progress, and if one model isn't installed or fails, it silently falls back to the next available one.&lt;/p&gt;

&lt;p&gt;$ git add .&lt;br&gt;
  $ git ai commit&lt;br&gt;
    ⠹ Generating commit message with copilot...&lt;br&gt;
  ✅ Generated by copilot&lt;/p&gt;




&lt;p&gt;Proposed: feat: add user authentication middleware&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7gcljp0lv3fzpxvr062i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7gcljp0lv3fzpxvr062i.png" alt=" " width="744" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Supports Claude CLI, GitHub Copilot CLI, and OpenAI Codex CLI&lt;/li&gt;
&lt;li&gt;Auto-detects which model to use (claude → copilot → codex)&lt;/li&gt;
&lt;li&gt;--edit / -e flag to review and modify in vi before committing&lt;/li&gt;
&lt;li&gt;Pure bash, no dependencies beyond the AI CLI itself&lt;/li&gt;
&lt;li&gt;One-line install via install.sh&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The motivation was simple: I kept writing the same kinds of commit messages over and over. With git-ai, I just stage my changes and let the AI handle the wording — it's surprisingly good at following Conventional Commits format.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/0xkohe/git-ai" rel="noopener noreferrer"&gt;https://github.com/0xkohe/git-ai&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cli</category>
      <category>git</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
