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_role in older projects), Claude and OpenAI API keys.

The rule: unless a service explicitly says a key is safe to publish, treat it as secret.

You may also hear of public and private keys in another sense: public-key cryptography. There, a key pair is two mathematically linked keys: what anyone locks with the public key, only the private key can unlock, and a signature made with the private key can be checked by anyone who has the public key. HTTPS uses key pairs (a website’s certificate holds its public key), and so do SSH keys for GitHub.

API keys are different: publishable and secret API keys are both just long random strings that the service looks up in its records, to see whose they are and what they may do. “Publishable” and “secret” describe their permissions, not any math. A webhook signing secret (in the table at the end of this page) is different again: a shared secret that both the sender (Stripe) and your server know.

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:

  1. 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.
  2. The function checks the token, looks up that user’s Stripe customer ID in the database, takes the price from its STRIPE_PRICE_ID setting, and asks Stripe, with the secret key, for a checkout page for that customer at that price.
  3. 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:

  1. 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.
  2. 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.

True. pk_ keys are publishable keys, designed for front-end code: they can’t create payments, change prices or read your account’s data. Keys that start with sk_ are secret and must stay on the server.

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?

A publishable key identifies your account and can do only what is safe for anyone to do; it’s designed to sit in front-end code, where anyone can read it. A secret key has full power over your account (charging cards, reading all your data, spending your money) and belongs only on the server.

Question 3 · Multiple choice

Which of these task-app keys may end up in the browser?

  1. The OpenAI API key
  2. The Supabase secret key
  3. The Supabase publishable key
  4. The Stripe webhook signing secret

c. The Supabase publishable key is made to be public: the app’s pages use it, and Row Level Security limits what each user can do with it. The other three are secrets that live only on Supabase’s servers: the OpenAI key could spend your credit, the Supabase secret key bypasses Row Level Security, and the signing secret would let anyone fake Stripe’s messages.

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?

It won’t work because Stripe creates a checkout only for a request made with the secret key (Stripe removed its browser-only checkout in 2025). And even if it did work, the browser would then decide the price and the customer. Anything in the browser can be read and changed by its user, for example with the developer tools, so anyone could pick their own price or someone else’s customer. The Edge Function decides those things on the server: the customer from the checked login token, the price from its STRIPE_PRICE_ID setting.

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?

So that users can’t change it. Whatever the browser sends can be changed by the person using it, so if the browser told the server which price to charge, a user could swap in a cheaper price. A setting on the server is out of the users’ reach. Keeping something on the server can be about control, not only about secrecy.

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

1 → B, 2 → A, 3 → D, 4 → C. Only the publishable key reaches the browser; the three secrets live on Supabase’s servers.

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?

It’s safe because the database’s Row Level Security rules decide, row by row, what each request may see or change: with the publishable key and a user’s login token, a request reaches only that user’s own tasks. A table without Row Level Security (or with rules that are too loose) would be open to anyone who has the key, which is everyone.

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?

The function’s address is public: anyone can send it a request. Without the check, anyone could send a fake “payment succeeded” message naming their own account and get Premium without paying. Stripe signs every real message with the signing secret, which only Stripe and your function know, so the function can tell a real message from a fake one and reject the fakes.

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.

  1. The shop’s server creates the payment (items, price, customer) with the secret key. Stripe answers with a one-time client secret for that payment, which the server passes to the browser.
  2. In the browser, Stripe.js, set up with the publishable key, shows the card form inside the shop’s page.
  3. The customer types the card number into Stripe’s form, and Stripe.js sends it, with the client secret, straight to Stripe.

The card number goes from the browser to Stripe, never through the shop’s server, so the shop doesn’t have to protect card numbers. The publishable key can only finish a payment the server already set up; it can’t create one or change its price.

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.

False. Both are just long random strings that the service looks up in its records; “publishable” and “secret” describe what each is allowed to do. In public-key cryptography (HTTPS, SSH keys), the two keys of a pair are mathematically linked: what the public key locks, only the private key can unlock.

© 2025-2026 Y. Rosenthal. All rights reserved.