In the early days of SalesSheet, we made a decision that many AI startups make: we called AI APIs directly from the browser. The user's API key was stored in localStorage, and every chat message triggered a fetch request from the client straight to the AI provider. It was fast to build, easy to debug, and fundamentally insecure.
Six months ago, we rewrote the entire AI layer to run server-side through Supabase Edge Functions. API keys no longer touch the browser. Every AI call is proxied through an authenticated, CORS-restricted server function. This post explains why we made the move, what the architecture looks like, and the specific security measures we implemented.
The Problem with Client-Side AI Calls
When an application makes AI API calls from the browser, several security risks emerge:
- Exposed API keys. Even if you store the key in localStorage or a secure cookie, it is accessible through browser DevTools. Any user can open the console and read their key — or worse, a malicious browser extension can harvest it.
- No request validation. With client-side calls, you cannot verify that the request is legitimate before it reaches the AI provider. A modified client could send any prompt to the API, potentially extracting data or running up costs.
- XSS vulnerability. A cross-site scripting attack on any page could steal the API key and use it from an attacker's infrastructure. The key is only as secure as every script running on the page.
- No rate limiting. Without a server in the middle, you cannot throttle requests or detect abuse patterns. A compromised client could burn through an API quota in minutes.
For a CRM that handles sensitive sales data, these risks are unacceptable. Your contacts, deal amounts, email content, and pipeline data all flow through AI API calls. If the API key is compromised, all of that data is exposed.
The Server-Side Architecture
Our current architecture puts Supabase Edge Functions between the browser and the AI provider. The flow is simple in concept but robust in implementation.
Here is what happens when a user sends a chat message:
- The browser sends the message to a Supabase Edge Function via HTTPS, with a JWT token for authentication.
- The Edge Function verifies the JWT, confirming the user's identity and permissions.
- The function retrieves the user's AI provider settings and API key from encrypted secrets storage.
- It constructs the AI API call with the user's message, conversation history, and available tools.
- The AI provider processes the request and returns a response.
- The Edge Function validates the response, logs usage metrics, and returns the result to the browser.
The critical detail: the API key never leaves the server. It is stored as a Supabase secret, encrypted at rest, and only accessible within the Edge Function runtime. The browser never sees it, the network never carries it, and it cannot be extracted through DevTools or XSS attacks.
PKCE Authentication
We use Proof Key for Code Exchange (PKCE) for our authentication flow. PKCE is specifically designed to protect OAuth flows in public clients (like browser applications) where you cannot safely store a client secret.
In a standard OAuth flow, the client uses a secret to exchange an authorization code for a token. In a browser, that secret would be visible in the source code. PKCE solves this by having the client generate a random "code verifier" for each authentication attempt. The client hashes this verifier and sends the hash with the authorization request. When exchanging the code for a token, the client sends the original verifier, which the server validates against the hash.
This means that even if an attacker intercepts the authorization code, they cannot exchange it for a token without the code verifier, which was generated in-memory and never transmitted in a way that could be intercepted.
CORS: Restricting Who Can Call Our Functions
Cross-Origin Resource Sharing (CORS) headers control which domains can make requests to our Edge Functions. We configure strict CORS policies that only accept requests from our own domain: salessheets.ai.
Any request from a different origin is rejected before it reaches the function logic. This prevents a common attack vector where a malicious website tries to make authenticated API calls using the user's session. Even if a user is logged into SalesSheet and visits a malicious site, that site cannot call our Edge Functions because the CORS policy blocks the request.
We also restrict the allowed HTTP methods (POST only for AI calls) and headers, minimizing the attack surface further.
XSS Prevention
Cross-site scripting is one of the most common web vulnerabilities, and it is especially dangerous in an AI application. If an attacker can inject a script into our application, they could potentially read the AI responses (which may contain sensitive CRM data), modify the prompts being sent to the AI, or extract authentication tokens.
Our XSS prevention strategy has multiple layers:
- Content Security Policy (CSP) — Strict CSP headers that limit which scripts can execute on the page. Inline scripts are blocked, and only scripts from our own domain and trusted CDNs are allowed.
- Input sanitization — All user input is sanitized before being rendered in the UI. This includes chat messages, contact names, and any other user-generated content.
- AI output escaping — AI responses are treated as untrusted content and escaped before rendering. Even if a prompt injection causes the AI to output HTML or JavaScript, it will be displayed as text rather than executed.
- HttpOnly cookies — Authentication tokens are stored in HttpOnly cookies, which are inaccessible to JavaScript. Even a successful XSS attack cannot steal the session token.
Row-Level Security
At the database level, Supabase's Row-Level Security (RLS) ensures that users can only access their own data. RLS policies are enforced at the PostgreSQL level, meaning they cannot be bypassed by application logic — even if there is a bug in our code.
When an Edge Function queries the database on behalf of a user, the query runs with that user's permissions. A user querying contacts sees only their contacts. A user querying deals sees only their deals. There is no administrative override in the application code that could accidentally expose one user's data to another.
This is particularly important for the BYOK model, where each user has their own API key. The RLS policy ensures that User A's API key cannot be read by User B, even if they share the same Supabase instance.
Encryption at Every Layer
Data encryption in SalesSheet operates at multiple levels:
- In transit — All connections use TLS 1.3. This includes browser-to-Edge-Function, Edge-Function-to-AI-Provider, and Edge-Function-to-Database connections. No data moves unencrypted.
- At rest — The PostgreSQL database encrypts all data at rest using AES-256. API keys stored as Edge Function secrets receive additional encryption through Supabase's secret management system.
- In memory — Edge Functions run in Deno's sandboxed runtime. Each function invocation gets its own isolated context, and secrets are only loaded into memory for the duration of the request.
What Changed After the Migration
Moving from client-side to server-side AI calls required rearchitecting how the entire chat system works. The browser no longer manages AI state — it sends messages and receives responses. All tool execution, model routing, and response formatting happens on the server.
The trade-off was a small increase in latency. Adding a server hop adds about 50–100ms to each request. For most operations, this is imperceptible. For the simplest queries routed to Flash Lite, the total response time went from 400ms to about 500ms. We considered this an acceptable trade-off for the security improvements.
The benefits were immediate and measurable. No more API key exposure in DevTools. No more vulnerability to browser extensions. Full request logging and rate limiting. And the ability to add new security measures at the server level without requiring client updates.
Security as a Feature
For a CRM, security is not an afterthought — it is a feature. Your sales data is one of the most valuable assets your business owns. The contacts, relationships, deal values, and communication history in your CRM represent years of work. Any tool that handles this data needs to treat security as a first-class concern.
Moving AI processing server-side was one of the most important architectural decisions we have made. Combined with AI-powered email processing, calendar integration, and multi-provider AI support, the security infrastructure ensures that powerful AI features do not come at the cost of data safety.
Enterprise-Grade Security, Startup-Simple UX
SalesSheet keeps your data safe with server-side AI, PKCE auth, and row-level security — all invisible to you.
Try SalesSheet Free — No Credit Card