39 39. Publishable and Secret Keys
As of Oct 22, 2026 · Official docs: Stripe API keys · Supabase API keys · Stripe Checkout
Many services give you two kinds of API keys: one that may be seen by anyone, and one that must never be seen. This page explains the difference, why the secret keys belong on a server, and where each key of the course’s task app lives. When you ask an AI tool to add a payment, a login or an AI feature to an app, this is how you tell whether its code puts a key in the right place.
39.1 Two kinds of keys
(What an API key is, and where keys are stored: see APIs, API Keys, and Third-Party Services.)
- A publishable (public) key is designed to appear in front-end code, where anyone can read it. It identifies your account and can do only what is safe for anyone to do, or what the service limits in some other way. Examples: Stripe keys starting with
pk_let Stripe’s ready-made card form, placed inside your own page, send card numbers straight to Stripe; Supabase’s publishable key lets anyone talk to your database, but its Row Level Security rules decide which rows each signed-in user may see or change (see What Happens on the Server). - A secret key has full power: charging cards, reading all your data, spending your money. It belongs only on the server. Examples: Stripe keys starting with
sk_, Supabase’s secret key (service_rolein older projects), Claude and OpenAI API keys.
The rule: unless a service explicitly says a key is safe to publish, treat it as secret.
39.2 Why the server holds the secret keys
Everything a website sends to your browser (its pages, its JavaScript, and any key inside them) can be read and changed by whoever uses that browser: the browser’s developer tools show it all, and a small script can send any request it likes. So a well-built app splits the work:
- The browser says what the user wants (“upgrade me”) and proves who the user is (the login token it got when the user signed in).
- The server decides what is actually allowed, and does it with the secret key.
Upgrading to Premium in the course’s task app shows how this works:
- The user clicks Upgrade. The browser calls the Edge Function
create-stripe-session(a small program on Supabase’s servers; see Edge Functions), sending only the user’s login token. - The function checks the token, looks up that user’s Stripe customer ID in the database, takes the price from its
STRIPE_PRICE_IDsetting, and asks Stripe, with the secret key, for a checkout page for that customer at that price. - Stripe answers with the checkout page’s address. The browser goes there, and the user types the card number on Stripe’s own page, never on the app’s.
Why not let the browser ask Stripe directly, with the publishable key? Stripe doesn’t allow it: creating a checkout requires the secret key (Stripe removed its browser-only checkout in 2025). And for good reason: that request decides what is sold, at what price, and to which customer. If a public key could make it, anyone who copied the key from the app’s pages could create a checkout at a price of their choosing, or open another customer’s subscription page and cancel their subscription. In the Edge Function, the browser can’t change any of that: the customer comes from the checked login, and the price from a setting on the server. (The price ID isn’t secret; it stays on the server so that nobody can change it.)
39.3 Why Stripe still has a publishable key
If every payment starts on a server, what is Stripe’s publishable key for? For apps that show Stripe’s payment form inside their own pages, instead of sending the user to a page on Stripe’s website as the task app does. Stripe’s JavaScript library, Stripe.js, draws that form, and it needs to know which Stripe account it works for: you give it the publishable key. Mobile apps use the publishable key the same way, with Stripe’s iPhone and Android libraries.
Even then, the work is split as above:
- Your server creates the payment (what is sold, the price, the customer) with the secret key. Stripe answers with a one-time client secret for that one payment, and the server passes it to the browser.
- In the browser, Stripe.js (set up with the publishable key) shows the form inside your page. The customer types the card number, and Stripe.js sends it straight to Stripe, together with the client secret, never to your server.
So the publishable key lets a page finish a payment that a server has already set up, at the price the server chose. It can’t create one, change the price, or see your other customers. A bonus: the card number never passes through your server, so you don’t have to protect it (card numbers come with strict security rules for anyone who stores or handles them).
In the task app, the Stripe publishable key isn’t used anywhere. You’d need it only if you (or Claude) changed the app to show the payment form inside its own pages.
39.4 The keys in the course’s task app
The task app that you set up in Classes 10–12 uses eight keys and secrets from four services. Each sits where the rules above say it should:
| Key | Kind | Where it lives | What it’s for |
|---|---|---|---|
Supabase publishable key (sb_publishable_...) |
Publishable | .env.local, as NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY, so it ends up in the browser |
Lets the app’s pages talk to your Supabase project. Row Level Security limits each user to their own tasks. The Edge Function create-task-with-ai uses it too, together with the user’s login token, so it can only touch that user’s tasks. |
Supabase secret key (sb_secret_...) |
Secret | Supabase hands it to the Edge Functions by itself; for the optional tests, .env.test.local |
Lets create-stripe-session and stripe-webhook read and update any user’s profile, bypassing Row Level Security, after checking who is asking (or that the message really comes from Stripe). |
OpenAI API key (sk-proj-...) |
Secret | Supabase secret OPENAI_API_KEY |
create-task-with-ai asks OpenAI for a task’s label. |
Stripe secret key (sk_test_...) |
Secret | Supabase secret STRIPE_SECRET_KEY, and in Supabase Vault as stripe |
The Edge Function creates checkout and subscription-management pages; the database creates a Stripe customer for each new user. |
Stripe publishable key (pk_test_...) |
Publishable | Not used | Needed only for a payment form inside the app’s own pages (above). |
Stripe webhook signing secret (whsec_...) |
Secret | Supabase secret STRIPE_WEBHOOK_SECRET |
Stripe signs every message it sends to stripe-webhook; the function checks the signature, so a fake “payment succeeded” message from anyone else is rejected. |
| Google OAuth client ID | Public | Supabase’s Google sign-in settings; it also appears in the address of Google’s sign-in page | Tells Google which app the user is signing in to. |
| Google OAuth client secret | Secret | Supabase’s Google sign-in settings | Proves to Google that it’s really your app when Supabase exchanges the sign-in code (see OAuth). |
Notice the pattern: every secret lives on a server (Supabase), and the only keys in the browser are the ones that are made to be public.
Practice Questions
Easy
Question 1 · True or false
A Stripe key that starts with pk_ is safe to include in the JavaScript sent to the browser.
Question 2 · Short answer
In one or two sentences each: what is a publishable key, and what is a secret key? Where does each belong?
Question 3 · Multiple choice
Which of these task-app keys may end up in the browser?
- The OpenAI API key
- The Supabase secret key
- The Supabase publishable key
- The Stripe webhook signing secret
Medium
Question 4 · Scenario
A classmate wants to simplify the task app: the browser would create the Stripe checkout itself, with the Stripe publishable key, and the Edge Function create-stripe-session would go. Why won’t that work, and why would it be a bad idea even if it did?
Question 5 · Short answer
A Stripe price ID (price_...) isn’t a secret: nobody can charge or change anything with it. Why does the task app still keep it on the server, as the Supabase secret STRIPE_PRICE_ID, rather than in the code sent to the browser?
Question 6 · Matching
Match each task-app key with where it lives.
| Key | Where it lives | |
|---|---|---|
| 1. Supabase publishable key | A. Supabase secret OPENAI_API_KEY |
|
| 2. OpenAI API key | B. .env.local, and from there the browser |
|
| 3. Stripe secret key | C. Supabase’s Google sign-in settings | |
| 4. Google OAuth client secret | D. Supabase secret STRIPE_SECRET_KEY and Supabase Vault |
Question 7 · Scenario
Anyone can find the task app’s Supabase publishable key in the app’s pages and use it to send requests to the database. Why is that safe, and what mistake would make it unsafe?
Hard
Question 8 · Scenario
In the task app, Stripe sends a message to the Edge Function stripe-webhook when a payment succeeds, and the function then switches that user to Premium. Suppose the function didn’t check the message’s signature (made with the webhook signing secret, whsec_...). What could someone do, and why does the signature prevent it?
Question 9 · Long answer
A shop wants Stripe’s card form inside its own checkout page instead of sending customers to a Stripe page. Describe the steps of one payment: what the shop’s server does, what the browser does, which Stripe key each uses, and why the card number never reaches the shop’s server.
Question 10 · True or false
A publishable API key and a secret API key are the public and private halves of a key pair, as in public-key cryptography.