Executables
Expose backend functions to the client with full access to server resources.
Why Use Executables
Your frontend needs to do something that requires server-side resources: accessing a secret API key, querying a database, or running business logic you don't want exposed in the browser.
Without executables, you'd need to set up an entire API layer: define routes, handle serialization, manage CORS, and deploy a separate server. With executables, you write a function and call it:
// Backend: just a decorated method
@executable()
async processPayment(orderId: string, amount: number): Promise<Receipt> {
const apiKey = this.secrets['PAYMENT_API_KEY']; // Access secrets securely
return await paymentService.charge(orderId, amount, apiKey);
}
// Frontend: call it like a local function
const receipt = await squid.executeFunction('processPayment', orderId, 99.99);
No routes. No API boilerplate. Just functions.
Overview
Executables are backend functions that can be called directly from the client. They provide a simple RPC-style interface for operations that require server-side resources such as secrets, databases, external APIs, or complex business logic.
When to use executables
| Use Case | Recommendation |
|---|---|
| Call a function from client with server-side logic | ✅ Executable |
| React to database changes | Use Triggers |
| Run code on a schedule | Use Schedulers |
| Expose an HTTP endpoint to external services | Use Webhooks |
| Real-time data synchronization | Use Database directly |
How it works
- You decorate a method with
@executable()in a class that extendsSquidService - Squid discovers and registers the function at deploy time
- Clients call the function using
squid.executeFunction('functionName', ...args) - The backend executes the function with full access to secrets, context, and integrations
- The result is serialized and returned to the client
Quick Start
Prerequisites
- A Squid backend project initialized with
squid init-backend - The
@squidcloud/backendpackage installed
Step 1: Create an executable function
Create a service class that extends SquidService and add your executable function:
import { executable, SquidService } from '@squidcloud/backend';
export class ExampleService extends SquidService {
@executable()
async greet(name: string): Promise<string> {
return `Hello, ${name}!`;
}
}
Step 2: Export the service
Ensure your service is exported from the service index file:
export * from './example-service';
Step 3: Deploy the backend
Deploy your backend to make the executable available:
squid deploy
Step 4: Call from the client
const greeting = await squid.executeFunction('greet', 'World');
console.log(greeting); // Output: "Hello, World!"
Authentication and Authorization
Executables have unlimited access to backend resources including secrets, databases, and integrations. Always validate that the caller is authorized to perform the requested action.
Unlike security rules which automatically guard access to resources, executables require you to manually check authentication within your function code.
Checking authentication
Use this.assertIsAuthenticated() to require authentication (throws 'UNAUTHORIZED' if not authenticated), or this.isAuthenticated() to check manually:
import { executable, SquidService } from '@squidcloud/backend';
export class SecureService extends SquidService {
@executable()
async getSecretData(): Promise<string> {
// Throws UNAUTHORIZED if not authenticated
this.assertIsAuthenticated();
// Access user details for authorization decisions
const userAuth = this.getUserAuth();
if (!userAuth?.attributes?.['role']?.includes('admin')) {
throw new Error('Admin access required');
}
return 'Secret data';
}
}
For detailed information on authentication methods and securing your backend, see Security rules.
Core Concepts
The request context
Every executable has access to the request context via this.context:
@executable()
async logRequestInfo(): Promise<void> {
const ctx = this.context;
console.log('App ID:', ctx.appId);
console.log('Client ID:', ctx.clientId); // Unique client identifier
console.log('Source IP:', ctx.sourceIp); // Client IP address
console.log('Headers:', ctx.headers); // Request headers (lowercase keys)
}
| Property | Type | Description |
|---|---|---|
appId | string | Your application ID |
clientId | string | undefined | Unique identifier for the calling client |
sourceIp | string | undefined | IP address of the caller |
headers | Record<string, any> | undefined | HTTP headers (keys are lowercase) |
Secrets and API keys
Access your application secrets defined in the Squid Console:
@executable()
async callExternalApi(): Promise<any> {
const apiKey = this.secrets['EXTERNAL_API_KEY'];
const response = await fetch('https://api.example.com/data', {
headers: { 'Authorization': `Bearer ${apiKey}` }
});
return response.json();
}
File uploads (SquidFile)
Executables can receive files uploaded from the client. Files sent as parameters are automatically converted to SquidFile objects.
Client-side:
// Single file
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
const file = fileInput.files[0];
const result = await squid.executeFunction('uploadDocument', file, 'My Document');
// Multiple files
const files = Array.from(fileInput.files);
const result = await squid.executeFunction('uploadDocuments', files);
Backend:
import { executable, SquidService } from '@squidcloud/backend';
import { SquidFile } from '@squidcloud/backend';
export class FileService extends SquidService {
@executable()
async uploadDocument(file: SquidFile, title: string): Promise<string> {
console.log('Original filename:', file.originalName);
console.log('MIME type:', file.mimetype);
console.log('Size (bytes):', file.size);
// Access file content as Uint8Array
const content = file.data;
// Process the file...
return `Uploaded: ${title} (${file.size} bytes)`;
}
@executable()
async uploadDocuments(files: SquidFile[]): Promise<string[]> {
return files.map(f => `Processed: ${f.originalName}`);
}
}
Using the Squid client
Access other Squid services from within an executable using this.squid. This gives you access to the same Database and storage APIs available in the client SDK:
@executable()
async createUserWithData(userData: UserData): Promise<void> {
const usersCollection = this.squid.collection<User>('users');
const userRef = usersCollection.doc(userData.id);
await userRef.insert(userData);
}
For detailed information on database operations, see the Database documentation.
Client-Side Advanced Options
Custom headers
Send custom headers accessible via this.context.headers:
const result = await squid.executeFunctionWithHeaders(
'processOrder',
{
'x-idempotency-key': 'order-123-attempt-1',
'x-client-version': '2.0.0'
},
orderData
);
@executable()
async processOrder(orderData: OrderData): Promise<Order> {
const idempotencyKey = this.context.headers?.['x-idempotency-key'];
const clientVersion = this.context.headers?.['x-client-version'];
// Use idempotency key to prevent duplicate processing
// ...
}
Caching results
Cache expensive function calls on the client to avoid redundant requests:
import { LastUsedValueExecuteFunctionCache } from '@squidcloud/client';
// Create a cache that stores results for 5 minutes
const weatherCache = new LastUsedValueExecuteFunctionCache<WeatherData>({
valueExpirationMillis: 5 * 60 * 1000
});
// Use the cache
const weather = await squid.executeFunction(
{
functionName: 'getWeather',
caching: { cache: weatherCache }
},
'New York'
);
Deduplicating concurrent calls
Prevent duplicate concurrent requests with the same arguments:
// Using default reference comparison
const result = await squid.executeFunction(
{
functionName: 'expensiveCalculation',
deduplication: true
},
inputData
);
// Using serialized value comparison (for object arguments)
import { compareArgsBySerializedValue } from '@squidcloud/client';
const result = await squid.executeFunction(
{
functionName: 'expensiveCalculation',
deduplication: { argsComparator: compareArgsBySerializedValue }
},
inputData
);
Rate Limiting
Protect your executables from abuse using the @limits decorator. You can define rate limits (queries per second) and quota limits (total calls per period), scoped globally, per user, or per IP address.
import { executable, limits, SquidService } from '@squidcloud/backend';
export class RateLimitedService extends SquidService {
@executable()
@limits({ rateLimit: 5, quotaLimit: { value: 100, scope: 'user', renewPeriod: 'monthly' } })
async limitedAction(): Promise<void> {
// ...
}
}
For detailed information on rate and quota limiting, including scoping options, enforcement behavior, and renewal periods, see Rate and quota limiting.
Error Handling
Throwing errors
Throw standard JavaScript errors; they will be serialized and returned to the client:
@executable()
async riskyOperation(data: InputData): Promise<Result> {
if (!data.requiredField) {
throw new Error('requiredField is missing');
}
try {
return await this.performOperation(data);
} catch (error) {
// Log server-side for debugging
console.error('Operation failed:', error);
// Throw a user-friendly message
throw new Error('Operation failed. Please try again.');
}
}
Handling errors on the client
try {
const result = await squid.executeFunction('riskyOperation', data);
console.log('Success:', result);
} catch (error) {
console.error('Function failed:', error.message);
// Handle the error appropriately
}
Common errors
| Error | Cause | Solution |
|---|---|---|
Function not found | Function name doesn't match any @executable | Check spelling; ensure service is exported |
UNAUTHORIZED | assertIsAuthenticated() failed | Ensure user is logged in before calling |
Rate limit exceeded | @limits threshold reached | Implement retry with backoff; adjust limits |
Network error | Connectivity issue | Implement retry logic |
Best Practices
Security
- Always validate authentication for sensitive operations
- Validate all input parameters before processing
- Never expose internal error details to clients
- Use rate limiting on public-facing executables
- Sanitize file uploads by checking type, size, and content
@executable()
@limits({ rateLimit: 10, quotaLimit: { value: 100, scope: 'user', renewPeriod: 'monthly' } })
async secureAction(input: UserInput): Promise<Result> {
// 1. Authenticate
this.assertIsAuthenticated();
// 2. Validate input
if (!input || typeof input.value !== 'string' || input.value.length > 1000) {
throw new Error('Invalid input');
}
// 3. Authorize (check permissions)
const user = this.getUserAuth();
if (!user?.attributes?.['canPerformAction']) {
throw new Error('Permission denied');
}
// 4. Execute with error handling
try {
return await this.doAction(input);
} catch (error) {
console.error('Action failed:', error);
throw new Error('Action failed');
}
}
Performance
- Use client-side caching for expensive, idempotent operations
- Enable deduplication to prevent redundant concurrent calls
- Keep payloads small by returning only necessary data
- Offload heavy processing to schedulers or queues for long-running tasks
Naming conventions
- Use camelCase for function names
- Use descriptive verbs (e.g.,
createOrder,updateProfile,deleteDocument) - Group related functions in the same service class
Code Examples
Database operations
import { executable, SquidService } from '@squidcloud/backend';
interface Product {
id: string;
name: string;
price: number;
stock: number;
}
export class InventoryService extends SquidService {
@executable()
async updateStock(productId: string, quantity: number): Promise<Product> {
this.assertIsAuthenticated();
const products = this.squid.collection<Product>('products');
const productRef = products.doc(productId);
const product = await productRef.snapshot();
if (!product) {
throw new Error(`Product ${productId} not found`);
}
const newStock = product.stock + quantity;
if (newStock < 0) {
throw new Error('Insufficient stock');
}
await productRef.update({ stock: newStock });
return { ...product, stock: newStock };
}
}
For more database operations (queries, transactions, joins), see the Database documentation.
Calling external APIs
import { executable, SquidService } from '@squidcloud/backend';
interface WeatherData {
temperature: number;
conditions: string;
}
export class WeatherService extends SquidService {
@executable()
async getWeather(city: string): Promise<WeatherData> {
const apiKey = this.secrets['WEATHER_API_KEY'];
const response = await fetch(
`https://api.weather.example.com/v1/current?city=${encodeURIComponent(city)}`,
{
headers: { 'X-API-Key': apiKey }
}
);
if (!response.ok) {
throw new Error(`Weather API error: ${response.status}`);
}
return response.json();
}
}
Processing file uploads
This example shows validating an uploaded file and storing it using Squid storage:
import { executable, SquidService } from '@squidcloud/backend';
import { SquidFile } from '@squidcloud/backend';
export class ImageService extends SquidService {
@executable()
async processImage(image: SquidFile): Promise<{ id: string; url: string }> {
this.assertIsAuthenticated();
// Validate file type
if (!image.mimetype.startsWith('image/')) {
throw new Error('Only image files are allowed');
}
// Validate file size (max 5MB)
const maxSize = 5 * 1024 * 1024;
if (image.size > maxSize) {
throw new Error('File size exceeds 5MB limit');
}
// Store in Squid storage
const storage = this.squid.storage('images');
const imageId = crypto.randomUUID();
const dirPath = 'uploads';
const filePath = `${dirPath}/${imageId}-${image.originalName}`;
// Convert SquidFile to File for upload
const file = new File([image.data], image.originalName, { type: image.mimetype });
await storage.uploadFile(dirPath, file);
const { url } = await storage.getDownloadUrl(filePath);
return { id: imageId, url };
}
}
See Also
- Triggers - React to database changes
- Schedulers - Run code on a schedule
- Webhooks - Expose HTTP endpoints
- Rate and quota limiting - Protect your backend functions
- Security Rules - Secure your backend