[{"data":1,"prerenderedAt":1541},["ShallowReactive",2],{"blog:\u002Fblog\u002Fnuxt\u002Fsupabase-rls-strategy":3},{"id":4,"title":5,"author":6,"body":7,"category":1524,"date":1525,"description":1526,"draft":1527,"extension":1528,"image":1529,"meta":1530,"navigation":113,"path":1531,"seo":1532,"series":1533,"seriesOrder":456,"seriesTitle":1534,"stem":1535,"tags":1536,"updatedAt":1529,"__hash__":1540},"blog\u002Fblog\u002Fnuxt\u002Fsupabase-rls-strategy\u002Findex.md","RLS 與「讀 Client，寫 Server」策略","charles",{"type":8,"value":9,"toc":1488},"minimark",[10,14,18,34,37,41,46,54,90,94,126,128,132,135,143,146,191,195,351,355,700,702,706,710,713,737,741,802,804,808,812,877,881,965,969,997,999,1002,1005,1034,1038,1072,1075,1095,1097,1100,1104,1107,1112,1123,1127,1138,1144,1147,1155,1160,1165,1204,1207,1212,1217,1269,1271,1274,1339,1341,1344,1347,1401,1403,1406,1447,1449,1452,1484],[11,12,13],"h2",{"id":13},"這篇要解決什麼問題",[15,16,17],"p",{},"Row Level Security (RLS) 是 Supabase 的核心安全機制。這篇文章將說明：",[19,20,21,25,28,31],"ul",{},[22,23,24],"li",{},"RLS 的核心概念",[22,26,27],{},"為什麼採用「讀 Client，寫 Server」策略",[22,29,30],{},"service_role 繞過 RLS 的必要性",[22,32,33],{},"常見的 RLS 陷阱與除錯技巧",[35,36],"hr",{},[11,38,40],{"id":39},"rls-核心概念","RLS 核心概念",[42,43,45],"h3",{"id":44},"什麼是-row-level-security","什麼是 Row Level Security",[15,47,48,49,53],{},"RLS 讓你可以在",[50,51,52],"strong",{},"資料列層級","控制存取權限，而非只在表層級。",[55,56,61],"pre",{"className":57,"code":58,"language":59,"meta":60,"style":60},"language-sql shiki shiki-themes material-theme-lighter github-light github-dark","-- 使用者只能看到自己的資料\nCREATE POLICY \"Users can view own data\"\nON public.users FOR SELECT\nUSING (auth.uid() = id);\n","sql","",[62,63,64,72,78,84],"code",{"__ignoreMap":60},[65,66,69],"span",{"class":67,"line":68},"line",1,[65,70,71],{},"-- 使用者只能看到自己的資料\n",[65,73,75],{"class":67,"line":74},2,[65,76,77],{},"CREATE POLICY \"Users can view own data\"\n",[65,79,81],{"class":67,"line":80},3,[65,82,83],{},"ON public.users FOR SELECT\n",[65,85,87],{"class":67,"line":86},4,[65,88,89],{},"USING (auth.uid() = id);\n",[42,91,93],{"id":92},"啟用-rls","啟用 RLS",[55,95,97],{"className":57,"code":96,"language":59,"meta":60,"style":60},"-- 新建表後立即啟用 RLS\nALTER TABLE public.users ENABLE ROW LEVEL SECURITY;\n\n-- 強制所有使用者（包括 table owner）都要通過 RLS\nALTER TABLE public.users FORCE ROW LEVEL SECURITY;\n",[62,98,99,104,109,115,120],{"__ignoreMap":60},[65,100,101],{"class":67,"line":68},[65,102,103],{},"-- 新建表後立即啟用 RLS\n",[65,105,106],{"class":67,"line":74},[65,107,108],{},"ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;\n",[65,110,111],{"class":67,"line":80},[65,112,114],{"emptyLinePlaceholder":113},true,"\n",[65,116,117],{"class":67,"line":86},[65,118,119],{},"-- 強制所有使用者（包括 table owner）都要通過 RLS\n",[65,121,123],{"class":67,"line":122},5,[65,124,125],{},"ALTER TABLE public.users FORCE ROW LEVEL SECURITY;\n",[35,127],{},[11,129,131],{"id":130},"讀-client寫-server策略","「讀 Client，寫 Server」策略",[42,133,134],{"id":134},"架構圖",[55,136,141],{"className":137,"code":139,"language":140},[138],"language-text","┌─────────────────────────────────────────────────────────────┐\n│                    Client (Browser)                          │\n├─────────────────────────────────────────────────────────────┤\n│  useSupabaseClient()                                         │\n│  ├── .select() ✅ 允許（RLS 保護）                           │\n│  ├── .insert() ❌ 禁止                                       │\n│  ├── .update() ❌ 禁止                                       │\n│  └── .delete() ❌ 禁止                                       │\n└─────────────────────────────────────────────────────────────┘\n                              │\n                              ▼\n┌─────────────────────────────────────────────────────────────┐\n│                    Server API                                │\n├─────────────────────────────────────────────────────────────┤\n│  getServerSupabaseClient() (service_role)                    │\n│  ├── 權限檢查 (requireUserSession)                           │\n│  ├── 業務邏輯驗證                                            │\n│  └── .insert() \u002F .update() \u002F .delete() ✅ 允許              │\n└─────────────────────────────────────────────────────────────┘\n","text",[62,142,139],{"__ignoreMap":60},[42,144,145],{"id":145},"為什麼這樣設計",[147,148,149,165],"table",{},[150,151,152],"thead",{},[153,154,155,159,162],"tr",{},[156,157,158],"th",{},"操作",[156,160,161],{},"端點",[156,163,164],{},"原因",[166,167,168,180],"tbody",{},[153,169,170,174,177],{},[171,172,173],"td",{},"SELECT",[171,175,176],{},"Client",[171,178,179],{},"RLS 保護，減少延遲，即時性高",[153,181,182,185,188],{},[171,183,184],{},"INSERT\u002FUPDATE\u002FDELETE",[171,186,187],{},"Server",[171,189,190],{},"集中權限檢查、日誌記錄、業務驗證",[42,192,194],{"id":193},"client-端唯讀查詢","Client 端唯讀查詢",[55,196,200],{"className":197,"code":198,"language":199,"meta":60,"style":60},"language-typescript shiki shiki-themes material-theme-lighter github-light github-dark","\u002F\u002F ✅ 正確 - Client 端只做查詢\nconst client = useSupabaseClient&lt;Database&gt;()\nconst { data } = await client\n  .from('users')\n  .select('id, name, email')\n  .order('created_at', { ascending: false })\n","typescript",[62,201,202,208,249,271,296,314],{"__ignoreMap":60},[65,203,204],{"class":67,"line":68},[65,205,207],{"class":206},"sutJx","\u002F\u002F ✅ 正確 - Client 端只做查詢\n",[65,209,210,214,218,222,226,229,232,236,239,241,244,246],{"class":67,"line":74},[65,211,213],{"class":212},"sbsja","const",[65,215,217],{"class":216},"s_hVV"," client",[65,219,221],{"class":220},"smGrS"," =",[65,223,225],{"class":224},"su5hD"," useSupabaseClient",[65,227,228],{"class":220},"&",[65,230,231],{"class":224},"lt",[65,233,235],{"class":234},"sP7_E",";",[65,237,238],{"class":224},"Database",[65,240,228],{"class":220},[65,242,243],{"class":224},"gt",[65,245,235],{"class":234},[65,247,248],{"class":224},"()\n",[65,250,251,253,256,259,262,264,268],{"class":67,"line":80},[65,252,213],{"class":212},[65,254,255],{"class":234}," {",[65,257,258],{"class":216}," data",[65,260,261],{"class":234}," }",[65,263,221],{"class":220},[65,265,267],{"class":266},"sVHd0"," await",[65,269,270],{"class":224}," client\n",[65,272,273,276,280,283,287,291,293],{"class":67,"line":86},[65,274,275],{"class":234},"  .",[65,277,279],{"class":278},"sGLFI","from",[65,281,282],{"class":224},"(",[65,284,286],{"class":285},"sjJ54","'",[65,288,290],{"class":289},"s_sjI","users",[65,292,286],{"class":285},[65,294,295],{"class":224},")\n",[65,297,298,300,303,305,307,310,312],{"class":67,"line":122},[65,299,275],{"class":234},[65,301,302],{"class":278},"select",[65,304,282],{"class":224},[65,306,286],{"class":285},[65,308,309],{"class":289},"id, name, email",[65,311,286],{"class":285},[65,313,295],{"class":224},[65,315,317,319,322,324,326,329,331,334,336,340,343,347,349],{"class":67,"line":316},6,[65,318,275],{"class":234},[65,320,321],{"class":278},"order",[65,323,282],{"class":224},[65,325,286],{"class":285},[65,327,328],{"class":289},"created_at",[65,330,286],{"class":285},[65,332,333],{"class":234},",",[65,335,255],{"class":234},[65,337,339],{"class":338},"skxfh"," ascending",[65,341,342],{"class":234},":",[65,344,346],{"class":345},"syTEX"," false",[65,348,261],{"class":234},[65,350,295],{"class":224},[42,352,354],{"id":353},"server-端寫入","Server 端寫入",[55,356,358],{"className":197,"code":357,"language":199,"meta":60,"style":60},"\u002F\u002F ✅ 正確 - 透過 Server API 寫入\nawait $fetch('\u002Fapi\u002Fv1\u002Fusers', {\n  method: 'POST',\n  body: { name: 'New User', email: 'user@example.com' },\n})\n\n\u002F\u002F Server API 內部使用 service_role\n\u002F\u002F server\u002Fapi\u002Fv1\u002Fusers\u002Findex.post.ts\nexport default defineEventHandler(async (event) =&gt; {\n  const { user } = await requireUserSession(event)\n  const body = await readBody(event)\n\n  const supabase = getServerSupabaseClient()\n  const { data, error } = await supabase\n    .from('users')\n    .insert({ ...body, created_by: user.id })\n    .select()\n    .single()\n\n  if (error) throw createError({ statusCode: 500, message: error.message })\n  return { data }\n})\n",[62,359,360,365,387,405,443,450,454,460,466,495,524,539,544,555,574,589,609,619,629,634,680,694],{"__ignoreMap":60},[65,361,362],{"class":67,"line":68},[65,363,364],{"class":206},"\u002F\u002F ✅ 正確 - 透過 Server API 寫入\n",[65,366,367,370,373,375,377,380,382,384],{"class":67,"line":74},[65,368,369],{"class":266},"await",[65,371,372],{"class":278}," $fetch",[65,374,282],{"class":224},[65,376,286],{"class":285},[65,378,379],{"class":289},"\u002Fapi\u002Fv1\u002Fusers",[65,381,286],{"class":285},[65,383,333],{"class":234},[65,385,386],{"class":234}," {\n",[65,388,389,392,394,397,400,402],{"class":67,"line":80},[65,390,391],{"class":338},"  method",[65,393,342],{"class":234},[65,395,396],{"class":285}," '",[65,398,399],{"class":289},"POST",[65,401,286],{"class":285},[65,403,404],{"class":234},",\n",[65,406,407,410,412,414,417,419,421,424,426,428,431,433,435,438,440],{"class":67,"line":86},[65,408,409],{"class":338},"  body",[65,411,342],{"class":234},[65,413,255],{"class":234},[65,415,416],{"class":338}," name",[65,418,342],{"class":234},[65,420,396],{"class":285},[65,422,423],{"class":289},"New User",[65,425,286],{"class":285},[65,427,333],{"class":234},[65,429,430],{"class":338}," email",[65,432,342],{"class":234},[65,434,396],{"class":285},[65,436,437],{"class":289},"user@example.com",[65,439,286],{"class":285},[65,441,442],{"class":234}," },\n",[65,444,445,448],{"class":67,"line":122},[65,446,447],{"class":234},"}",[65,449,295],{"class":224},[65,451,452],{"class":67,"line":316},[65,453,114],{"emptyLinePlaceholder":113},[65,455,457],{"class":67,"line":456},7,[65,458,459],{"class":206},"\u002F\u002F Server API 內部使用 service_role\n",[65,461,463],{"class":67,"line":462},8,[65,464,465],{"class":206},"\u002F\u002F server\u002Fapi\u002Fv1\u002Fusers\u002Findex.post.ts\n",[65,467,469,472,475,478,480,483,486,489,492],{"class":67,"line":468},9,[65,470,471],{"class":266},"export",[65,473,474],{"class":266}," default",[65,476,477],{"class":278}," defineEventHandler",[65,479,282],{"class":224},[65,481,482],{"class":278},"async",[65,484,485],{"class":224}," (event) ",[65,487,488],{"class":220},"=&",[65,490,491],{"class":224},"gt; ",[65,493,494],{"class":234},"{\n",[65,496,498,501,504,507,509,512,516,518,522],{"class":67,"line":497},10,[65,499,500],{"class":224},"  const ",[65,502,503],{"class":234},"{",[65,505,506],{"class":224}," user",[65,508,261],{"class":234},[65,510,511],{"class":224}," = await ",[65,513,515],{"class":514},"sVXei","requireUserSession",[65,517,282],{"class":234},[65,519,521],{"class":520},"s99_P","event",[65,523,295],{"class":234},[65,525,527,530,533,535,537],{"class":67,"line":526},11,[65,528,529],{"class":224},"  const body = await ",[65,531,532],{"class":514},"readBody",[65,534,282],{"class":234},[65,536,521],{"class":520},[65,538,295],{"class":234},[65,540,542],{"class":67,"line":541},12,[65,543,114],{"emptyLinePlaceholder":113},[65,545,547,550,553],{"class":67,"line":546},13,[65,548,549],{"class":224},"  const supabase = ",[65,551,552],{"class":514},"getServerSupabaseClient",[65,554,248],{"class":234},[65,556,558,560,562,564,566,569,571],{"class":67,"line":557},14,[65,559,500],{"class":224},[65,561,503],{"class":234},[65,563,258],{"class":224},[65,565,333],{"class":234},[65,567,568],{"class":224}," error",[65,570,261],{"class":234},[65,572,573],{"class":224}," = await supabase\n",[65,575,577,580,582,585,587],{"class":67,"line":576},15,[65,578,579],{"class":224},"    .from(",[65,581,286],{"class":285},[65,583,290],{"class":584},"sZUrc",[65,586,286],{"class":285},[65,588,295],{"class":338},[65,590,592,595,597,599,602,605,607],{"class":67,"line":591},16,[65,593,594],{"class":338},"    .insert({ ...body, created_by",[65,596,342],{"class":234},[65,598,506],{"class":224},[65,600,601],{"class":234},".",[65,603,604],{"class":224},"id ",[65,606,447],{"class":234},[65,608,295],{"class":224},[65,610,612,615,617],{"class":67,"line":611},17,[65,613,614],{"class":234},"    .",[65,616,302],{"class":278},[65,618,248],{"class":224},[65,620,622,624,627],{"class":67,"line":621},18,[65,623,614],{"class":234},[65,625,626],{"class":278},"single",[65,628,248],{"class":224},[65,630,632],{"class":67,"line":631},19,[65,633,114],{"emptyLinePlaceholder":113},[65,635,637,640,643,646,649,651,653,656,658,662,664,667,669,671,673,676,678],{"class":67,"line":636},20,[65,638,639],{"class":266},"  if",[65,641,642],{"class":224}," (error) ",[65,644,645],{"class":266},"throw",[65,647,648],{"class":278}," createError",[65,650,282],{"class":224},[65,652,503],{"class":234},[65,654,655],{"class":338}," statusCode",[65,657,342],{"class":234},[65,659,661],{"class":660},"srdBf"," 500",[65,663,333],{"class":234},[65,665,666],{"class":338}," message",[65,668,342],{"class":234},[65,670,568],{"class":224},[65,672,601],{"class":234},[65,674,675],{"class":224},"message ",[65,677,447],{"class":234},[65,679,295],{"class":224},[65,681,683,686,688,691],{"class":67,"line":682},21,[65,684,685],{"class":266},"  return",[65,687,255],{"class":234},[65,689,690],{"class":224}," data ",[65,692,693],{"class":234},"}\n",[65,695,697],{"class":67,"line":696},22,[65,698,699],{"class":224},"})\n",[35,701],{},[11,703,705],{"id":704},"service_role-繞過-rls","service_role 繞過 RLS",[42,707,709],{"id":708},"為什麼-server-api-需要繞過-rls","為什麼 Server API 需要繞過 RLS",[15,711,712],{},"Server API 使用 service_role key，可以繞過 RLS 政策。這是因為：",[714,715,716,725,731],"ol",{},[22,717,718,721,722,724],{},[50,719,720],{},"Server 端已經做過權限檢查","：",[62,723,515],{}," 驗證身份與角色",[22,726,727,730],{},[50,728,729],{},"需要執行跨使用者的操作","：管理員查看所有使用者資料",[22,732,733,736],{},[50,734,735],{},"效能考量","：避免 RLS 檢查的額外開銷",[42,738,740],{"id":739},"rls-policy-必須包含-service_role-繞過","RLS Policy 必須包含 service_role 繞過",[55,742,744],{"className":57,"code":743,"language":59,"meta":60,"style":60},"-- ✅ 正確：包含 service_role 繞過\nCREATE POLICY \"Allow manager update\" ON public.users FOR UPDATE\nUSING (\n  (SELECT auth.role()) = 'service_role'  -- ⚠️ 必須加這行！\n  OR public.current_user_role() IN ('admin', 'manager')\n);\n\n-- ❌ 錯誤：缺少 service_role 繞過\nCREATE POLICY \"Allow manager update\" ON public.users FOR UPDATE\nUSING (\n  public.current_user_role() IN ('admin', 'manager')\n);\n",[62,745,746,751,756,761,766,771,776,780,785,789,793,798],{"__ignoreMap":60},[65,747,748],{"class":67,"line":68},[65,749,750],{},"-- ✅ 正確：包含 service_role 繞過\n",[65,752,753],{"class":67,"line":74},[65,754,755],{},"CREATE POLICY \"Allow manager update\" ON public.users FOR UPDATE\n",[65,757,758],{"class":67,"line":80},[65,759,760],{},"USING (\n",[65,762,763],{"class":67,"line":86},[65,764,765],{},"  (SELECT auth.role()) = 'service_role'  -- ⚠️ 必須加這行！\n",[65,767,768],{"class":67,"line":122},[65,769,770],{},"  OR public.current_user_role() IN ('admin', 'manager')\n",[65,772,773],{"class":67,"line":316},[65,774,775],{},");\n",[65,777,778],{"class":67,"line":456},[65,779,114],{"emptyLinePlaceholder":113},[65,781,782],{"class":67,"line":462},[65,783,784],{},"-- ❌ 錯誤：缺少 service_role 繞過\n",[65,786,787],{"class":67,"line":468},[65,788,755],{},[65,790,791],{"class":67,"line":497},[65,792,760],{},[65,794,795],{"class":67,"line":526},[65,796,797],{},"  public.current_user_role() IN ('admin', 'manager')\n",[65,799,800],{"class":67,"line":541},[65,801,775],{},[35,803],{},[11,805,807],{"id":806},"policy-模板","Policy 模板",[42,809,811],{"id":810},"讀取政策select","讀取政策（SELECT）",[55,813,815],{"className":57,"code":814,"language":59,"meta":60,"style":60},"-- 登入使用者可讀取\nCREATE POLICY \"Authenticated users can read\" ON public.resources\nFOR SELECT USING (\n  (SELECT auth.role()) = 'service_role'\n  OR (SELECT auth.role()) = 'authenticated'\n);\n\n-- 僅特定角色可讀取\nCREATE POLICY \"Staff can read\" ON public.sensitive_data\nFOR SELECT USING (\n  (SELECT auth.role()) = 'service_role'\n  OR public.current_user_role() IN ('admin', 'manager', 'staff')\n);\n",[62,816,817,822,827,832,837,842,846,850,855,860,864,868,873],{"__ignoreMap":60},[65,818,819],{"class":67,"line":68},[65,820,821],{},"-- 登入使用者可讀取\n",[65,823,824],{"class":67,"line":74},[65,825,826],{},"CREATE POLICY \"Authenticated users can read\" ON public.resources\n",[65,828,829],{"class":67,"line":80},[65,830,831],{},"FOR SELECT USING (\n",[65,833,834],{"class":67,"line":86},[65,835,836],{},"  (SELECT auth.role()) = 'service_role'\n",[65,838,839],{"class":67,"line":122},[65,840,841],{},"  OR (SELECT auth.role()) = 'authenticated'\n",[65,843,844],{"class":67,"line":316},[65,845,775],{},[65,847,848],{"class":67,"line":456},[65,849,114],{"emptyLinePlaceholder":113},[65,851,852],{"class":67,"line":462},[65,853,854],{},"-- 僅特定角色可讀取\n",[65,856,857],{"class":67,"line":468},[65,858,859],{},"CREATE POLICY \"Staff can read\" ON public.sensitive_data\n",[65,861,862],{"class":67,"line":497},[65,863,831],{},[65,865,866],{"class":67,"line":526},[65,867,836],{},[65,869,870],{"class":67,"line":541},[65,871,872],{},"  OR public.current_user_role() IN ('admin', 'manager', 'staff')\n",[65,874,875],{"class":67,"line":546},[65,876,775],{},[42,878,880],{"id":879},"寫入政策insertupdatedelete","寫入政策（INSERT\u002FUPDATE\u002FDELETE）",[55,882,884],{"className":57,"code":883,"language":59,"meta":60,"style":60},"-- Manager 以上可寫入\nCREATE POLICY \"Manager can insert\" ON public.resources\nFOR INSERT WITH CHECK (\n  (SELECT auth.role()) = 'service_role'\n  OR public.current_user_role() IN ('admin', 'manager')\n);\n\nCREATE POLICY \"Manager can update\" ON public.resources\nFOR UPDATE USING (\n  (SELECT auth.role()) = 'service_role'\n  OR public.current_user_role() IN ('admin', 'manager')\n);\n\nCREATE POLICY \"Manager can delete\" ON public.resources\nFOR DELETE USING (\n  (SELECT auth.role()) = 'service_role'\n  OR public.current_user_role() IN ('admin', 'manager')\n);\n",[62,885,886,891,896,901,905,909,913,917,922,927,931,935,939,943,948,953,957,961],{"__ignoreMap":60},[65,887,888],{"class":67,"line":68},[65,889,890],{},"-- Manager 以上可寫入\n",[65,892,893],{"class":67,"line":74},[65,894,895],{},"CREATE POLICY \"Manager can insert\" ON public.resources\n",[65,897,898],{"class":67,"line":80},[65,899,900],{},"FOR INSERT WITH CHECK (\n",[65,902,903],{"class":67,"line":86},[65,904,836],{},[65,906,907],{"class":67,"line":122},[65,908,770],{},[65,910,911],{"class":67,"line":316},[65,912,775],{},[65,914,915],{"class":67,"line":456},[65,916,114],{"emptyLinePlaceholder":113},[65,918,919],{"class":67,"line":462},[65,920,921],{},"CREATE POLICY \"Manager can update\" ON public.resources\n",[65,923,924],{"class":67,"line":468},[65,925,926],{},"FOR UPDATE USING (\n",[65,928,929],{"class":67,"line":497},[65,930,836],{},[65,932,933],{"class":67,"line":526},[65,934,770],{},[65,936,937],{"class":67,"line":541},[65,938,775],{},[65,940,941],{"class":67,"line":546},[65,942,114],{"emptyLinePlaceholder":113},[65,944,945],{"class":67,"line":557},[65,946,947],{},"CREATE POLICY \"Manager can delete\" ON public.resources\n",[65,949,950],{"class":67,"line":576},[65,951,952],{},"FOR DELETE USING (\n",[65,954,955],{"class":67,"line":591},[65,956,836],{},[65,958,959],{"class":67,"line":611},[65,960,770],{},[65,962,963],{"class":67,"line":621},[65,964,775],{},[42,966,968],{"id":967},"僅限-admin","僅限 Admin",[55,970,972],{"className":57,"code":971,"language":59,"meta":60,"style":60},"CREATE POLICY \"Admin only\" ON public.system_settings\nFOR ALL USING (\n  (SELECT auth.role()) = 'service_role'\n  OR public.current_user_role() = 'admin'\n);\n",[62,973,974,979,984,988,993],{"__ignoreMap":60},[65,975,976],{"class":67,"line":68},[65,977,978],{},"CREATE POLICY \"Admin only\" ON public.system_settings\n",[65,980,981],{"class":67,"line":74},[65,982,983],{},"FOR ALL USING (\n",[65,985,986],{"class":67,"line":80},[65,987,836],{},[65,989,990],{"class":67,"line":86},[65,991,992],{},"  OR public.current_user_role() = 'admin'\n",[65,994,995],{"class":67,"line":122},[65,996,775],{},[35,998],{},[11,1000,1001],{"id":1001},"效能優化",[42,1003,1004],{"id":1004},"使用子查詢快取",[55,1006,1008],{"className":57,"code":1007,"language":59,"meta":60,"style":60},"-- ✅ 效能好：使用 (SELECT ...) 包裝，讓 Postgres 快取結果\nUSING ((SELECT auth.role()) = 'service_role')\n\n-- ❌ 效能差：每行都會重新計算\nUSING (auth.role() = 'service_role')\n",[62,1009,1010,1015,1020,1024,1029],{"__ignoreMap":60},[65,1011,1012],{"class":67,"line":68},[65,1013,1014],{},"-- ✅ 效能好：使用 (SELECT ...) 包裝，讓 Postgres 快取結果\n",[65,1016,1017],{"class":67,"line":74},[65,1018,1019],{},"USING ((SELECT auth.role()) = 'service_role')\n",[65,1021,1022],{"class":67,"line":80},[65,1023,114],{"emptyLinePlaceholder":113},[65,1025,1026],{"class":67,"line":86},[65,1027,1028],{},"-- ❌ 效能差：每行都會重新計算\n",[65,1030,1031],{"class":67,"line":122},[65,1032,1033],{},"USING (auth.role() = 'service_role')\n",[42,1035,1037],{"id":1036},"使用-helper-函式","使用 Helper 函式",[55,1039,1041],{"className":57,"code":1040,"language":59,"meta":60,"style":60},"-- ✅ 正確：使用 helper 函式\npublic.current_user_role()\npublic.current_user_id()\n\n-- ❌ 錯誤：直接查表（效能差、容易出錯）\nSELECT role FROM public.user_roles WHERE id = auth.uid()\n",[62,1042,1043,1048,1053,1058,1062,1067],{"__ignoreMap":60},[65,1044,1045],{"class":67,"line":68},[65,1046,1047],{},"-- ✅ 正確：使用 helper 函式\n",[65,1049,1050],{"class":67,"line":74},[65,1051,1052],{},"public.current_user_role()\n",[65,1054,1055],{"class":67,"line":80},[65,1056,1057],{},"public.current_user_id()\n",[65,1059,1060],{"class":67,"line":86},[65,1061,114],{"emptyLinePlaceholder":113},[65,1063,1064],{"class":67,"line":122},[65,1065,1066],{},"-- ❌ 錯誤：直接查表（效能差、容易出錯）\n",[65,1068,1069],{"class":67,"line":316},[65,1070,1071],{},"SELECT role FROM public.user_roles WHERE id = auth.uid()\n",[42,1073,1074],{"id":1074},"建立索引",[55,1076,1078],{"className":57,"code":1077,"language":59,"meta":60,"style":60},"-- 如果 policy 經常用到某欄位，確保有索引\nCREATE INDEX idx_resources_owner ON public.resources(owner_id);\nCREATE INDEX idx_resources_department ON public.resources(department_id);\n",[62,1079,1080,1085,1090],{"__ignoreMap":60},[65,1081,1082],{"class":67,"line":68},[65,1083,1084],{},"-- 如果 policy 經常用到某欄位，確保有索引\n",[65,1086,1087],{"class":67,"line":74},[65,1088,1089],{},"CREATE INDEX idx_resources_owner ON public.resources(owner_id);\n",[65,1091,1092],{"class":67,"line":80},[65,1093,1094],{},"CREATE INDEX idx_resources_department ON public.resources(department_id);\n",[35,1096],{},[11,1098,1099],{"id":1099},"踩坑經驗",[42,1101,1103],{"id":1102},"toast-成功但資料沒變","Toast 成功但資料沒變",[15,1105,1106],{},"這是最常見的 RLS 陷阱：",[15,1108,1109,721],{},[50,1110,1111],{},"症狀",[714,1113,1114,1117,1120],{},[22,1115,1116],{},"API 回傳成功",[22,1118,1119],{},"Toast 顯示「儲存成功」",[22,1121,1122],{},"重新整理後資料沒變",[15,1124,1125,721],{},[50,1126,164],{},[19,1128,1129,1132,1135],{},[22,1130,1131],{},"service_role 繞過了 SELECT policy（所以能讀到資料）",[22,1133,1134],{},"但 UPDATE\u002FDELETE policy 沒有加 service_role 繞過",[22,1136,1137],{},"結果：UPDATE 執行但影響 0 行（因為 RLS 過濾掉了）",[15,1139,1140,1143],{},[50,1141,1142],{},"解法","：所有 CUD 操作的 policy 都要加 service_role 繞過。",[42,1145,1146],{"id":1146},"查詢回傳空陣列",[15,1148,1149,721,1151,1154],{},[50,1150,1111],{},[62,1152,1153],{},"SELECT * FROM table"," 回傳空陣列，但資料確實存在。",[15,1156,1157,1159],{},[50,1158,164],{},"：SELECT policy 沒有允許當前使用者。",[15,1161,1162,721],{},[50,1163,1164],{},"除錯",[55,1166,1168],{"className":57,"code":1167,"language":59,"meta":60,"style":60},"-- 檢查現有 Policy\nSELECT * FROM pg_policies WHERE tablename = 'your_table';\n\n-- 以特定使用者身份測試\nSET LOCAL ROLE authenticated;\nSET LOCAL request.jwt.claims = '{\"sub\": \"user-uuid-here\"}';\nSELECT * FROM public.your_table;\n",[62,1169,1170,1175,1180,1184,1189,1194,1199],{"__ignoreMap":60},[65,1171,1172],{"class":67,"line":68},[65,1173,1174],{},"-- 檢查現有 Policy\n",[65,1176,1177],{"class":67,"line":74},[65,1178,1179],{},"SELECT * FROM pg_policies WHERE tablename = 'your_table';\n",[65,1181,1182],{"class":67,"line":80},[65,1183,114],{"emptyLinePlaceholder":113},[65,1185,1186],{"class":67,"line":86},[65,1187,1188],{},"-- 以特定使用者身份測試\n",[65,1190,1191],{"class":67,"line":122},[65,1192,1193],{},"SET LOCAL ROLE authenticated;\n",[65,1195,1196],{"class":67,"line":316},[65,1197,1198],{},"SET LOCAL request.jwt.claims = '{\"sub\": \"user-uuid-here\"}';\n",[65,1200,1201],{"class":67,"line":456},[65,1202,1203],{},"SELECT * FROM public.your_table;\n",[42,1205,1206],{"id":1206},"查詢很慢",[15,1208,1209,1211],{},[50,1210,164],{},"：Policy 中有複雜子查詢。",[15,1213,1214,1216],{},[50,1215,1142],{},"：改用 helper 函式或加索引。",[55,1218,1220],{"className":57,"code":1219,"language":59,"meta":60,"style":60},"-- ❌ 避免：複雜的子查詢會影響效能\nUSING (\n  EXISTS (\n    SELECT 1 FROM public.permissions\n    WHERE user_id = auth.uid() AND resource_id = your_table.id\n  )\n)\n\n-- ✅ 改用 helper 函式封裝邏輯\nUSING (public.can_read_resource(your_table.id))\n",[62,1221,1222,1227,1231,1236,1241,1246,1251,1255,1259,1264],{"__ignoreMap":60},[65,1223,1224],{"class":67,"line":68},[65,1225,1226],{},"-- ❌ 避免：複雜的子查詢會影響效能\n",[65,1228,1229],{"class":67,"line":74},[65,1230,760],{},[65,1232,1233],{"class":67,"line":80},[65,1234,1235],{},"  EXISTS (\n",[65,1237,1238],{"class":67,"line":86},[65,1239,1240],{},"    SELECT 1 FROM public.permissions\n",[65,1242,1243],{"class":67,"line":122},[65,1244,1245],{},"    WHERE user_id = auth.uid() AND resource_id = your_table.id\n",[65,1247,1248],{"class":67,"line":316},[65,1249,1250],{},"  )\n",[65,1252,1253],{"class":67,"line":456},[65,1254,295],{},[65,1256,1257],{"class":67,"line":462},[65,1258,114],{"emptyLinePlaceholder":113},[65,1260,1261],{"class":67,"line":468},[65,1262,1263],{},"-- ✅ 改用 helper 函式封裝邏輯\n",[65,1265,1266],{"class":67,"line":497},[65,1267,1268],{},"USING (public.can_read_resource(your_table.id))\n",[35,1270],{},[11,1272,1273],{"id":1273},"常見問題速查",[147,1275,1276,1286],{},[150,1277,1278],{},[153,1279,1280,1282,1284],{},[156,1281,1111],{},[156,1283,164],{},[156,1285,1142],{},[166,1287,1288,1301,1311,1321],{},[153,1289,1290,1292,1295],{},[171,1291,1103],{},[171,1293,1294],{},"缺少 service_role 繞過",[171,1296,1297,1298],{},"加上 ",[62,1299,1300],{},"(SELECT auth.role()) = 'service_role'",[153,1302,1303,1305,1308],{},[171,1304,1146],{},[171,1306,1307],{},"RLS 未開放讀取",[171,1309,1310],{},"檢查 SELECT policy",[153,1312,1313,1315,1318],{},[171,1314,1206],{},[171,1316,1317],{},"Policy 中有複雜子查詢",[171,1319,1320],{},"改用 helper 函式或加索引",[153,1322,1323,1326,1329],{},[171,1324,1325],{},"API 回傳 HTML",[171,1327,1328],{},"路由衝突",[171,1330,1331,1332,1335,1336],{},"避免同目錄下同時用 ",[62,1333,1334],{},"[id].ts"," 和 ",[62,1337,1338],{},"[id]\u002Fxxx.ts",[35,1340],{},[11,1342,1343],{"id":1343},"檢查清單",[15,1345,1346],{},"建立 RLS Policy 前確認：",[19,1348,1351,1363,1373,1379,1385,1391],{"className":1349},[1350],"contains-task-list",[22,1352,1355,1359,1360,1362],{"className":1353},[1354],"task-list-item",[1356,1357],"input",{"disabled":113,"type":1358},"checkbox"," 包含 ",[62,1361,1300],{}," 繞過",[22,1364,1366,1368,1369,1372],{"className":1365},[1354],[1356,1367],{"disabled":113,"type":1358}," 使用 ",[62,1370,1371],{},"(SELECT ...)"," 包裝 auth 函式",[22,1374,1376,1378],{"className":1375},[1354],[1356,1377],{"disabled":113,"type":1358}," 使用 helper 函式而非直接查表",[22,1380,1382,1384],{"className":1381},[1354],[1356,1383],{"disabled":113,"type":1358}," 寫入操作（INSERT\u002FUPDATE\u002FDELETE）都有對應 policy",[22,1386,1388,1390],{"className":1387},[1354],[1356,1389],{"disabled":113,"type":1358}," 相關欄位已建立索引",[22,1392,1394,1396,1397,1400],{"className":1393},[1354],[1356,1395],{"disabled":113,"type":1358}," ",[62,1398,1399],{},"supabase db lint --level warning"," 無警告",[35,1402],{},[11,1404,1405],{"id":1405},"最佳實踐總結",[714,1407,1408,1417,1427,1433,1441],{},[22,1409,1410,721,1413,1416],{},[50,1411,1412],{},"Client 只讀",[62,1414,1415],{},"useSupabaseClient"," 只用於 SELECT",[22,1418,1419,1422,1423,1426],{},[50,1420,1421],{},"Server 寫入","：所有 CUD 操作透過 ",[62,1424,1425],{},"\u002Fapi\u002F"," 路由",[22,1428,1429,1432],{},[50,1430,1431],{},"service_role 繞過","：CUD policy 必須加 service_role 繞過",[22,1434,1435,1437,1438,1440],{},[50,1436,1001],{},"：使用 ",[62,1439,1371],{}," 包裝、helper 函式、索引",[22,1442,1443,1446],{},[50,1444,1445],{},"測試驗證","：每次修改 RLS 後都要測試",[35,1448],{},[11,1450,1451],{"id":1451},"延伸閱讀",[19,1453,1454,1463,1470,1477],{},[22,1455,1456],{},[1457,1458,1462],"a",{"href":1459,"rel":1460},"https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fauth\u002Frow-level-security",[1461],"nofollow","Supabase RLS 文件",[22,1464,1465],{},[1457,1466,1469],{"href":1467,"rel":1468},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fddl-rowsecurity.html",[1461],"PostgreSQL RLS 官方文件",[22,1471,1472,1473],{},"上一篇：",[1457,1474,1476],{"href":1475},"\u002Fblog\u002Fnuxt\u002Fsupabase-local-first","Supabase Local-First 開發流程",[22,1478,1479,1480],{},"下一篇：",[1457,1481,1483],{"href":1482},"\u002Fblog\u002Fnuxt\u002Fsupabase-function-security","Database Function 安全規範",[1485,1486,1487],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sutJx, html code.shiki .sutJx{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit}html pre.shiki code .sbsja, html code.shiki .sbsja{--shiki-light:#9C3EDA;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s_hVV, html code.shiki .s_hVV{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .smGrS, html code.shiki .smGrS{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .su5hD, html code.shiki .su5hD{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sP7_E, html code.shiki .sP7_E{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sVHd0, html code.shiki .sVHd0{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit}html pre.shiki code .sGLFI, html code.shiki .sGLFI{--shiki-light:#6182B8;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sjJ54, html code.shiki .sjJ54{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s_sjI, html code.shiki .s_sjI{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .skxfh, html code.shiki .skxfh{--shiki-light:#E53935;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .syTEX, html code.shiki .syTEX{--shiki-light:#FF5370;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVXei, html code.shiki .sVXei{--shiki-light:#E53935;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s99_P, html code.shiki .s99_P{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#E36209;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit}html pre.shiki code .sZUrc, html code.shiki .sZUrc{--shiki-light:#E53935;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .srdBf, html code.shiki .srdBf{--shiki-light:#F76D47;--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":60,"searchDepth":80,"depth":80,"links":1489},[1490,1491,1495,1501,1505,1510,1515,1520,1521,1522,1523],{"id":13,"depth":74,"text":13},{"id":39,"depth":74,"text":40,"children":1492},[1493,1494],{"id":44,"depth":80,"text":45},{"id":92,"depth":80,"text":93},{"id":130,"depth":74,"text":131,"children":1496},[1497,1498,1499,1500],{"id":134,"depth":80,"text":134},{"id":145,"depth":80,"text":145},{"id":193,"depth":80,"text":194},{"id":353,"depth":80,"text":354},{"id":704,"depth":74,"text":705,"children":1502},[1503,1504],{"id":708,"depth":80,"text":709},{"id":739,"depth":80,"text":740},{"id":806,"depth":74,"text":807,"children":1506},[1507,1508,1509],{"id":810,"depth":80,"text":811},{"id":879,"depth":80,"text":880},{"id":967,"depth":80,"text":968},{"id":1001,"depth":74,"text":1001,"children":1511},[1512,1513,1514],{"id":1004,"depth":80,"text":1004},{"id":1036,"depth":80,"text":1037},{"id":1074,"depth":80,"text":1074},{"id":1099,"depth":74,"text":1099,"children":1516},[1517,1518,1519],{"id":1102,"depth":80,"text":1103},{"id":1146,"depth":80,"text":1146},{"id":1206,"depth":80,"text":1206},{"id":1273,"depth":74,"text":1273},{"id":1343,"depth":74,"text":1343},{"id":1405,"depth":74,"text":1405},{"id":1451,"depth":74,"text":1451},"Nuxt","2026-01-22","設計安全的 Supabase RLS 政策，實作 Client 端唯讀、Server 端寫入的資料存取策略。",false,"md",null,{},"\u002Fblog\u002Fnuxt\u002Fsupabase-rls-strategy",{"title":5,"description":1526},"nuxt-fullstack","Nuxt 4 全棧實戰筆記","blog\u002Fnuxt\u002Fsupabase-rls-strategy\u002Findex",[1524,1537,1538,1539],"Supabase","PostgreSQL","RLS","smkO4pMwQORJOgOEFbZcTakUNOWfs6bmljnt7Fs1Y6E",1780512499592]