[{"data":1,"prerenderedAt":483},["ShallowReactive",2],{"blog-diagrams/which-api-key-goes-where":3},{"id":4,"title":5,"body":6,"category":456,"date":457,"dateModified":457,"description":458,"draft":459,"extension":460,"faq":461,"featured":459,"headerVariant":466,"image":467,"keywords":469,"meta":470,"navigation":237,"ogDescription":471,"ogTitle":472,"path":473,"readTime":474,"schemaOrg":475,"schemaType":476,"seo":477,"sitemap":478,"stem":479,"tags":480,"twitterCard":481,"__hash__":482},"blog/blog/diagrams/which-api-key-goes-where.md","Which API Key Goes Where (Diagram)",{"type":7,"value":8,"toc":448},"minimark",[9,13,16,24,29,48,57,60,64,74,78,81,110,132,136,139,195,207,211,214,373,413,432,444],[10,11,12],"p",{},"Every leaked-key incident starts the same way: somebody could not tell which kind of key they were holding.",[10,14,15],{},"That is a reasonable mistake. Some providers make it obvious, and some ship two keys that look nearly identical and behave completely differently. This tree starts with the only thing you always have, which is the first few characters.",[17,18],"diagram",{"alt":19,"caption":20,"height":21,"src":22,"width":23},"A decision tree starting from the key's prefix. Keys beginning sk_, sk-ant- or sb_secret_ are server only. Keys beginning pk_ or sb_publishable_ are browser safe. AIzaSy keys are browser safe with a referrer restriction. JWT keys beginning eyJhbGci must be decoded: service_role is server only, anon is browser safe only with Row Level Security enabled.","Start at the top with the characters your key begins with.",1238,"which-api-key-goes-where",2168,[25,26,28],"h2",{"id":27},"server-only","Server only",[10,30,31,35,36,39,40,43,44,47],{},[32,33,34],"code",{},"sk_"," (Stripe and many others), ",[32,37,38],{},"sk-ant-"," (Anthropic), ",[32,41,42],{},"sb_secret_"," (Supabase), and any Supabase JWT that decodes to ",[32,45,46],{},"service_role",".",[10,49,50,51,53,54,56],{},"These bypass restrictions rather than operate within them. A ",[32,52,46],{}," key ignores every Row Level Security policy you have written. A Stripe ",[32,55,34],{}," key can issue refunds and read every customer record. There is no configuration that makes them safe to ship to a browser.",[10,58,59],{},"If one of these has reached a browser, the fix is not to hide it better. Rotate it, then move it to a server route.",[25,61,63],{"id":62},"browser-safe-no-conditions","Browser safe, no conditions",[10,65,66,69,70,73],{},[32,67,68],{},"pk_"," and ",[32,71,72],{},"sb_publishable_",". These are designed to be read. They identify your project and nothing more, and the provider assumes they are public.",[25,75,77],{"id":76},"browser-safe-but-only-with-a-condition-attached","Browser safe, but only with a condition attached",[10,79,80],{},"This is the row that catches people, because the answer is \"yes, but\" and the \"but\" is doing all the work.",[82,83,84,95],"ul",{},[85,86,87,94],"li",{},[88,89,90,93],"strong",{},[32,91,92],{},"AIzaSy"," (Google)."," Fine in a browser once you set an HTTP referrer restriction in the Cloud console so it only works from your domain. Skip that step and anyone can copy it out of your bundle and bill their usage to you.",[85,96,97,104,105,109],{},[88,98,99,100,103],{},"A JWT that decodes to ",[32,101,102],{},"anon"," (Supabase)."," Fine in a browser ",[106,107,108],"em",{},"if"," Row Level Security is on for every table. The anon key is not itself a restriction, it is an identifier. RLS is the restriction.",[111,112,113],"warning-box",{},[10,114,115,122,123,126,127,69,129,131],{},[88,116,117,118,121],{},"The ",[32,119,120],{},"eyJhbGci"," trap."," Both legacy Supabase keys are JWTs and both start with the same characters, so they are indistinguishable at a glance. That is exactly why people shipped the wrong one. Decode the payload and read the ",[32,124,125],{},"role"," field before you trust it. The newer ",[32,128,72],{},[32,130,42],{}," prefixes exist to make this mistake harder.",[25,133,135],{"id":134},"how-to-decode-a-jwt-without-a-tool","How to decode a JWT without a tool",[10,137,138],{},"Paste this in your browser console, replacing the placeholder:",[140,141,146],"pre",{"className":142,"code":143,"language":144,"meta":145,"style":145},"language-js shiki shiki-themes github-dark","JSON.parse(atob(\"YOUR_KEY\".split(\".\")[1]));\n","js","",[32,147,148],{"__ignoreMap":145},[149,150,153,157,160,164,167,170,172,176,178,181,183,186,189,192],"span",{"class":151,"line":152},"line",1,[149,154,156],{"class":155},"sDLfK","JSON",[149,158,47],{"class":159},"s95oV",[149,161,163],{"class":162},"svObZ","parse",[149,165,166],{"class":159},"(",[149,168,169],{"class":162},"atob",[149,171,166],{"class":159},[149,173,175],{"class":174},"sU2Wk","\"YOUR_KEY\"",[149,177,47],{"class":159},[149,179,180],{"class":162},"split",[149,182,166],{"class":159},[149,184,185],{"class":174},"\".\"",[149,187,188],{"class":159},")[",[149,190,191],{"class":155},"1",[149,193,194],{"class":159},"]));\n",[10,196,197,198,200,201,203,204,206],{},"You will get an object with a ",[32,199,125],{}," field. ",[32,202,102],{}," means check your RLS. ",[32,205,46],{}," means rotate it now.",[25,208,210],{"id":209},"the-mermaid-source","The Mermaid source",[10,212,213],{},"The diagram is generated from a text file. Copy it and add whatever providers your own stack uses.",[140,215,219],{"className":216,"code":217,"language":218,"meta":145,"style":145},"language-mermaid shiki shiki-themes github-dark","flowchart TB\n    Q{\"What does your key start with?\"}\n\n    Q -->|\"\u003Ccode>sk_\u003C/code> &nbsp; \u003Ccode>sk-ant-\u003C/code> &nbsp; \u003Ccode>sb_secret_\u003C/code>\"| SEC\n    Q -->|\"\u003Ccode>pk_\u003C/code> &nbsp; \u003Ccode>sb_publishable_\u003C/code>\"| PUB\n    Q -->|\"\u003Ccode>AIza\u003C/code> &nbsp; (Google)\"| REF\n    Q -->|\"\u003Ccode>eyJhbGci\u003C/code> &nbsp; (a JWT)\"| JWT\n\n    JWT{\"Decode it. What is the role?\"}\n    JWT -->|\"\u003Ccode>service_role\u003C/code>\"| SEC\n    JWT -->|\"\u003Ccode>anon\u003C/code>\"| RLS\n\n    SEC[\"\u003Cb>Server only.\u003C/b>\u003Cbr/>If it reaches a browser, rotate it today.\"]\n    PUB[\"\u003Cb>Browser is fine.\u003C/b>\u003Cbr/>It is designed to be read.\"]\n    REF[\"\u003Cb>Browser, with a condition.\u003C/b>\u003Cbr/>Set an HTTP referrer restriction\u003Cbr/>in the Google Cloud console first.\"]\n    RLS[\"\u003Cb>Browser, with a condition.\u003C/b>\u003Cbr/>Row Level Security must be on\u003Cbr/>for every table.\"]\n\n    classDef ask fill:#f5f5f4,stroke:#57534e,stroke-width:3px,color:#1c1917\n    classDef danger fill:#fef2f2,stroke:#ef4444,stroke-width:3px,color:#1c1917\n    classDef safe fill:#ecfdf5,stroke:#10b981,stroke-width:3px,color:#1c1917\n    classDef caution fill:#fffbeb,stroke:#f59e0b,stroke-width:3px,color:#1c1917\n\n    class Q,JWT ask\n    class SEC danger\n    class PUB safe\n    class REF,RLS caution\n","mermaid",[32,220,221,226,232,239,245,251,257,263,268,274,280,286,291,297,303,309,315,320,326,332,338,344,349,355,361,367],{"__ignoreMap":145},[149,222,223],{"class":151,"line":152},[149,224,225],{},"flowchart TB\n",[149,227,229],{"class":151,"line":228},2,[149,230,231],{},"    Q{\"What does your key start with?\"}\n",[149,233,235],{"class":151,"line":234},3,[149,236,238],{"emptyLinePlaceholder":237},true,"\n",[149,240,242],{"class":151,"line":241},4,[149,243,244],{},"    Q -->|\"\u003Ccode>sk_\u003C/code> &nbsp; \u003Ccode>sk-ant-\u003C/code> &nbsp; \u003Ccode>sb_secret_\u003C/code>\"| SEC\n",[149,246,248],{"class":151,"line":247},5,[149,249,250],{},"    Q -->|\"\u003Ccode>pk_\u003C/code> &nbsp; \u003Ccode>sb_publishable_\u003C/code>\"| PUB\n",[149,252,254],{"class":151,"line":253},6,[149,255,256],{},"    Q -->|\"\u003Ccode>AIza\u003C/code> &nbsp; (Google)\"| REF\n",[149,258,260],{"class":151,"line":259},7,[149,261,262],{},"    Q -->|\"\u003Ccode>eyJhbGci\u003C/code> &nbsp; (a JWT)\"| JWT\n",[149,264,266],{"class":151,"line":265},8,[149,267,238],{"emptyLinePlaceholder":237},[149,269,271],{"class":151,"line":270},9,[149,272,273],{},"    JWT{\"Decode it. What is the role?\"}\n",[149,275,277],{"class":151,"line":276},10,[149,278,279],{},"    JWT -->|\"\u003Ccode>service_role\u003C/code>\"| SEC\n",[149,281,283],{"class":151,"line":282},11,[149,284,285],{},"    JWT -->|\"\u003Ccode>anon\u003C/code>\"| RLS\n",[149,287,289],{"class":151,"line":288},12,[149,290,238],{"emptyLinePlaceholder":237},[149,292,294],{"class":151,"line":293},13,[149,295,296],{},"    SEC[\"\u003Cb>Server only.\u003C/b>\u003Cbr/>If it reaches a browser, rotate it today.\"]\n",[149,298,300],{"class":151,"line":299},14,[149,301,302],{},"    PUB[\"\u003Cb>Browser is fine.\u003C/b>\u003Cbr/>It is designed to be read.\"]\n",[149,304,306],{"class":151,"line":305},15,[149,307,308],{},"    REF[\"\u003Cb>Browser, with a condition.\u003C/b>\u003Cbr/>Set an HTTP referrer restriction\u003Cbr/>in the Google Cloud console first.\"]\n",[149,310,312],{"class":151,"line":311},16,[149,313,314],{},"    RLS[\"\u003Cb>Browser, with a condition.\u003C/b>\u003Cbr/>Row Level Security must be on\u003Cbr/>for every table.\"]\n",[149,316,318],{"class":151,"line":317},17,[149,319,238],{"emptyLinePlaceholder":237},[149,321,323],{"class":151,"line":322},18,[149,324,325],{},"    classDef ask fill:#f5f5f4,stroke:#57534e,stroke-width:3px,color:#1c1917\n",[149,327,329],{"class":151,"line":328},19,[149,330,331],{},"    classDef danger fill:#fef2f2,stroke:#ef4444,stroke-width:3px,color:#1c1917\n",[149,333,335],{"class":151,"line":334},20,[149,336,337],{},"    classDef safe fill:#ecfdf5,stroke:#10b981,stroke-width:3px,color:#1c1917\n",[149,339,341],{"class":151,"line":340},21,[149,342,343],{},"    classDef caution fill:#fffbeb,stroke:#f59e0b,stroke-width:3px,color:#1c1917\n",[149,345,347],{"class":151,"line":346},22,[149,348,238],{"emptyLinePlaceholder":237},[149,350,352],{"class":151,"line":351},23,[149,353,354],{},"    class Q,JWT ask\n",[149,356,358],{"class":151,"line":357},24,[149,359,360],{},"    class SEC danger\n",[149,362,364],{"class":151,"line":363},25,[149,365,366],{},"    class PUB safe\n",[149,368,370],{"class":151,"line":369},26,[149,371,372],{},"    class REF,RLS caution\n",[374,375,376,401,407],"faq-section",{},[377,378,380],"faq-item",{"question":379},"How can I tell if an API key is safe to put in my frontend?",[10,381,382,383,385,386,388,389,391,392,394,395,397,398,400],{},"Read the first few characters. Keys meant for browsers usually say so: ",[32,384,68],{}," for publishable, ",[32,387,72],{}," for Supabase. Keys starting ",[32,390,34],{},", ",[32,393,38],{}," or ",[32,396,42],{}," are secret and must stay on your server. If the key starts ",[32,399,120],{}," it is a JWT and you have to decode it, because the same shape is used for both the safe and the dangerous Supabase key.",[377,402,404],{"question":403},"Is the Supabase anon key safe to expose?",[10,405,406],{},"Only if Row Level Security is enabled on every table. The anon key is designed to be public, but it is not itself a restriction. RLS is the restriction. With RLS off, the anon key reads everything, which is how most exposed Supabase databases happen.",[377,408,410],{"question":409},"What about Google API keys starting with AIzaSy?",[10,411,412],{},"They are usually fine in a browser, but only once you add an HTTP referrer restriction in the Google Cloud console so the key only works from your own domain. Without that restriction anyone who copies it out of your bundle can bill their usage to your project.",[414,415,416,422,427],"related-articles",{},[417,418],"related-card",{"description":419,"href":420,"title":421},"The line that decides whether a key is exposed in the first place","/blog/diagrams/vibe-coded-trust-boundary","Where the Trust Boundary Sits",[417,423],{"description":424,"href":425,"title":426},"Why only one of the two Supabase keys gets checked","/blog/diagrams/supabase-key-trust-boundary","Supabase Keys and the Trust Boundary",[417,428],{"description":429,"href":430,"title":431},"The first hour after a key gets out, and why your spend limit did not stop it","/blog/how-to/ai-api-key-stolen","Your Key Was Stolen. Now What?",[433,434,437,441],"cta-box",{"href":435,"label":436},"/","Start Free Scan",[25,438,440],{"id":439},"not-sure-what-is-in-your-bundle","Not sure what is in your bundle?",[10,442,443],{},"A scan reads your deployed JavaScript and tells you which keys are in it.",[445,446,447],"style",{},"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 pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}",{"title":145,"searchDepth":228,"depth":228,"links":449},[450,451,452,453,454,455],{"id":27,"depth":228,"text":28},{"id":62,"depth":228,"text":63},{"id":76,"depth":228,"text":77},{"id":134,"depth":228,"text":135},{"id":209,"depth":228,"text":210},{"id":439,"depth":228,"text":440},"diagrams","2026-07-31","A decision tree for telling a browser-safe API key from one that has to stay on your server, starting from the characters the key begins with.",false,"md",[462,464,465],{"question":379,"answer":463},"Read the first few characters. Keys meant for browsers usually say so: pk_ for publishable, sb_publishable_ for Supabase. Keys starting sk_, sk-ant- or sb_secret_ are secret and must stay on your server. If the key starts eyJhbGci it is a JWT and you have to decode it, because the same shape is used for both the safe and the dangerous Supabase key.",{"question":403,"answer":406},{"question":409,"answer":412},"blue",{"src":468,"alt":19},"https://checkyourvibe.dev/diagrams/which-api-key-goes-where.png","which api key is safe in frontend, sk_ vs pk_ key, service_role vs anon key, is my api key safe to expose, publishable vs secret key",{"ogImage":468},"Read the first few characters of your key and this tells you where it is allowed to live.",null,"/blog/diagrams/which-api-key-goes-where","4 min read","[object Object]","Article",{"title":5,"description":458},{"loc":473},"blog/diagrams/which-api-key-goes-where",[],"summary_large_image","jLM1IiPnUWRWNO9AhAeaOU9JR4x2cdPYZBK0xpGq6OY",1785543175821]