Key Points 9 min read
  • Two-thirds of security incidents involve reading or writing data — at their core they come down to 'who can read and write which data,' i.e. authentication and access control.
  • PostgreSQL is built around role, using schema (search_path), column-level GRANT, and Row Level Security as three layers that guard from the outside in.
  • RLS is a Turing-complete dynamic rule system that validates permissions in real time on every transaction, enabling zero-latency revocation the instant someone leaves.
Table of Contents

TL;DR

Many people call PostgreSQL the “most secure database,” and the original video goes a step further and argues it is the “most secure system” — because it can do all-around access control at the row, column, and even cell level. But before we get to those features, there’s a more worthwhile question to raise first: the entire software industry takes “database security” far less seriously than it should.

Two-Thirds of Security Incidents Are About Reading and Writing

There’s a saying: “The essence of system security is the security of reading and writing data.” Looking back at historical security incidents, they roughly fall into three categories:

  • Data breaches (READ): Confidential data gets read out. There are more of these than you can count — half the dark web is propped up by them.
  • Irreversible destruction (WRITE): Other programs can be restarted when they break, but the database is the single point of truth in the system. Once the real data is altered or deleted, it’s often unrecoverable. The classic case is the 2017 GitLab incident — after a string of “textbook-level absurd” operations, an engineer deleted 300GB of user data hosted in their own PostgreSQL, with no recovery possible.
  • System takedown (DOWN): Things like ransomware and DDoS attacks that render the system unusable. This one leans more toward being a network-architecture problem, though.

Two of the three categories (READ / WRITE) are directly about reading and writing data. In other words, the core of security is really “who can read and write which data” — that is, authentication and access control.

The Industry’s Odd Pattern: The Closer to the Database, the Looser the Defense

The paradox is that the industry’s defensive strength follows a distribution of “tightest at the outermost layer, and increasingly loose as you approach the database.”

  • Frontend: Browsers have evolved from a sieve of vulnerabilities in the early days to today’s near-impenetrable sandbox.
  • Frontend–backend communication: SSL / TLS has gone through several generations of upgrades, and HTTPS has gone from niche to default.
  • Backend: Carelessly exposing IPs and SSH ports is now rare.

But the moment the data reaches its final destination — the database — the picture suddenly gets sloppy. Many large projects have the backend connect to the database directly with the admin account, with almost no protection at all; and even when there is some, it’s just encrypting the admin username and password and stuffing them into the backend code.

There are reasons for doing this: fewer accounts, easier management; application-layer code is also simpler, plus there are performance benefits. But more often than not, it’s a gambler’s mindset — the feeling that “the backend is deployed inside our own house, the house is already well-defended, and the backend connecting to the database is like going from the living room to the bedroom, so why make it so complicated?” The result: anyone who breaks into this house (whether intentionally or not) immediately gets their hands on the nuclear bomb that can destroy the world in an instant — the admin account.

“Security has a scope; everything outside that scope must be treated as insecure.”

After identity data leaves the browser sandbox, travels over HTTPS encryption, and reaches the backend, why does the backend still need to verify identity again? Because every time you step outside “the former’s security framework,” the data is untrustworthy as far as “the latter” is concerned. By the same logic, when the database receives an external request — no matter who the source is — as long as it’s outside the database’s own security scope, it should redo a complete identity and permission check. This is fundamentally no different from how any other application layer operates.

PostgreSQL’s Core: role

PostgreSQL’s security mechanism is designed around the “role.” Whether you’re building a sandbox environment or doing identity and permission checks, you first have to create the roles yourself — you can, for now, simply think of a role as “the database’s login account.”

Next, let’s use a very simple query to understand the whole mechanism. Suppose I want to find the user IDs and names for users in the group named bilibili — this SQL goes through a whole series of permission checks from start to finish:

flowchart TD
    Q["Query enters<br/>(as some role)"] --> S{"Schema layer<br/>search_path visibility"}
    S -->|not visible| D1["Deny"]
    S -->|visible| C{"Column layer<br/>GRANT permissions<br/>(CRUD each independent)"}
    C -->|not authorized| D2["Deny"]
    C -->|has permission| R{"Row layer<br/>RLS real-time check"}
    R -->|policy fails| D3["Deny"]
    R -->|policy passes| OK["Return that row"]

Layer 1: The Schema Sandbox

Unlike MySQL, PostgreSQL has a full database → schema → table three-level structure. A transaction can’t run across databases, but within the same database it can query tables across different schemas.

There’s no isolation between schemas, so beyond helping perfectionists organize and categorize — achieving a namespace effect — what other value does it have? It does have some. PostgreSQL has a search_path mechanism that lets you set different schema visibility for different roles. For example, you can restrict each department’s systems to see only their own schema, while a system that operates across departments can see several departments’ schemas.

If the architecture isn’t complex, you can define just three schemas like this:

  • public: Tables users can query directly.
  • private: Stores confidential data like accounts, passwords, and sessions. A user role without permission on this schema can’t see these tables, and can only access them indirectly through fixed triggers or functions — thereby controlling the user’s exposure to confidential data.
  • worker: Tables completely unrelated to users, such as background asynchronous services that run automatically — visible only to the service itself, avoiding manual queries or side effects.

Treating schemas as a sandbox that isolates tables of different security levels is PostgreSQL’s first layer of defense.

Layer 2: Column-Level GRANT

Once you get into a table, PostgreSQL isn’t like Excel where “opening it shows you everything.” By default, a role has no read or write permission on any column — you have to grant it manually with GRANT, and CRUD is granted separately.

Take the earlier query: to read a user’s ID and name, you have to GRANT the SELECT permission on the ID and name columns to the role issuing the query.

Many people get lazy and grant all CRUD permissions on all tables and all columns to every newly created role at once. That’s certainly a lot more convenient — but if your goal is to save time, you might as well just DROP TABLE and get it over with in one step.

The advice at the column layer is: give the user only the “minimum necessary” permissions.

  • Membership level: users can only view it, not change it, so grant only SELECT.
  • Synchronized third-party parameters (such as an openID or unionID obtained via binding): although they belong to the user, if they’re only used when the backend calls an API and the user themselves never uses them, then you shouldn’t even grant SELECT.

A security report noted that 96% of a company’s permission settings are “empty” — permissions get created, but nobody uses them and nobody turns them off. The report warns that as AI starts taking over operations, these “readily available” permissions will become an enormous security risk.

So for authorization, the recommendation is to adopt an “add-only, never subtract” principle, to avoid inadvertently leaking surplus permissions.

Layer 3: Row Level Security (RLS)

Past the column layer comes the last and most powerful part of PostgreSQL — Row Level Security. It has a unique, Turing-complete dynamic rule system.

The search_path and GRANT discussed earlier are both static matching: you run a command to grant a role some permission on a schema / column, and that permission exists permanently until it’s explicitly revoked.

RLS, on the other hand, is fully dynamic: at the start of every transaction, it validates the role’s CRUD permissions in real time.

Take an orders table as an example. Suppose you want “buyers can only read and write their own orders, and sellers can only read but not write”:

  1. When the user issues a request, inject the user ID into this transaction via PostgreSQL’s runtime parameters (pg_settings).
  2. When the query reaches RLS validation, retrieve this user ID and compare it directly against the current row’s buyer and seller IDs.
  3. In the UPDATE policy, check whether the user is the buyer; in the SELECT policy, check whether the user is the buyer or the seller.

The key point: even if you’re the highest-privileged user, as long as your ID doesn’t match this row’s buyer or seller, you can’t read this row’s data.

The reason RLS is the last and strongest lock in the whole mechanism is that the validation logic it runs can be any form of SQL or function, and inside that function you can call any data in real time to assist the validation. For example, to add “allow this store’s employees to read all of the store’s orders”:

  • During RLS validation, first query the employee data table to get the store ID the user currently works at;
  • Then go back to the orders table and check whether the order’s store ID matches.

Because this is real-time validation, the instant an employee leaves and the employee data table is updated, they immediately lose read access to all orders — zero latency.

If you don’t care about performance and have no TPS bottleneck, you can even stuff all your “system-layer + business-layer” validation logic into that one function and do it as real-time RLS validation.

Closing: Flexible, but Don’t Go to Extremes

By this point you should have a feel for the shape of PostgreSQL’s layered security system: it’s not only 360-degree full coverage, it’s also extremely flexible in use.

If you want to take it to the absolute extreme, you can go back to the topmost layer of role and create a separate, independent role for every single user, so you can configure permissions individually per person at the schema level. That said, it’s not recommended — because many database performance nodes are tied to the role, such as connections: connections in the connection pool can only be reused among the same role, so the more roles you have, the more the benefits of connection reuse get shattered.

There’s always a tradeoff between security and performance. What PostgreSQL gives you isn’t a single switch but a whole set of tools that go from the outside in and can be fine or coarse: role determines identity, schema fences off the sandbox, GRANT locks down to the column, and RLS guards every single row. Use these four layers well, and the database itself becomes your last — and most reliable — line of defense.

References

Ask this article

Answers come from this article only. Click any prompt below or open the chat at the bottom right.

🇺🇸 English

PostgreSQL gets called "the most secure database" a lot. But the original video I'm working from goes further — it calls PostgreSQL the most secure *system* in the world. And the reason is that it can control access all the way down to a single row, a single column, even a single cell. We'll get to those features. But before we do, there's a more interesting question worth sitting with first: the entire software industry takes database security far less seriously than it should.

Let's start with a saying that stuck with me — the essence of system security is really the security of reading and writing data. Look back at the history of security incidents, and they mostly fall into three buckets.

First, data breaches. That's the READ problem — confidential data gets read out. There are more of these than you can count. Honestly, half the dark web is propped up by them.

Second, irreversible destruction. That's the WRITE problem. Here's the thing — most programs, when they break, you just restart them. But the database is the single point of truth. Once the real data gets altered or deleted, it's often gone for good. The textbook example is the 2017 GitLab incident. After a string of just absurd, comedy-of-errors operations, an engineer deleted three hundred gigabytes of user data sitting in their own PostgreSQL. No recovery possible.

And third, system takedown — the DOWN problem. Ransomware, DDoS, the stuff that makes a system unusable. Though that one leans more toward being a network architecture issue.

So notice: two of those three categories, reading and writing, are directly about data. Which means the real core of security is a very simple question — who can read and write which data. Authentication and access control. That's it.

Now here's the paradox, and it's a strange one. If you map out where the industry puts its defensive effort, you find it's tightest at the very outermost layer and gets looser and looser the closer you get to the database.

Think about the frontend. Browsers used to be a sieve of vulnerabilities. Today they're a near-impenetrable sandbox. Think about the communication between frontend and backend — SSL and TLS have gone through generations of upgrades, and HTTPS went from niche to the default everywhere. Think about the backend — carelessly exposing an IP or an SSH port is rare these days.

But the moment the data reaches its final destination, the database, everything suddenly gets sloppy. Tons of large projects have the backend connecting to the database directly with the admin account. Almost no protection. And when there *is* some, it's often just encrypting the admin username and password and stuffing them into the backend code.

There are real reasons people do this. Fewer accounts are easier to manage. The application code is simpler. There are performance benefits. But more often than not, it's a gambler's mindset. The feeling is: the backend is deployed inside our own house, the house is already well-defended, so the backend talking to the database is like walking from the living room to the bedroom — why make it complicated? And the result is that anyone who breaks into this house, intentionally or not, immediately gets their hands on the nuclear button. The admin account that can wipe out everything in an instant.

Here's the principle that reframes all of this: security has a scope, and everything outside that scope must be treated as insecure. Think about it — identity data leaves the browser sandbox, travels over HTTPS, arrives at the backend. And the backend *still* re-verifies identity. Why? Because the moment data steps outside the previous layer's security framework, it's untrustworthy to the next layer. So by exactly the same logic — when the database receives a request, no matter who the source claims to be, as long as it's outside the database's own security scope, the database should run a complete identity and permission check all over again. That's not paranoid. That's just how every other application layer already works.

So how does PostgreSQL actually do this? The whole mechanism is designed around one concept: the role. Whether you're building a sandbox or doing permission checks, you first create the roles yourself. For now, just think of a role as the database's login account.

Let me walk you through the journey a single query takes. Say I want to find the user IDs and names for everyone in a group called "bilibili." That one simple query passes through a whole gauntlet of checks. First it hits the schema layer — is this table even visible to me? If not, denied. If yes, it hits the column layer — do I have permission on these specific columns? If not, denied. If yes, it reaches the row layer, Row Level Security — does the policy allow me to see this exact row, right now? If the policy fails, denied. Only if it passes all three does the row come back. Schema, then column, then row. Outside in.

Let's take those one at a time.

Layer one — the schema sandbox. Unlike MySQL, PostgreSQL has a full three-level structure: database, then schema, then table. A single transaction can't run across databases, but within one database it can query tables across different schemas. Now, there's no hard isolation between schemas, so beyond helping the organizers among us keep things tidy — a namespace effect — what's the real value? It comes from a mechanism called search_path, which lets you set different schema visibility for different roles. So you can restrict each department's system to see only its own schema, while a cross-department system gets to see several.

If your architecture is simple, three schemas is a clean pattern. A "public" schema for tables users can query directly. A "private" schema for the sensitive stuff — accounts, passwords, sessions. A role without permission on that private schema can't even see those tables; it can only touch them indirectly through fixed triggers or functions, which tightly controls how much exposure a user gets to confidential data. And then a "worker" schema for things completely unrelated to users — background async services that run on their own, visible only to the service itself, so nobody accidentally queries them and causes side effects. Using schemas as a sandbox that isolates tables of different security levels — that's PostgreSQL's first line of defense.

Layer two — column-level GRANT. Once you're inside a table, PostgreSQL is nothing like Excel, where opening the file shows you everything. By default, a role has zero read or write permission on *any* column. You have to grant it by hand, and create, read, update, and delete are each granted separately. So back to our query — to read a user's ID and name, you have to explicitly grant SELECT on the ID column and the name column to the role making the request.

Now, a lot of people get lazy and just grant all CRUD on all tables and all columns to every new role in one shot. Sure, that's convenient. But if saving time is really your goal, you might as well just DROP the table and save even more.

The real guidance here is: give each user only the minimum necessary permissions. Membership level? Users can view it but not change it — so grant only SELECT. Synchronized third-party parameters, like an openID or unionID you got from account binding? Those technically belong to the user, but if they're only ever used when the backend calls an external API, and the user never touches them directly, then you shouldn't even grant SELECT.

And there's a sobering statistic behind this. One security report found that ninety-six percent of a company's permission settings were empty — created, but never used and never turned off. And the report's warning is pointed: as AI starts taking over operational tasks, all these lying-around, ready-to-use permissions become an enormous risk. So the recommendation for authorization is an add-only, never-subtract principle. Grant carefully, so you never accidentally leak surplus permissions you forgot you handed out.

Layer three — and this is the last and most powerful piece — Row Level Security, or RLS. It has a dynamic rule system that's actually Turing-complete.

Here's the key distinction. The search_path and GRANT we just talked about are both *static* matching. You run a command, you grant a permission, and that permission just sits there permanently until someone explicitly revokes it. RLS is *fully dynamic*. At the start of every single transaction, it validates the role's permissions in real time.

Let me make it concrete with an orders table. Say the rule is: buyers can read and write only their own orders, and sellers can read but not write. How does RLS pull that off? When the user makes a request, you inject their user ID into the transaction through PostgreSQL's runtime parameters. When the query hits RLS validation, it grabs that user ID and compares it directly against the current row's buyer ID and seller ID. In the update policy, it checks — are you the buyer? In the select policy, it checks — are you the buyer *or* the seller?

And here's the punchline that makes RLS special: even if you are the single highest-privileged user in the entire database, if your ID doesn't match this row's buyer or seller, you cannot read this row. Full stop.

The reason RLS is the strongest lock of all is that its validation logic can be *any* SQL or function, and inside that function you can pull in any data you want, live, to help make the decision. Picture adding a new rule — let a store's employees read all of that store's orders. During RLS validation, you first query the employee table to find which store this user currently works at. Then you go back to the orders table and check whether the order's store ID matches. And because this is real-time, the instant an employee quits and you update the employee table, they lose read access to every order immediately. Zero latency. And if you genuinely don't care about performance, if you have no throughput bottleneck, you could stuff your entire system-layer and business-layer validation logic into that one function and run it all as real-time RLS.

So let me bring this home with a word of caution, because flexibility cuts both ways. If you wanted to take this to the absolute extreme, you could go back to the top and create a separate, independent role for every single user, and configure per-person permissions at the schema level. Don't do that. The reason is performance — a lot of database performance is tied to the role. Connection pooling, for instance: a pooled connection can only be reused among the same role. So the more roles you create, the more you shatter the benefit of connection reuse. There's always a tradeoff between security and performance.

Let me leave you with three things to hold onto.

First — the industry has its defenses backwards. Effort is highest at the outermost layer, the browser, and gets sloppiest right at the database, which is exactly where the crown jewels live. Fix that inversion.

Second — treat every layer boundary as a fresh trust boundary. The database should re-verify identity and permissions on every request, the same way the backend already does, because anything outside your security scope is by definition untrustworthy.

And third — PostgreSQL doesn't hand you one master switch. It hands you four tools that work from the outside in and can be as coarse or as fine as you need. Role decides who you are. Schema fences off the sandbox. GRANT locks things down to the column. And RLS guards every individual row, in real time. Use all four well, and the database itself stops being the soft, unguarded target at the end of the chain — and becomes your last, and most reliable, line of defense.

🇹🇼 中文

很多人說 PostgreSQL 是「最安全的資料庫」,但有種說法更進一步,說它其實是「最安全的系統」,因為它能做到 row、column,甚至是單一 cell 等級的權限控制。不過在聊這些功能之前,我想先講一個更根本的問題:整個軟體業對「資料庫安全」的重視,其實遠遠不夠。

先講一句話:系統安全的本質,就是資料讀寫的安全。回顧歷史上的資安事件,大概可以分成三類。

第一類是機密外洩,也就是 READ,機密資料被讀走。這種事件多到數不清,半個暗網都靠它撐起來。

第二類是不可逆的破壞,也就是 WRITE。別的程式壞了還能重啟,但資料庫是整個系統裡的單一真實來源,一旦真實資料被改掉或刪掉,往往就救不回來了。經典案例就是 2017 年的 GitLab 事件,在一連串堪稱教科書等級荒謬的操作之後,工程師把自家 PostgreSQL 裡三百 GB 的使用者資料刪掉了,無法復原。

第三類是系統被打掛,也就是 DOWN,像勒索軟體、DDoS 攻擊。不過這類比較偏網路架構的問題。

你看,三類裡有兩類,READ 跟 WRITE,都直接跟資料讀寫有關。所以資安的核心其實就是一句話:誰能讀寫哪些資料。也就是認證跟權限控制。

弔詭的地方來了。業界的防護強度,呈現一種「最外層守最嚴、越靠近資料庫越鬆散」的分布。前端,瀏覽器從早年的漏洞篩子,演進到今天近乎銅牆鐵壁的 sandbox。前後端通訊,SSL、TLS 一代一代升級,HTTPS 從小眾變成預設。後端,隨便暴露 IP 跟 SSH port 的做法,現在也很少見了。

可是資料一旦流到最終目的地——資料庫,畫面突然就潦草了。很多大型專案,後端直接用 admin 帳號裸連資料庫,幾乎沒有防護。就算有,也只是把 admin 帳密加密後塞進後端程式碼裡。

這樣做不是完全沒理由:帳號少、好管理,應用層程式碼也更簡單,還有效能上的好處。但更多時候,這其實是一種僥倖心態,覺得後端部署在自己家裡,家裡都防好了,後端連資料庫就像客廳走到臥室,沒必要搞那麼複雜。結果就是,任何人闖進這間房子,不管有意還是無意,就直接拿到了那顆能瞬間毀滅世界的核彈——admin 帳號。

這裡有個很重要的觀念:安全是有範圍的,超出範圍的一切,都要當成不安全。你想想,身分資料離開瀏覽器 sandbox、經過 HTTPS 加密抵達後端之後,後端為什麼還要再驗一次身分?因為每一次跨出「前一層的安全框架」,對後一層來說,資料就是不可信的。同樣的道理,資料庫收到外部請求時,不管來源是誰,只要在資料庫的安全範圍之外,就應該重新做一次完整的身分與權限驗證。這跟其他應用層的做法,本質上沒有差別。

好,那 PostgreSQL 到底怎麼做?整套機制的核心,是圍繞「role」設計的。你可以先簡單把 role 理解成「資料庫的登入帳號」。無論是建 sandbox 環境,還是做身分權限驗證,你都得先自己把 role 建出來。

我們用一個很單純的查詢來理解整套機制。假設我想查出群組名稱叫 bilibili 的使用者,他的 ID 跟 name。這條 SQL 從進到資料庫、到吐出結果,會依序經過三道關卡:先是 schema 層檢查這張表對你可不可見,可見才進到欄位層檢查你有沒有 GRANT 權限,有權限才進到 row 層做 RLS 的即時驗證,全部通過,才回傳這一列資料。任何一關不過,直接拒絕。我們一層一層看。

第一層,schema 沙箱。跟 MySQL 不同,PostgreSQL 有完整的 database、schema、table 三層結構。一個交易不能跨 database,但在同一個 database 裡可以跨 schema 查不同的 table。

schema 之間其實沒有硬隔離,那它除了幫完美主義者做分類、達成 namespace 效果之外,還有什麼價值?有。PostgreSQL 有個叫 search_path 的機制,可以為不同的 role 設定不同的 schema 可見度。比如限制各部門的系統只看得到自己的 schema,而跨部門運行的系統,能看到好幾個部門的 schema。

如果架構不複雜,可以只定義三個 schema。第一個是 public,放使用者可以直接查詢的 table。第二個是 private,放帳號、密碼、session 這些機密資料,沒有這個 schema 權限的 user,根本查不到這些表,只能透過固定的 trigger 或 function 間接存取,藉此控制使用者對機密資料的接觸面。第三個是 worker,放跟使用者完全無關的 table,比如背景自動跑的非同步服務,只對服務本身可見,避免被人工查詢或 side effect 干擾。把 schema 當成隔離不同安全等級的沙箱,這就是第一層防禦。

第二層,欄位級的 GRANT。進到 table,PostgreSQL 並不像 Excel 那樣「打開就全看得到」。預設情況下,一個 role 對任何欄位都沒有讀寫權限,你必須手動用 GRANT 授權,而且 CRUD 是分開授權的。以剛剛那個查詢為例,要查 user 的 ID 跟 name,你就得對發起查詢的那個 role,GRANT 這兩個欄位的 SELECT 權限。

很多人偷懶,直接一次把所有表、所有欄位的全部 CRUD 權限,授給所有新建的 role。這當然方便很多,但我說句難聽的,如果你的目標是省時間,那不如直接 DROP TABLE,一步到位。

欄位層的建議就一句話:只給使用者最小必要的權限。會員等級這種,使用者只能看不能改,那就只給 SELECT。而像同步進來的第三方參數,比如綁定取得的 openID、unionID,雖然屬於這個使用者,但如果只有後端呼叫 API 時才用得到、使用者本人根本用不到,那連 SELECT 都不該給。

有份資安報告很驚人,它指出企業裡百分之九十六的權限設定是「空的」,權限被建立了,卻沒人用、也沒人關掉。報告還警告,當 AI 開始接手操作,這些隨手可用的權限,會變成巨大的資安風險。所以授權上,建議採取「只加不減」的原則,避免不小心洩漏出多餘的權限。

第三層,也是最強的一層——Row Level Security,RLS。前面講的 search_path 跟 GRANT 都是靜態匹配:你執行一條指令把權限授出去,這個權限就永久存在,直到被明確 revoke。但 RLS 是完全動態的,它在每次交易開始時,即時驗證這個 role 的 CRUD 權限。

舉個訂單 table 的例子。假設我要做到「買家只能讀寫自己的訂單,賣家只能讀不能寫」。流程是這樣:使用者發請求時,透過 PostgreSQL 的 runtime 參數,把 user ID 注入這一次交易。查詢進到 RLS 驗證時,取出這個 user ID,直接跟當前這一列的買家、賣家 ID 去比對。在 UPDATE 的 policy 裡,檢查你是不是買家;在 SELECT 的 policy 裡,檢查你是買家還是賣家。

這裡的關鍵是:就算你是權限最高的使用者,只要你的 ID 不符合這一列的買家或賣家,你就讀不到這一列。

RLS 之所以是最後也最強的一把鎖,是因為它執行的驗證邏輯,可以是任意形式的 SQL 或 function,而 function 內部還能即時去撈任何資料來輔助驗證。比如我要新增一條規則:允許店家員工讀取店內所有訂單。RLS 驗證時,就先去查員工資料表,拿到這個使用者目前任職的店家 ID,再回到訂單表,比對這張訂單的店家 ID 是否相符。

因為這是即時驗證,所以員工一離職、員工資料表被更新的那一瞬間,他就立刻失去所有訂單的讀取權限,零延遲。如果你完全不在意效能、沒有 TPS 瓶頸,你甚至可以把所有系統層加業務層的驗證邏輯,全部塞進那一支 function 裡做即時 RLS 驗證。

最後聊聊彈性,還有一個提醒。如果你想做到最極致,可以回到最上層的 role,為每一個使用者各建一個獨立的 role,這樣就能從 schema 層級,為每個人單獨配權限。不過這個做法並不建議,因為很多資料庫的效能節點是綁在 role 上的,比如連線。connection pool 裡的連線,只能在相同 role 之間重用,role 一多,連線復用的效益就被打碎了。安全跟效能之間,始終要權衡。

好,收個尾,我想留三個重點給你。

第一,資安的核心就是讀寫控制,而業界最大的盲點,是離資料庫越近、防守反而越鬆,後端拿 admin 帳號裸連,等於把核彈放在客廳。

第二,PostgreSQL 給你的是一整套由外而內的分層工具:role 決定你是誰,schema 圈出你能看到的沙箱,GRANT 鎖到單一欄位,RLS 守住每一列,而且 RLS 是即時、動態、零延遲的。

第三,權限要「最小必要、只加不減」,別為了省事一次全開,也別走到極端幫每個人建 role 而拖垮效能。把這四層用好,資料庫本身,就會是你最後也最可靠的那道防線。

Tags

Related Articles

Live English Tutor: Building a Real-Time Voice AI English Tutor with LiveKit + Gemini Native Audio

A real-time-voice-first AI English tutoring system: students converse with the AI teacher Emma via microphone (optionally with video/screen sharing), the system corrects mistakes in real time, and generates a post-class report in Chinese. The technical core is LiveKit (Self-hosted WebRTC) + Google Gemini 2.5 Flash Native Audio, with a FastAPI backend handling auth, courses, and data persistence.

RAG's Five Stages: From Pipeline to Reasoning Retrieval, and the Naive RAG on My Own Site

Over the past two years RAG evolved from a 'linear pipeline' to 'loop-based reasoning'. It maps cleanly to five stages: Naive, Advanced, Modular, Graph, Agentic. The real inflection point is control moving from pipeline to agent — a System 1 → System 2 shift. Looking back at engineer-news's own RAG stack, it's stuck at the Naive edge — so this post also lays out what to fix next.