PTENES
MODULE 6.2

🔐 Secrets & Security

When the app leaves your machine and starts running in the cloud, it needs keys to access services—and these keys can’t live in the code. Here you establish the discipline of secrets: environment variables (local vs. cloud), .env.example and read via process.env, OAuth & refresh token, the golden rules, RLS to isolate data in the database, security auditing, and why an open app becomes a financial risk. Module practical: there’s code you can copy and run.

8
Topics
~60
Minutes
Advanced
Level
Practical
Type
Module progress 0% · 0 of 8

🔐 The leap from “prototype” to “secure product”

An app on your machine can even have the key pasted into code that no one can see. But as soon as it goes up to GitHub and runs in the cloud, any lapse with secrets becomes a real problem — a leaked key costs you money, and exposed data becomes an incident. This module covers the habits that keep this under control: where secrets live, how the code reads them, and who can see what.

Before the eight topics, take a look at the idea that ties them all together: the code is public (it goes to GitHub), but the secrets stay outside it, in a separate vault, and are injected only at runtime. One more layer — the database with RLS — ensures that even when the app is live, one user never sees another user’s data.

📦 Code (public) pushes to GitHub · no key 🔐 Secrets vault .env (local) · dashboard (cloud) environment variables injects at runtime 🛡️ Database + RLS each user can only see its data what’s public stays in the code · what’s secret stays in the vault · the database protects the data

Illustrative diagram — layered security: no secrets in the code, keys only in the vault and injected at runtime, and the database isolating data with RLS independently of app bugs.

1

🔑 Environment variables

What it is

A environment variable is a value that is stored outside the code and is injected into the program only when it runs (in runtime). The effect is simple and powerful: the code never contains the key. Where you would previously write const key = "sk-ant-abc123...", now write const key = process.env.ANTHROPIC_API_KEY — the actual value comes from the environment, not the file.

What is it?Runtime — the moment when the program is actually running. API key — a password that gives your app access to a service (Anthropic, Firecrawl, Google). Hard-coded — a value written directly in the code (what we want to avoid for secrets).

✗ Wrong — key inside the code const key = "sk-ant-abc123secreta..." ✓ Correct — code without secrets const key = process.env.ANTHROPIC_API_KEY 🔐 environment (vault) injects the value only at runtime

Illustrative diagram — the code becomes a reference (“give me the key called ANTHROPIC_API_KEY”); the actual value lives in the environment and never appears in the repository.

Why learn

It’s the mechanism that separates “what’s public” (the code) from “what’s secret” (the keys). Without it, every secret travels with the code—and all it takes is one git push for it to be exposed on GitHub forever, even if you delete it later. Environment variables are the foundation for everything in the next topics: they’re the basis of secrets management.

Key concepts

Outside the code

The secret doesn’t live in the file.

At runtime

Injected only at runtime.

By name

The code asks for the key by name.

Nothing hard-coded

Key never written by hand.

2

🗄️ Local (.env) vs. cloud (dashboard)

What it is

The same secret lives in two different places, depending on where the code runs. In the local, it stays in a file .env on your machine. In the model cloud, it stays in the Environment Variables from the platform (Trigger.dev, Vercel), which injects the values at runtime. The detail that catches everyone off guard: the Trigger.dev can’t read your .env local — in the cloud, it only knows what you registered in the dashboard.

What is it?Environment Variables (dashboard) — the platform screen where you add each secret as a pair key/value. Development environment — to test; production — what actually runs for users.

🎯 Goal: store all the project's secrets outside the code, in a local file.

.env file (copy it and fill in the values)
# .env — NUNCA vai para o GitHub (precisa estar no .gitignore)
ANTHROPIC_API_KEY=<sua-chave>
FIRECRAWL_API_KEY=<sua-chave>

# Google (acesso via OAuth — ver tópico 4)
GOOGLE_CLIENT_ID=<seu-client-id>
GOOGLE_CLIENT_SECRET=<seu-client-secret>
GOOGLE_REFRESH_TOKEN=<seu-refresh-token>
SHEET_ID=<id-da-sua-planilha>

✅ How to verify: run cat .gitignore and confirm that there's a line .env — if the file appears in git status as "to be committed," STOP and add it to .gitignore before any push.

🎯 Practical tip

When deploying to production, register each key for the .env also in the cloud dashboard—and select both development how much production. Forgetting to add a key in production is the classic source of the bug that “only fails in prod”: everything works locally, but in the cloud the app breaks with “key is undefined.”

Why learn

Because “it worked on my machine” isn’t enough: the cloud environment is different, with a different secrets vault. Understanding that these are two places — and that they need to stay in sync — prevents the most frustrating deploy error. And it reinforces the rule from topic 5: you can (and sometimes should) use distinct keys for local and production.

Key concepts

Local = .env

File on your machine.

Cloud = dashboard

Env vars panel.

Doesn't read .env

The cloud ignores your local file.

Dev + prod

Add them in both environments.

3

📄 .env.example and process.env

What it is

O .env.example is a safe-to-commit template: lists the names of the keys the project needs, but with no value. It documents “what this project requires” without exposing a single secret. The process.env it’s how the code reads these values at runtime—it never writes them directly. In practice, you ask Claude Code to create the .env, confirm that it’s in the .gitignore, generate the .env.example and adjust the code to read from process.env.

🎯 Goal: document which secrets the project needs without exposing any—this file CAN go on GitHub.

.env.example file (safe to commit)
# .env.example — só os NOMES, nenhum valor. Seguro de commitar.
# Copie para .env e preencha os valores reais.
ANTHROPIC_API_KEY=
FIRECRAWL_API_KEY=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_REFRESH_TOKEN=
SHEET_ID=

✅ How to verify: open the file—no value after the =. Anyone who clones the repo can see what they need to configure, but won’t receive any of your secrets.

🎯 Goal: read the secret at runtime without ever writing it in the code.

code (reads from the environment)
// lê o valor do ambiente — o código fica sem segredo
const key = process.env.ANTHROPIC_API_KEY;

// falha rápido e claro se a chave não foi configurada
if (!key) {
  throw new Error("Faltou ANTHROPIC_API_KEY no ambiente (.env ou dashboard).");
}

✅ How to verify: run the code without the key in the environment — it should fail with a clear message, not with a confusing error later on. This “fail fast” approach saves hours of debugging.

🎯 Goal: let Claude Code configure the entire secrets schema at once.

prompt for Claude Code (in plan mode)
Configure as variáveis de ambiente do projeto para estas chaves:
ANTHROPIC_API_KEY, FIRECRAWL_API_KEY, GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET, GOOGLE_REFRESH_TOKEN, SHEET_ID.

Faça, nesta ordem:
1. crie o arquivo .env (com placeholders <sua-chave>);
2. garanta que .env está no .gitignore;
3. crie um .env.example com as mesmas chaves SEM valores;
4. ajuste o código para ler tudo de process.env (nada hard-coded).
Não cole nenhuma chave real no chat.

✅ How to verify: at the end, git status shows .env.example as a new file, but does NOT show .env. Search the code for “sk-” in quotes to make sure no key was hard-coded.

Why learn

Because a project without .env.example is a puzzle: whoever clones it doesn’t know which keys they need and ends up finding out through trial and error. The pair .env.example + process.env handle this elegantly — document what’s needed and keep secrets out of the code. It’s standard practice for any professional project.

Key concepts

.env.example

Names only, no values.

Committable

The example can be pushed; the .env can’t.

process.env

Read at runtime.

Document

Say what the project needs.

4

🔄 OAuth & refresh token

What it is

OAuth is the protocol that allows delegate access to a service (for example, Google) without exposing your password. Instead of giving your password to the app, you authorize it to act on your behalf, and it receives tokens. The refresh token is the long-lived token that renews access automatically when temporary access expires — it's what keeps a hosted agent running without you having to log in again every time.

What is it?Client ID / Client secret — your app’s “identity” in Google Cloud Console. Access token — short-lived access that expires quickly. Refresh token — long-lived access that generates new access tokens. Service account — an alternative robot account (here, we use the user’s OAuth, which is simpler for personal Sheets/Drive).

Why learn

Because this is how cloud agents access Google Sheets, Drive, and Gmail without you opening the browser for every run. The concrete steps: create OAuth credentials (client ID + secret) in Google Cloud Console and obtain a refresh token. The standard flow through OAuth Playground sometimes fails with “access blocked” or “request is invalid”; the alternative that works is a terminal script that prints an authorization URL and, at the end, returns the refresh token for you to paste into .env and in the Trigger.dev env vars.

✓ Delegated access (OAuth)

  • ✓Authorize the app without handing over your password.
  • ✓A refresh token renews access automatically.
  • ✓If the Playground fails, generate the token with a terminal script.

✗ Dangerous shortcuts

  • ✗Paste your Google password directly into the app.
  • ✗Log manually every time the agent runs.
  • ✗Commit the refresh token to the repository.

🎯 Practical tip

If the OAuth Playground gets stuck with “access blocked / request is invalid,” don’t keep trying it. Ask Claude Code for a small terminal script that uses your GOOGLE_CLIENT_ID e GOOGLE_CLIENT_SECRET, prints the authorization URL, receives the code Google returns, and spits out the GOOGLE_REFRESH_TOKEN in the console — then just paste it into the .env (and in the cloud dashboard).

Key concepts

Delegated access

Authorize without giving your password.

Refresh token

Renews access automatically.

Client ID/secret

App identity.

Terminal fallback

The script generates the token.

5

🏆 Golden rules

What it is

Three non-negotiable rules for handling secrets. One: never paste API keys into the AI chat — chat isn’t the place for secrets. Two: never commit the .env on GitHub — once it’s in the history, consider the key leaked. Three: rotate/regenerate keys periodically, and ideally use keys different for local and production. The mental model: treat an API key like access to a credit card.

The life cycle of a secure secret

1

Generate the key in the service dashboard (Anthropic, Firecrawl) and copy it directly for the .env — without going through the chat.

2

Isolate: o .env in the .gitignore; the value registered in the cloud dashboard (dev and prod).

3

Use via process.env — never hard-coded, never printed in a log.

4

Rotate: if you suspect a leak, regenerate the key in the dashboard—the old one is invalidated immediately. Separate dev/prod keys limit the damage.

⚠️ Key = credit card

A leaked key doesn’t just grant “access”: it lets someone make AI calls in your account, accumulating usage and costs under your name. Bots scan GitHub for exposed keys within minutes. So the rule is strict: if a key is exposed, rotate it immediately.

Why learn

Because most security incidents involving AI apps don’t come from sophisticated hackers — they come from a key pasted in the wrong place. Internalizing these three rules is what separates a careless prototype from a product you can leave running with peace of mind.

Key concepts

Never in chat

Key kept out of the conversation with the AI.

Never commit

.env always in .gitignore.

Rotate

Regenerate it if you’re unsure.

Key = card

If it leaks, someone else spends it for you.

6

🛡️ RLS — isolate data in the database

What it is

RLS (Row-Level Security) are rules that live inside the database itself (in Supabase, for example) and prevent one user from seeing another user’s rows—even if there’s a bug in the frontend. It’s a extra layer as a defense: the database protects the data regardless of the app. If your code accidentally requests "all leads," the database, with RLS, returns only that user's leads.

What is it?Row — a record in the table (e.g., a lead). Policy — the rule that says "who can view/change which rows". Defense in depth — don’t rely on a single layer; if one fails, the other catches it.

Frontend with a bug asks for “all leads” 🛡️ Database with RLS filters by user_id (policy) only the user's leads ✓ other people’s leads blocked ✗ the database is the last line of defense — it protects you even if the app is wrong

Illustrative diagram — with RLS, the isolation rule lives in the database; a frontend error can no longer expose another user's data.

🎯 Goal: isolate the data by user in the database itself, with an RLS policy on the leads table.

SQL in the Supabase editor (copy and run)
-- liga o Row-Level Security na tabela
alter table leads enable row level security;

-- política: cada usuário só enxerga as próprias linhas
-- (compara o dono da linha com o usuário autenticado)
create policy "cada um vê só os seus leads"
on leads
for select
using ( user_id = auth.uid() );

✅ How to verify: log in as user A and list the leads—only theirs appear. Log in as B and try to read A’s leads: the database returns nothing. Isolation is guaranteed at the database level, not in the app.

🎯 Practical tip

When creating the project in Supabase: define a bank password separate from the account login password, choose the region closest to users, enable RLS in tables with user data and run the SQL migration that creates the table. RLS turned off on an exposed table is the #1 security failure in quickly built apps.

Why learn

Because you’ll build quickly, and fast code has bugs. RLS is the seat belt: even if the frontend slips up, the database won’t let one user see another user’s data. It’s the difference between “an annoying bug” and “a data leak that makes the news.”

Key concepts

Database rule

The protection is built into the foundation.

Isolate by user

Each one only sees its own.

Extra layer

Independent of the frontend.

Defense in depth

If one fails, the other catches it.

7

🔍 Security audit

What it is

Before putting the app into production, you ask the AI to act as a security expert and review the project against a four-point checklist: protected routes, no key exposed in the frontend, secrets only in environment variables e RLS enabled. A good practice is to clear the conversation before (fresh context audits better) and then fix everything that fails.

🎯 Goal: perform an explicit security audit before deployment.

prompt for Claude Code (clear the conversation first)
Aja como especialista de segurança e audite este app.
Verifique, item por item:
1. rotas protegidas (nada sensível acessível sem login);
2. nenhuma chave de API exposta no frontend / no bundle;
3. segredos APENAS em variáveis de ambiente (nada hard-coded);
4. RLS habilitado em todas as tabelas com dados de usuário.

Liste o que FALHAR e, para cada falha, diga exatamente
como corrigir. Não invente: se não der pra confirmar, diga.

✅ How to verify: you get a list of actionable findings. Fix each issue and run the audit again until the list is empty — only then deploy to production.

✓ Audit that works

  • ✓Clear the context first for a fresh analysis.
  • ✓Explicit 4-point checklist.
  • ✓Fix anything that fails and re-audit until nothing remains.

✗ What goes wrong

  • ✗Thinking that “it compiled” = “it’s secure.”
  • ✗Let the API key end up in the frontend.
  • ✗Deploy to production without ever reviewing security.

Why learn

Because with vibe coding, you build fast, and it’s easy to leave a door open without realizing it. An explicit audit with a checklist is cheap (one prompt) and catches exactly what slipped through. It’s “trust but verify” applied to security: don’t assume it’s secure — verify.

Key concepts

Specialist

Ask the AI to define its role.

Checklist of 4

Routes, keys, env, RLS.

Clean context

Audit with a fresh conversation.

Fix and repeat

Until the list is empty.

8

⚠️ App open = risk

What it is

A app open — without authentication — it's a concrete financial risk: anyone with the URL can trigger your workflow and consume your AI tokens. Since you pay for usage, strangers’ usage becomes your bill. So don’t leave an app without login running in production for long: before sharing it, add authentication (Supabase) and usage limits.

What is it?Authentication — require login before using the app. Usage limit (rate limit / quota) — a cap on how many times someone can call the app. AI Token — the unit of usage you pay the provider for each model call.

⚠️ The bill that arrives at the end of the month

You publish a cool app, post the link, and go to sleep. A bot finds the URL, triggers the workflow thousands of times, and each run calls the AI model. By morning, the provider’s bill has exploded—with usage that wasn’t yours. An open app + a paid AI call = a direct risk to your wallet.

Why learn

Because this is the natural trigger for the next module. Adding Supabase login and usage limits isn’t just security — it’s the first step toward a business model: if you’re going to control who uses it and how much, you’re one step away from charging for it. This reasoning drives the freemium with Stripe that you see in 6.3.

Key concepts

URL = access

Whoever has the link fires it off.

You pay for usage

Strangers’ tokens end up on your bill.

Authentication

Log in before using (Supabase).

Usage limits

Per-user limit → freemium.

Setting: where NEVER should you add an API key?

📌 Module Summary

✓
Environment variables — the secret lives outside the code and is injected at runtime; the code never contains the key.
✓
Local vs. cloud — .env on your machine, dashboard in the cloud; Trigger.dev doesn’t read your .env—set up dev and prod.
✓
.env.example and process.env — template with names (safe to commit) + runtime reading, with nothing hard-coded.
✓
OAuth & refresh token — delegated access without a password; the refresh token renews it automatically; fallback through a script in the terminal.
✓
Golden rules — never in chat, never commit the .env, rotate it; a key is like a credit card.
✓
RLS in the database — Supabase rules isolate data by user even if there's a frontend bug (defense in depth).
✓
Security audit — the AI, acting as an expert, checks routes, keys, env, and RLS; fix anything that fails before deploy.
✓
Open app = risk — without login, anyone can use up your tokens; add authentication and usage limits.

Next Module:

6.3 — Reliability & Monetization