Skip to main content

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 CaseRecommendation
Call a function from client with server-side logic✅ Executable
React to database changesUse Triggers
Run code on a scheduleUse Schedulers
Expose an HTTP endpoint to external servicesUse Webhooks
Real-time data synchronizationUse Database directly

How it works

  1. You decorate a method with @executable() in a class that extends SquidService
  2. Squid discovers and registers the function at deploy time
  3. Clients call the function using squid.executeFunction('functionName', ...args)
  4. The backend executes the function with full access to secrets, context, and integrations
  5. The result is serialized and returned to the client

Quick Start

Prerequisites

  • A Squid backend project initialized with squid init-backend
  • The @squidcloud/backend package installed

Step 1: Create an executable function

Create a service class that extends SquidService and add your executable function:

Backend code
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:

service/index.ts
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

Client code
const greeting = await squid.executeFunction('greet', 'World');
console.log(greeting); // Output: "Hello, World!"

Authentication and Authorization

Security Warning

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:

Backend code
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:

Backend code
@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)
}
PropertyTypeDescription
appIdstringYour application ID
clientIdstring | undefinedUnique identifier for the calling client
sourceIpstring | undefinedIP address of the caller
headersRecord<string, any> | undefinedHTTP headers (keys are lowercase)

Secrets and API keys

Access your application secrets defined in the Squid Console:

Backend code
@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:

Client code
// 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:

Backend code
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:

Backend code
@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:

Client code
const result = await squid.executeFunctionWithHeaders(
'processOrder',
{
'x-idempotency-key': 'order-123-attempt-1',
'x-client-version': '2.0.0'
},
orderData
);
Backend code
@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:

Client code
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:

Client code
// 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.

Backend code
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:

Backend code
@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

Client code
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

ErrorCauseSolution
Function not foundFunction name doesn't match any @executableCheck spelling; ensure service is exported
UNAUTHORIZEDassertIsAuthenticated() failedEnsure user is logged in before calling
Rate limit exceeded@limits threshold reachedImplement retry with backoff; adjust limits
Network errorConnectivity issueImplement retry logic

Best Practices

Security

  1. Always validate authentication for sensitive operations
  2. Validate all input parameters before processing
  3. Never expose internal error details to clients
  4. Use rate limiting on public-facing executables
  5. Sanitize file uploads by checking type, size, and content
Backend code
@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

  1. Use client-side caching for expensive, idempotent operations
  2. Enable deduplication to prevent redundant concurrent calls
  3. Keep payloads small by returning only necessary data
  4. 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

Backend code
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

Backend code
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:

Backend code
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