If you are new to development or you are a new Indie dev like me you may have done mistake by shipping your API Key's Client Side Code because you may have not known that it can be easily stolen and you can get big bill from your api provider because of access use of api or exhaust quota. But now you can easily get rid of this problem by following this post.
Don't Ship your Api Key's Client Side & Thank me later
Are you doing mistake of shipping your api key's client side?
— Perry (@realperrygupta) May 22, 2024
I ran the poll on Twitter/X as you can see problem is real
Most of the newer indie devs does this mistake unknowingly that how blunder it can make.
Best way to tackle this is to implement Auth.
But not all small to midsize apps needs auth let's take simple Example of AI chatbot apps. So if we don't have auth in apps how we can tackle api key without shipping in client and save unnecessary headaches & Money? The answer is you can eliminate exposing API with serverless cloud function like AWS Lambda or Firebase Cloud Function or Cloudflare Workers.
We will Use Cloudflare workers to eliminate this issue
This solution will work on any cloud platform with little code modifications. so what is Cloudflare workers? It's a serverless option from Cloudflare similar to AWS Lambda or Firebase Cloud Functions. So now question is ...
Why Cloudflare Workers?
Because its secure, simple, easy and free for the small and midsize Apps. Free tier has 100,000 Calls free every day on top of it billable time is only CPU Usage unlike AWS Lambda where you get charged for whole call time.
Example Usecase & Solution:
You made an app for OpenAI or similar chatbot and you have paid plans for that you have used RevenueCat SDK. In this simple use case you need api key's from OpenAI & RevenueCat. So now the thing is you don't want to deal with implement Auth and Backend. But you also don't want to ship your Api Keys in App Binary. The answer is you need to proxy your requests from the cloud. In simple words your api keys and functions to call OpenAI API will be hosted on cloud like Cloudflare workers. After implementing code which we will discuss ahead cloudflare will provide url to call api's or your own custom url's you will call those url's to get data back. Not calling API's directly in the app.
Prerequisites
- You need a little bit knowledge of how Serverless/Workers functions or tech work.
- You need to have little knowledge of Javascript
Login to cloudflare and go to Workers and Pages in left hand side window pane




Worker.js is your main file where your functions will run
Before running code lets save our api keys and encrypt them

import os
# Generate a 256-bit (32 bytes) random key
hmac_secret_key = os.urandom(32)
print("HMAC Secret Key:", hmac_secret_key.hex())
Run this python code by saving it in hmac.py file it will generate HMAC_SECRET_KEY string
You can run file in terminal by command python hmac.py or python3 hmac.py it will generate HMAC_SECRET_KEY
Why HMAC_SECRET_KEY? Its an alternative to Basic Auth.
From Google, Hash-Based Message Authentication Code (HMAC) is a cryptographic technique that uses a secret key and a hash function to authenticate messages between parties. HMAC is a common mechanism used in secure file transfer protocols, such as HTTPS, FTPS, and SFTP, to ensure data integrity and prevent threats.
You will generate signatures in your apps Swift or Android to send with every request you made then it will be matched in Cloudflare worker then only it will be honoured.


export default {
async fetch(request, env, ctx) {
try {
const ip = request.headers.get('CF-Connecting-IP');
const rateLimitKey = `rate_limit:${ip}`;
const currentTimestamp = new Date().getTime();
// Retrieve or initialize rate limiting info
const rateLimitData = await env.RATE_LIMIT_COUNTER.get(rateLimitKey, 'text');
let rateLimitInfo = rateLimitData ? JSON.parse(rateLimitData) : { value: 0, lastUpdate: 0 };
let { value, lastUpdate } = rateLimitInfo;
// Check rate limit expiration or increment
if (currentTimestamp - lastUpdate > 60 * 1000) {
value = 1; // Reset after 60 seconds
lastUpdate = currentTimestamp;
} else {
value++;
}
// Check if rate limit exceeded
if (value > 2) {
console.log(`Rate limit exceeded for ${ip}`);
return new Response('Too many requests', { status: 429 });
}
// Update rate limit counter
await env.RATE_LIMIT_COUNTER.put(rateLimitKey, JSON.stringify({ value, lastUpdate }), { expirationTtl: 60 }); // 60 seconds until counter resets
// Proceed with request handling
const requestClone = request.clone();
const reqBody = await request.json();
console.log(reqBody);
return handleRequest(requestClone, reqBody, env, ctx);
} catch (error) {
console.error('Error handling request:', error);
return new Response('Server error', { status: 500 });
}
}
};
async function handleRequest(request, reqBody, env, ctx) {
try {
const apiKey = env.OPENAI_API_KEY;
const hmacSecretKey = env.HMAC_SECRET_KEY;
if (!apiKey || !hmacSecretKey) {
return new Response('Server configuration error', { status: 500 });
}
if (request.method !== 'POST' || new URL(request.url).pathname !== '/generate') {
return new Response('Not found', { status: 404 });
}
const userPrompt = reqBody.prompt;
if (!userPrompt) {
return new Response(JSON.stringify({ error: 'Missing "prompt" in request body' }), { status: 400 });
}
const isSignatureValid = await verifyHmacSignature(request, hmacSecretKey, reqBody);
if (!isSignatureValid) {
return new Response('Invalid signature', { status: 401 });
}
// Call generateText and handle possible exceptions right here
const imageResponse = await generateText(apiKey, userPrompt);
return new Response(JSON.stringify(imageResponse), {
headers: { 'Content-Type': 'application/json' },
status: 200
});
} catch (error) {
console.error('Failed to handle request:', error);
return new Response(JSON.stringify({ error: 'Generation failed', details: error.message }), { status: 500 });
} finally {
// Optionally, you can use finally if you need to execute code no matter the result
// Example: logging, cleaning up resources, etc.
}
}
async function generateText(apiKey, prompt) {
try {
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': apiKey,
'anthropic-version': '2023-06-01' // Confirm this version
},
body: JSON.stringify({
model: 'claude-3-haiku-20240307',
max_tokens: 1024,
messages: [{ role: 'user', content: prompt }]
}),
});
const data = await response.json();
if (response.ok) {
if (data.content && data.content.length > 0 && data.content[0].type === "text") {
const generatedText = data.content[0].text;
return generatedText;
} else {
console.error("API response is missing text content. Full response:", JSON.stringify(data));
throw new Error('No text content returned in the API response');
}
} else {
let errorMessage = `API request failed with status code ${response.status}`;
if (data.error && data.error.message) {
errorMessage += `: ${data.error.message}`;
}
console.error("Detailed API request error:", errorMessage);
throw new Error(errorMessage);
}
} catch (error) {
console.error('Error during text generation:', error);
throw new Error(`Text generation failed: ${error.message}`);
}
}
async function verifyHmacSignature(request, secretKey, reqBody) {
const signature = request.headers.get('x-signature');
const encoder = new TextEncoder();
// Ensure the JSON stringification is consistent
const data = encoder.encode(JSON.stringify(reqBody, Object.keys(reqBody).sort()));
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(secretKey),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign', 'verify']
);
const signatureArrayBuffer = await crypto.subtle.sign('HMAC', key, data);
const hashHex = Array.from(new Uint8Array(signatureArrayBuffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
return hashHex === signature;
}
Copy this code and paste it in workers.js
So whats happening in this code
export default {
async fetch(request, env, ctx) {
try {
const ip = request.headers.get('CF-Connecting-IP');
const rateLimitKey = `rate_limit:${ip}`;
const currentTimestamp = new Date().getTime();
// Retrieve or initialize rate limiting info
const rateLimitData = await env.RATE_LIMIT_COUNTER.get(rateLimitKey, 'text');
let rateLimitInfo = rateLimitData ? JSON.parse(rateLimitData) : { value: 0, lastUpdate: 0 };
let { value, lastUpdate } = rateLimitInfo;
// Check rate limit expiration or increment
if (currentTimestamp - lastUpdate > 60 * 1000) {
value = 1; // Reset after 60 seconds
lastUpdate = currentTimestamp;
} else {
value++;
}
// Check if rate limit exceeded
if (value > 2) {
console.log(`Rate limit exceeded for ${ip}`);
return new Response('Too many requests', { status: 429 });
}
// Update rate limit counter
await env.RATE_LIMIT_COUNTER.put(rateLimitKey, JSON.stringify({ value, lastUpdate }), { expirationTtl: 60 }); // 60 seconds until counter resets
// Proceed with request handling
const requestClone = request.clone();
const reqBody = await request.json();
console.log(reqBody);
return handleRequest(requestClone, reqBody, env, ctx);
} catch (error) {
console.error('Error handling request:', error);
return new Response('Server error', { status: 500 });
}
}
};
export default { ... } is main entry point of worker, when it receives an incoming request. It allows you to export a single object that contains the request handling logic for your Worker.
- Inside this object, you can define various handlers or methods that will be executed based on the incoming request. The most common handler is the
fetch
handler, which is responsible for handling fetch events (HTTP requests). - The
fetch
handler is an async function that takes two arguments:request
(an instance of theRequest
object representing the incoming request) andenv
(an object containing the Worker's runtime environment data). - Within the
fetch
handler, you can write your custom logic to process the incoming request and generate an appropriate response.
In our case we are implementing a rate-limiting mechanism for incoming requests using Cloudflare Workers. It checks the client's IP address and maintains a counter for the number of requests made within a 60-second window. If the number of requests exceeds a limit of 2, it returns a 429 (Too Many Requests) response. Otherwise, it increments the counter and proceeds with handling the request. The rate-limiting information is stored in a Cloudflare Worker's KV store, with a expiration time of 60 seconds to reset the counter periodically.
Rate limiting is optional though you can modify code to remove it

- The function first checks if the required environment variables (Anthropic_API_Key and HMAC_SECRET_KEY) are available. If not, it returns a 500 Server Error response.
- It then checks if the incoming request method is POST and if the URL path is /generate. If not, it returns a 404 Not Found response.
- The function extracts the prompt from the request body. If the prompt is missing, it returns a 400 Bad Request response with an error message.
- It calls the verifyHmacSignature function (not shown) to verify the HMAC signature of the request. If the signature is invalid, it returns a 401 Unauthorized response.
- If everything is valid, it calls the generateText function (not shown) with the API key and the user's prompt, passing the response from generateText to the client.
- If an exception occurs during the process, it catches the error, logs it, and returns a 500 Internal Server Error response with the error details.
- There's an empty finally block where you could optionally add code to execute regardless of the result (e.g., logging, cleaning up resources).
async function generateText(apiKey, prompt) {
try {
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': apiKey,
'anthropic-version': '2023-06-01' // Confirm this version
},
body: JSON.stringify({
model: 'claude-3-haiku-20240307',
max_tokens: 1024,
messages: [{ role: 'user', content: prompt }]
}),
});
const data = await response.json();
if (response.ok) {
if (data.content && data.content.length > 0 && data.content[0].type === "text") {
// const generatedText = data.content[0].text;
return data;
} else {
console.error("API response is missing text content. Full response:", JSON.stringify(data));
throw new Error('No text content returned in the API response');
}
} else {
let errorMessage = `API request failed with status code ${response.status}`;
if (data.error && data.error.message) {
errorMessage += `: ${data.error.message}`;
}
console.error("Detailed API request error:", errorMessage);
throw new Error(errorMessage);
}
} catch (error) {
console.error('Error during text generation:', error);
throw new Error(`Text generation failed: ${error.message}`);
}
}
this async function is responsible to call anthropic messages api which in return will provide text from claude
async function verifyHmacSignature(request, secretKey, reqBody) {
const signature = request.headers.get('x-signature');
const encoder = new TextEncoder();
// Ensure the JSON stringification is consistent
const data = encoder.encode(JSON.stringify(reqBody, Object.keys(reqBody).sort()));
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(secretKey),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign', 'verify']
);
const signatureArrayBuffer = await crypto.subtle.sign('HMAC', key, data);
const hashHex = Array.from(new Uint8Array(signatureArrayBuffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
return hashHex === signature;
}
This function verifies that call is coming from your app only
How to call /generate api from swift app?
import CryptoKit
import Foundation
private var signature:String = ""
struct TextGenerationRequest: Codable {
let prompt: String
}
func generateText(prompt: String, completion: @escaping (Result<String, Error>) -> Void) {
let requestBody = TextGenerationRequest(prompt: prompt)
guard let url = URL(string: "https://yourdomain.com/generate") else {
completion(.failure(NSError(domain: "InvalidURL", code: 0, userInfo: nil)))
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(signature, forHTTPHeaderField: "x-signature")
do {
request.httpBody = try JSONEncoder().encode(requestBody)
} catch {
completion(.failure(error))
return
}
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
let error = NSError(domain: "HTTPResponseError", code: (response as? HTTPURLResponse)?.statusCode ?? 0, userInfo: nil)
completion(.failure(error))
return
}
guard let data = data, let generatedText = String(data: data, encoding: .utf8) else {
let error = NSError(domain: "DataDecodingError", code: 0, userInfo: nil)
completion(.failure(error))
return
}
completion(.success(generatedText))
}
task.resume()
}
func generateHMACSignature(secretKey: String, message: String) -> String {
let key = SymmetricKey(data: Data(secretKey.utf8))
let messageData = Data(message.utf8)
let sign = HMAC<SHA256>.authenticationCode(for: messageData, using: key)
// Convert the authentication code to a hex string
return sign.map { String(format: "%02x", $0) }.joined()
}
// Example usage
let secretKey = "3360b950067c163f80895b89b627fa13c2ad883977"
let message = "{\"prompt\": \"Your Text prompt here\"}" // Ensure JSON format matches exactly
signature = generateHMACSignature(secretKey: secretKey, message: message)
print("Generated HMAC signature:", signature)
Usage
generateText(prompt: "Write a short story about a cat.") { result in
switch result {
case .success(let generatedText):
// Handle the generated text
print(generatedText)
case .failure(let error):
// Handle the error
print("Error: \(error.localizedDescription)")
}
}
Conclusion: We can save our api keys from bad actors with robust Cloudflare workers/serverless. It is most secure method to achieve. Hope it will help you. if you found any issue please reach out to me on Twitter. https://twitter.com/realperrygupta