@squidcloud/cli 1.0.413 → 1.0.414

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1849 @@
1
+ ---
2
+ name: Squid Development
3
+ description: Provides comprehensive, code-verified knowledge about developing applications with Squid. Use this skill proactively before writing, editing, or creating any code that uses Squid's Client SDK, Backend SDK, or CLI to ensure compliance with Squid's APIs and best practices.
4
+ ---
5
+
6
+ # Squid Development Skill (Verified)
7
+
8
+ This skill provides comprehensive, code-verified knowledge about developing applications with Squid.
9
+
10
+ ## Overview
11
+
12
+ Squid is a backend-as-a-service platform that provides:
13
+ - **Client SDK** (`@squidcloud/client`) - TypeScript/JavaScript SDK for frontend
14
+ - **Backend SDK** (`@squidcloud/backend`) - TypeScript SDK with decorators for backend
15
+ - **CLI** (`@squidcloud/cli`) - Local development and deployment tools
16
+ - **Built-in integrations** - Databases, queues, storage, AI, APIs
17
+
18
+ ## CLI Commands
19
+
20
+ ### Installation
21
+ ```bash
22
+ npm install -g @squidcloud/cli
23
+ ```
24
+
25
+ ### `squid init <project-name>`
26
+ Creates new backend project with `.env` and example service.
27
+
28
+ ```bash
29
+ squid init backend --appId YOUR_APP_ID --apiKey YOUR_API_KEY --environmentId dev --squidDeveloperId YOUR_DEV_ID --region us-east-1.aws
30
+ ```
31
+
32
+ ### `squid start`
33
+ Runs backend locally with hot-reload, connects to Squid Cloud via reverse proxy.
34
+
35
+ ```bash
36
+ cd backend
37
+ squid start
38
+ ```
39
+
40
+ **Extended logging in `.env`:**
41
+ ```env
42
+ SQUID_LOG_TYPES=QUERY,MUTATION,AI,API,ERROR
43
+ ```
44
+
45
+ ### `squid deploy`
46
+ Builds and deploys to Squid Cloud.
47
+
48
+ ```bash
49
+ squid deploy [--apiKey KEY] [--environmentId prod] [--skipBuild]
50
+ ```
51
+
52
+ ### `squid build`
53
+ Builds backend project.
54
+
55
+ ```bash
56
+ squid build [--dev] [--skip-version-check]
57
+ ```
58
+
59
+ ## Client SDK
60
+
61
+ ### Where Can You Use the SDK?
62
+
63
+ The Squid Client SDK can be instantiated in various environments:
64
+ - **Web applications** (vanilla JavaScript, React, Vue, Angular, etc.)
65
+ - **Node.js servers** (Express, Fastify, etc.)
66
+ - **Mobile applications** (React Native, etc.)
67
+ - **Desktop applications** (Electron, etc.)
68
+
69
+ **Important:** When writing backend code that runs on Squid's infrastructure, the Squid client is **already initialized and available** for you (see Backend SDK section). You don't need to manually create a new instance.
70
+
71
+ ### Initialization
72
+
73
+ #### Configuration Options
74
+
75
+ **Required Parameters:**
76
+ - **`appId`** (string): Your Squid application identifier. Can be found in the Squid Console.
77
+ - **`region`** (string): The region where your Squid application is deployed:
78
+ - AWS regions: `'us-east-1.aws'`, `'ap-south-1.aws'`, `'eu-central-1.aws'`
79
+ - GCP regions: `'us-central1.gcp'`
80
+
81
+ **Optional Parameters:**
82
+ - **`apiKey`** (string): API key that bypasses security rules and provides full administrative access.
83
+ - **IMPORTANT**: Has **complete control over your application** and bypasses all security rules
84
+ - Only use in trusted environments (backend servers, admin tools, development)
85
+ - **Never expose in client-side code** (web browsers, mobile apps)
86
+ - **`authProvider`** (object): Authentication provider that supplies OAuth2.0 tokens for the current user
87
+ - `integrationId` (string): The ID of your auth integration
88
+ - `getToken` (function): Returns a promise that resolves to the user's auth token (or undefined if not authenticated)
89
+ - Authentication details are **available in backend functions** via the execution context
90
+ - **`environmentId`** (string): Environment identifier (dev, staging, prod). Defaults to production.
91
+ - **`squidDeveloperId`** (string): Your developer identifier, used for local development and debugging.
92
+ - **`consoleRegion`** (string): Console region (optional)
93
+
94
+ #### Basic Setup
95
+
96
+ ```typescript
97
+ import { Squid } from '@squidcloud/client';
98
+
99
+ const squid = new Squid({
100
+ appId: 'your-app-id',
101
+ region: 'us-east-1.aws',
102
+ environmentId: 'dev' // optional
103
+ });
104
+ ```
105
+
106
+ #### Authentication: API Key vs Auth Provider
107
+
108
+ **Using API Key (Server-Side Only):**
109
+ ```typescript
110
+ const squid = new Squid({
111
+ appId: 'your-app-id',
112
+ region: 'us-east-1.aws',
113
+ apiKey: 'your-api-key' // Full admin access
114
+ });
115
+ ```
116
+ - **Use cases**: Backend servers, admin tools, scripts, development environments
117
+ - **Security**: Has full control - bypasses all security rules
118
+ - **Warning**: Never use in client-side code
119
+
120
+ **Using Auth Provider (Client-Side):**
121
+ ```typescript
122
+ const squid = new Squid({
123
+ appId: 'your-app-id',
124
+ region: 'us-east-1.aws',
125
+ authProvider: {
126
+ integrationId: 'auth-integration-id',
127
+ getToken: async () => {
128
+ // Return user's OAuth token
129
+ return await getCurrentUserToken();
130
+ }
131
+ }
132
+ });
133
+ ```
134
+ - **Use cases**: Web apps, mobile apps, any user-facing application
135
+ - **Security**: Respects security rules based on authenticated user
136
+ - **Backend access**: User authentication details are available in backend functions through the execution context
137
+
138
+ **Setting Auth Provider After Initialization:**
139
+ ```typescript
140
+ const squid = new Squid({
141
+ appId: 'your-app-id',
142
+ region: 'us-east-1.aws'
143
+ });
144
+
145
+ // Later, when user logs in
146
+ squid.setAuthProvider({
147
+ integrationId: 'auth-integration-id',
148
+ getToken: async () => userAuthToken
149
+ });
150
+ ```
151
+
152
+ ### Database & Data Management
153
+
154
+ Squid provides database functionality similar to Firestore but more powerful, with collections, document references, and real-time capabilities. Squid supports multiple database types including NoSQL databases (like MongoDB) and relational databases (like PostgreSQL, MySQL).
155
+
156
+ #### Security Rules
157
+
158
+ Every database operation in Squid can be secured using **security rules**. Security rules are backend functions decorated with `@secureDatabase` or `@secureCollection` that contain business logic to determine whether an operation is allowed.
159
+
160
+ ```typescript
161
+ // Example security rule for a collection (read operations)
162
+ @secureCollection('users', 'read')
163
+ async canReadUsers(context: QueryContext<User>): Promise<boolean> {
164
+ // Your business logic here
165
+ // context.collectionName, context.integrationId, context.limit, etc.
166
+ return true; // or false based on your logic
167
+ }
168
+
169
+ // Example security rule for mutations (insert, update, delete)
170
+ @secureCollection('users', 'update')
171
+ async canUpdateUsers(context: MutationContext): Promise<boolean> {
172
+ // Access document before and after the mutation
173
+ const before = context.before;
174
+ const after = context.after;
175
+
176
+ // Check if specific paths were affected
177
+ if (context.affectsPath('email')) {
178
+ return false; // Don't allow email changes
179
+ }
180
+
181
+ return true;
182
+ }
183
+
184
+ // Database-wide security rule
185
+ @secureDatabase('insert', 'my-integration-id')
186
+ async canInsertAnywhere(context: MutationContext): Promise<boolean> {
187
+ // Your business logic here
188
+ return context.after?.createdBy === 'admin';
189
+ }
190
+ ```
191
+
192
+ These decorated functions return a boolean (or `Promise<boolean>`) indicating whether the operation is allowed. The developer has full control to implement any business logic needed for authorization.
193
+
194
+ **Context types:**
195
+ - `QueryContext` - Used for 'read' and 'all' operations (exported from `@squidcloud/backend`)
196
+ - `MutationContext` - Used for 'insert', 'update', 'delete' operations (exported from `@squidcloud/backend`)
197
+
198
+ Security decorators (`@secureDatabase`, `@secureCollection`) are exported from `@squidcloud/backend`.
199
+
200
+ #### Collection Access
201
+
202
+ A collection represents a set of documents (similar to a table in relational databases or a collection in NoSQL databases).
203
+
204
+ ```typescript
205
+ // Get collection reference in the built-in database
206
+ const users = squid.collection<User>('users');
207
+
208
+ // Get collection reference in a specific integration
209
+ const orders = squid.collection<Order>('orders', 'postgres-db');
210
+ ```
211
+
212
+ **Type parameter**: The generic type `<T>` defines the structure of documents in the collection.
213
+
214
+ **Document References - The `doc()` method has different behaviors:**
215
+
216
+ **1. No parameters - `doc()`**
217
+ Generates a new document ID (useful for creating new documents):
218
+ ```typescript
219
+ // For built_in_db without schema - generates a random ID
220
+ const newDocRef = collection.doc();
221
+ await newDocRef.insert({ name: 'John', age: 30 });
222
+
223
+ // For other integrations - creates a placeholder that gets resolved on insert
224
+ const newDocRef = collection.doc();
225
+ ```
226
+
227
+ **2. String parameter - `doc(stringId)`**
228
+ **Only supported for `built_in_db` integration without a defined schema:**
229
+ ```typescript
230
+ // Valid only for built_in_db collections without schema
231
+ const docRef = collection.doc('my-doc-id');
232
+ const docRef = collection.doc('user-12345');
233
+ ```
234
+ **Important**: For collections with defined schemas or non-built_in_db integrations, you must use object format.
235
+
236
+ **3. Object parameter - `doc({id: value})`**
237
+ Used for collections with defined primary keys. The object maps primary key field names to their values:
238
+ ```typescript
239
+ // Single primary key field "id"
240
+ const docRef = collection.doc({ id: 'user-123' });
241
+
242
+ // Single primary key field "userId"
243
+ const docRef = collection.doc({ userId: 42 });
244
+
245
+ // Composite primary key (id1, id2)
246
+ const docRef = collection.doc({ id1: 'part1', id2: 'part2' });
247
+
248
+ // Partial primary key - missing fields generated on server if supported
249
+ const docRef = collection.doc({ id1: 'part1' }); // id2 generated on insert
250
+ ```
251
+
252
+ **Key points about document IDs:**
253
+ - For `built_in_db` without schema: can use string IDs or object format
254
+ - For `built_in_db` with schema: must use object format matching primary key fields
255
+ - For other integrations: must use object format matching primary key fields
256
+ - Partial primary keys: missing fields may be auto-generated on insert (if integration supports it)
257
+ - Empty `doc()`: generates a placeholder ID that gets resolved when document is created
258
+
259
+ ```typescript
260
+ // Examples:
261
+
262
+ // Insert
263
+ await userDoc.insert({ name: 'John', email: 'john@example.com' });
264
+
265
+ // Update (partial)
266
+ await userDoc.update({ name: 'Jane' });
267
+
268
+ // Update specific path
269
+ await userDoc.setInPath('address.street', 'Main St');
270
+
271
+ // Delete specific path
272
+ await userDoc.deleteInPath('tempData');
273
+
274
+ // Delete document
275
+ await userDoc.delete();
276
+
277
+ // Get data (promise)
278
+ const userData = await userDoc.snapshot();
279
+ console.log(userData);
280
+
281
+ // Get data (observable - realtime)
282
+ userDoc.snapshots().subscribe(data => {
283
+ console.log(data);
284
+ });
285
+
286
+ // Get cached data (no server fetch)
287
+ const cached = userDoc.peek();
288
+
289
+ // Bulk operations
290
+ await users.insertMany([
291
+ { id: 'user-1', data: { name: 'Alice' } },
292
+ { id: 'user-2', data: { name: 'Bob' } }
293
+ ]);
294
+
295
+ await users.deleteMany(['user-1', 'user-2']);
296
+ ```
297
+
298
+ #### Real-time Subscriptions
299
+
300
+ Squid provides real-time data synchronization similar to Firestore. Subscribe to document or query changes and receive updates automatically.
301
+
302
+ **Document Subscriptions:**
303
+ ```typescript
304
+ // Subscribe to document changes
305
+ const subscription = docRef.snapshots().subscribe((userData) => {
306
+ if (userData) {
307
+ console.log('Document updated:', userData);
308
+ } else {
309
+ console.log('Document deleted or does not exist');
310
+ }
311
+ });
312
+
313
+ // Unsubscribe when done
314
+ subscription.unsubscribe();
315
+ ```
316
+
317
+ **Query Subscriptions:**
318
+ ```typescript
319
+ // Subscribe to query results - ALWAYS use dereference()
320
+ const subscription = collection
321
+ .query()
322
+ .eq('status', 'active')
323
+ .gte('age', 18)
324
+ .dereference() // Important: Converts DocumentReferences to actual data
325
+ .snapshots()
326
+ .subscribe((users) => {
327
+ console.log('Active users updated:', users);
328
+ // users is Array<User> with actual data
329
+ });
330
+
331
+ // Unsubscribe when done
332
+ subscription.unsubscribe();
333
+ ```
334
+
335
+ **Real-time Updates:**
336
+ When data changes on the server (from any client or backend operation):
337
+ - Document subscriptions receive the updated document data
338
+ - Query subscriptions receive the updated query results
339
+ - Updates are pushed to clients via WebSocket connections
340
+ - Changes are automatically reflected in the local data
341
+
342
+ **Important**: Always unsubscribe from subscriptions when they're no longer needed to prevent memory leaks.
343
+
344
+ ### Database - Queries
345
+
346
+ **ALL Available Operators:**
347
+ - Comparison: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`
348
+ - Arrays: `in`, `nin`, `arrayIncludesSome`, `arrayIncludesAll`, `arrayNotIncludes`
349
+ - Patterns: `like`, `notLike` (% = any chars, _ = one char)
350
+
351
+ ```typescript
352
+ // Basic query
353
+ const activeUsers = await users.query()
354
+ .eq('status', 'active')
355
+ .gt('age', 18)
356
+ .sortBy('name', true) // true = ascending
357
+ .limit(100) // max 20000, default 1000
358
+ .snapshot();
359
+
360
+ activeUsers.data.forEach(doc => {
361
+ console.log(doc.data); // Document data
362
+ });
363
+
364
+ // Realtime query
365
+ users.query()
366
+ .eq('status', 'active')
367
+ .snapshots().subscribe(snapshot => {
368
+ snapshot.data.forEach(doc => console.log(doc.data));
369
+ });
370
+
371
+ // Pattern matching
372
+ const results = await users.query()
373
+ .like('email', '%.com') // case-insensitive by default
374
+ .snapshot();
375
+
376
+ // Array operators
377
+ const tagged = await posts.query()
378
+ .in('category', ['tech', 'news'])
379
+ .arrayIncludesSome('tags', ['urgent', 'important'])
380
+ .snapshot();
381
+
382
+ // Using where() method (all operators above are shortcuts for where)
383
+ const results1 = await users.query().eq('status', 'active').snapshot();
384
+ const results2 = await users.query().where('status', '==', 'active').snapshot();
385
+ // These are equivalent
386
+
387
+ // OR queries - combine multiple queries with OR logic
388
+ const query1 = users.query().eq('status', 'active');
389
+ const query2 = users.query().eq('status', 'pending');
390
+ const orResults = await users.or(query1, query2)
391
+ .dereference()
392
+ .snapshot();
393
+ // Returns documents matching either query
394
+ // Note: Results are merged and deduplicated
395
+
396
+ // Multiple sort
397
+ const sorted = await users.query()
398
+ .sortBy('lastName', true)
399
+ .sortBy('firstName', true)
400
+ .limit(50)
401
+ .snapshot();
402
+
403
+ // Join Queries - combine data from multiple collections
404
+ // Start with joinQuery() and alias
405
+ const results = await teachers
406
+ .joinQuery('teacher') // Alias for teachers collection
407
+ .join(
408
+ students.query(),
409
+ 'students', // Alias for students collection
410
+ { left: 'id', right: 'teacherId' } // Join condition
411
+ )
412
+ .dereference() // Important: converts refs to actual data
413
+ .snapshot();
414
+ // Results: Array<{ teacher: Teacher, students?: Student }>
415
+
416
+ // Inner join (only matching records)
417
+ const innerResults = await teachers
418
+ .joinQuery('teacher')
419
+ .join(
420
+ students.query(),
421
+ 'students',
422
+ { left: 'id', right: 'teacherId' },
423
+ { isInner: true } // Inner join option
424
+ )
425
+ .dereference()
426
+ .snapshot();
427
+
428
+ // Multi-level joins
429
+ const threeWay = await collection1
430
+ .joinQuery('a')
431
+ .join(collection2.query(), 'b', { left: 'id', right: 'parentId' })
432
+ .join(collection3.query(), 'c', { left: 'id', right: 'grandParentId' })
433
+ .dereference()
434
+ .snapshot();
435
+
436
+ // Grouped joins (nests one-to-many as arrays)
437
+ const grouped = await teachers
438
+ .joinQuery('teacher')
439
+ .join(students.query(), 'students', { left: 'id', right: 'teacherId' })
440
+ .grouped()
441
+ .dereference()
442
+ .snapshot();
443
+ // Results: Array<{ teacher: Teacher, students: Student[] }>
444
+
445
+ // Dereference - Converts DocumentReferences to actual data
446
+ // WITHOUT dereference: returns Array<DocumentReference<User>>
447
+ const refs = await users.query().eq('active', true).snapshot();
448
+ // refs.data[0].data to access actual data
449
+
450
+ // WITH dereference: returns Array<User> directly
451
+ const userData = await users.query()
452
+ .eq('active', true)
453
+ .dereference()
454
+ .snapshot();
455
+ // userData[0] is the actual user object
456
+
457
+ // ALWAYS use dereference() for:
458
+ // - Real-time subscriptions (makes working with data easier)
459
+ // - When you need document data directly
460
+ // DON'T use dereference() if:
461
+ // - You need DocumentReference methods like .update() or .delete()
462
+
463
+ // Pagination
464
+ const pagination = users.query()
465
+ .sortBy('createdAt', false)
466
+ .dereference()
467
+ .paginate({
468
+ pageSize: 50, // Number of items per page (default: 100)
469
+ subscribe: true // Subscribe to real-time updates (default: true)
470
+ });
471
+
472
+ const firstPage = await pagination.first(); // Jump to first page
473
+ const nextPage = await pagination.next(); // Go to next page
474
+ const prevPage = await pagination.prev(); // Go to previous page
475
+
476
+ // Check pagination state
477
+ console.log(firstPage.hasNext); // boolean
478
+ console.log(firstPage.hasPrev); // boolean
479
+
480
+ // Watch changes
481
+ users.query()
482
+ .eq('status', 'active')
483
+ .changes()
484
+ .subscribe(changes => {
485
+ console.log('Inserts:', changes.inserts);
486
+ console.log('Updates:', changes.updates);
487
+ console.log('Deletes:', changes.deletes);
488
+ });
489
+ ```
490
+
491
+ **Note:** `offset()` does NOT exist - use `paginate()` for pagination.
492
+
493
+ ### Database - Transactions
494
+
495
+ ```typescript
496
+ await squid.runInTransaction(async (txId) => {
497
+ const userRef = squid.collection('users').doc('user-1');
498
+ const accountRef = squid.collection('accounts').doc('acc-1');
499
+
500
+ // Pass txId as last parameter to each operation
501
+ // Use incrementInPath/decrementInPath for numeric operations
502
+ await userRef.decrementInPath('balance', 100, txId);
503
+ await accountRef.incrementInPath('balance', 100, txId);
504
+
505
+ // Both commit together or rollback together
506
+ });
507
+ ```
508
+
509
+ ### Database - Numeric Operations
510
+
511
+ ```typescript
512
+ // Increment/decrement
513
+ await userDoc.incrementInPath('loginCount', 1);
514
+ await userDoc.decrementInPath('credits', 50);
515
+
516
+ // For arrays/objects, use update() with full new value
517
+ const currentData = await userDoc.snapshot();
518
+ await userDoc.update({
519
+ tags: [...currentData.tags, 'newTag'],
520
+ notifications: [...currentData.notifications, { msg: 'Hi' }]
521
+ });
522
+ ```
523
+
524
+ ### Database - Native Queries
525
+
526
+ ```typescript
527
+ // SQL (PostgreSQL, MySQL, etc.) - uses ${param} syntax
528
+ const users = await squid.executeNativeRelationalQuery<User[]>(
529
+ 'postgres-db',
530
+ 'SELECT * FROM users WHERE age > ${minAge}',
531
+ { minAge: 18 }
532
+ );
533
+
534
+ // MongoDB aggregation
535
+ const orders = await squid.executeNativeMongoQuery<Order[]>(
536
+ 'mongo-db',
537
+ 'orders',
538
+ [
539
+ { $match: { status: 'completed' } },
540
+ { $group: { _id: '$userId', total: { $sum: '$amount' } } }
541
+ ]
542
+ );
543
+
544
+ // Elasticsearch
545
+ const products = await squid.executeNativeElasticQuery(
546
+ 'elastic-db',
547
+ 'products',
548
+ { query: { match: { name: 'laptop' } } },
549
+ '_search', // endpoint (optional)
550
+ 'GET' // method (optional)
551
+ );
552
+
553
+ // Pure (Finos Legend Pure Language) - uses ${param} syntax
554
+ const items = await squid.executeNativePureQuery(
555
+ 'my-db',
556
+ 'from products->filter(p | $p.price < ${maxPrice})',
557
+ { maxPrice: 1000 }
558
+ );
559
+ ```
560
+
561
+ ### Database - Security (@secureDatabase, @secureCollection)
562
+
563
+ Secure your database operations with backend decorators. These are backend-only decorators that define who can access your data.
564
+
565
+ **Backend - Secure entire database:**
566
+ ```typescript
567
+ import { SquidService, secureDatabase, QueryContext, MutationContext } from '@squidcloud/backend';
568
+
569
+ export class MyService extends SquidService {
570
+ @secureDatabase('read')
571
+ allowRead(context: QueryContext<User>): boolean {
572
+ const userId = this.getUserAuth()?.userId;
573
+ if (!userId) return false;
574
+ return context.isSubqueryOf('userId', '==', userId);
575
+ }
576
+
577
+ @secureDatabase('write')
578
+ allowWrite(context: MutationContext<User>): boolean {
579
+ return this.isAuthenticated();
580
+ }
581
+
582
+ @secureDatabase('all')
583
+ allowAll(): boolean {
584
+ return this.isAuthenticated();
585
+ }
586
+ }
587
+ ```
588
+
589
+ **Types:** `'read'`, `'write'`, `'update'`, `'insert'`, `'delete'`, `'all'`
590
+
591
+ **Backend - Secure specific collection:**
592
+ ```typescript
593
+ // QueryContext methods (for 'read' operations)
594
+ @secureCollection('users', 'read')
595
+ allowUserRead(context: QueryContext): boolean {
596
+ const userId = this.getUserAuth()?.userId;
597
+ if (!userId) return false;
598
+ // Check if query filters on a specific field
599
+ return context.isSubqueryOf('id', '==', userId);
600
+ }
601
+
602
+ // MutationContext methods (for 'insert', 'update', 'delete' operations)
603
+ @secureCollection('users', 'update')
604
+ allowUserUpdate(context: MutationContext<User>): boolean {
605
+ const userId = this.getUserAuth()?.userId;
606
+ if (!userId) return false;
607
+
608
+ // context.before: document state before mutation
609
+ // context.after: document state after mutation
610
+
611
+ // Check if specific paths were affected
612
+ if (context.affectsPath('email')) {
613
+ return false; // Don't allow email changes
614
+ }
615
+
616
+ // Check ownership
617
+ return context.after?.id === userId;
618
+ }
619
+ ```
620
+
621
+ **Context types (exported from `@squidcloud/backend`):**
622
+ - `QueryContext<T>` - Used for 'read' and 'all' operations
623
+ - `isSubqueryOf(field, operator, value)` - Check if query filters on field
624
+ - `collectionName`, `integrationId`, `limit` properties
625
+ - `MutationContext<T>` - Used for 'insert', 'update', 'delete' operations
626
+ - `affectsPath(path)` - Check if specific field path was modified
627
+ - `before` - Document before mutation (undefined for insert)
628
+ - `after` - Document after mutation (undefined for delete)
629
+
630
+ ### Backend Functions (@executable)
631
+
632
+ Backend functions allow you to write custom server-side logic that can be called from the client.
633
+
634
+ **Backend - Define the function:**
635
+ ```typescript
636
+ import { SquidService, executable, SquidFile } from '@squidcloud/backend';
637
+
638
+ export class MyService extends SquidService {
639
+ @executable()
640
+ async greetUser(name: string): Promise<string> {
641
+ return `Hello, ${name}`;
642
+ }
643
+
644
+ @executable()
645
+ async uploadFile(file: SquidFile): Promise<Result> {
646
+ console.log(file.originalName, file.size, file.data);
647
+ return { success: true };
648
+ }
649
+ }
650
+ ```
651
+
652
+ **Client - Call the function:**
653
+ ```typescript
654
+ // Execute @executable() decorated function
655
+ const result = await squid.executeFunction('greetUser', 'John');
656
+ const typedResult = await squid.executeFunction<string>('greetUser', 'John');
657
+
658
+ // With headers
659
+ const result = await squid.executeFunctionWithHeaders(
660
+ 'processPayment',
661
+ { 'X-Custom-Header': 'value' },
662
+ paymentData
663
+ );
664
+ ```
665
+
666
+ ### Webhooks (@webhook)
667
+
668
+ Webhooks allow you to create publicly accessible HTTP endpoints that can receive data from external services.
669
+
670
+ **Backend - Define the webhook:**
671
+ ```typescript
672
+ import { SquidService, webhook, WebhookRequest } from '@squidcloud/backend';
673
+
674
+ export class MyService extends SquidService {
675
+ @webhook('github-events')
676
+ async handleGithub(request: WebhookRequest): Promise<any> {
677
+ console.log(request.body);
678
+ console.log(request.headers);
679
+ console.log(request.queryParams);
680
+ console.log(request.httpMethod); // 'post' | 'get' | 'put' | 'delete'
681
+ console.log(request.files);
682
+
683
+ return this.createWebhookResponse({ received: true }, 200);
684
+ }
685
+ }
686
+ ```
687
+
688
+ **Webhook URL:** `https://<appId>.<region>.squid.cloud/webhooks/<webhook-id>`
689
+
690
+ **Client - Call webhook programmatically (optional):**
691
+ ```typescript
692
+ // Usually webhooks are called by external services, but you can also call them from client
693
+ const result = await squid.executeWebhook<Response>('github-events', {
694
+ headers: { 'X-GitHub-Event': 'push' },
695
+ queryParams: { ref: 'main' },
696
+ body: { commits: [...] },
697
+ files: [file1, file2]
698
+ });
699
+ ```
700
+
701
+ ### AI - Supported Models
702
+
703
+ Squid supports multiple AI providers and models for different use cases:
704
+
705
+ **Chat Models** (for AI Agents, AI Query, etc.):
706
+
707
+ *OpenAI:*
708
+ - `o1`, `o3`, `o3-mini`, `o4-mini`
709
+ - `gpt-5`, `gpt-5-mini`, `gpt-5-nano`
710
+ - `gpt-4.1` (default), `gpt-4.1-mini`, `gpt-4.1-nano`
711
+ - `gpt-4o`, `gpt-4o-mini`
712
+
713
+ *Anthropic (Claude):*
714
+ - `claude-3-7-sonnet-latest`
715
+ - `claude-haiku-4-5-20251001`
716
+ - `claude-opus-4-20250514`, `claude-opus-4-1-20250805`
717
+ - `claude-sonnet-4-20250514`, `claude-sonnet-4-5-20250929`
718
+
719
+ *Google (Gemini):*
720
+ - `gemini-2.5-pro`, `gemini-2.5-flash`, `gemini-2.5-flash-lite`
721
+
722
+ *xAI (Grok):*
723
+ - `grok-3`, `grok-3-fast`, `grok-3-mini`, `grok-3-mini-fast`
724
+ - `grok-4`, `grok-4-fast-reasoning`, `grok-4-fast-non-reasoning`
725
+ - Note: `grok-3-mini` is ~10x less expensive than `grok-3`
726
+ - Note: `*-fast` models are ~2x more expensive and only marginally faster
727
+
728
+ **Embedding Models** (for Knowledge Bases):
729
+ - `text-embedding-3-small` (OpenAI, default) - 8,192 token limit
730
+ - `voyage-3-large` (Voyage) - 32,000 token limit
731
+
732
+ **Image Generation Models:**
733
+ - `dall-e-3` (OpenAI)
734
+ - `stable-diffusion-core` (Stability)
735
+ - `flux-pro-1.1`, `flux-kontext-pro` (Flux)
736
+
737
+ **Audio Models:**
738
+
739
+ *Transcription:*
740
+ - `whisper-1`
741
+ - `gpt-4o-transcribe`, `gpt-4o-mini-transcribe`
742
+
743
+ *Text-to-Speech:*
744
+ - `tts-1`, `tts-1-hd`, `gpt-4o-mini-tts`
745
+
746
+ ### AI - Agents
747
+
748
+ AI Agents are powerful, configurable AI assistants that can interact with your data, call functions, connect to integrations, and collaborate with other agents.
749
+
750
+ **An AI Agent can:**
751
+ - Maintain conversation history (**memory is enabled by default**)
752
+ - Call backend functions decorated with `@aiFunction`
753
+ - Query databases and APIs through connected integrations
754
+ - Search knowledge bases for relevant information
755
+ - Collaborate with other AI agents
756
+ - Process voice input and generate voice output
757
+ - Accept files as part of chat requests
758
+
759
+ **Creating and Managing Agents:**
760
+
761
+ ```typescript
762
+ const aiClient = squid.ai();
763
+
764
+ // Get the built-in agent (no ID required)
765
+ const builtInAgent = aiClient.agent();
766
+
767
+ // Get a custom agent by ID
768
+ const myAgent = aiClient.agent('my-agent-id');
769
+
770
+ // Get an agent with an Agent API key (allows calling without App API key, bypasses @secureAiAgent methods)
771
+ const agentWithKey = aiClient.agent('my-agent-id', {
772
+ apiKey: 'your-agent-api-key'
773
+ });
774
+
775
+ // Create or update an agent
776
+ await myAgent.upsert({
777
+ description: 'Customer support assistant',
778
+ isPublic: false, // Whether the agent is publicly accessible
779
+ auditLog: true, // Enable audit logging for compliance
780
+ options: {
781
+ model: 'gpt-4o', // or 'claude-sonnet-4-5-20250929', 'gemini-2.5-flash'
782
+ instructions: 'You are a helpful customer support assistant. Be concise and professional.',
783
+ temperature: 0.7
784
+ }
785
+ });
786
+
787
+ // Get agent details
788
+ const agentInfo = await myAgent.get();
789
+ console.log(agentInfo.id, agentInfo.description, agentInfo.options.model);
790
+
791
+ // Update specific properties
792
+ await myAgent.updateInstructions('You are a technical support specialist.');
793
+ await myAgent.updateModel('claude-sonnet-4-5-20250929');
794
+ await myAgent.updateGuardrails(['no-harmful-content']);
795
+
796
+ // Delete an agent
797
+ await myAgent.delete();
798
+
799
+ // List all agents
800
+ const agents = await squid.ai().listAgents();
801
+ agents.forEach(agent => console.log(`${agent.id}: ${agent.description}`));
802
+ ```
803
+
804
+ **Agent Lifecycle:**
805
+ 1. **Creation** - Use `upsert()` to create a new agent with an ID
806
+ 2. **Active** - Agent can process requests
807
+ 3. **Update** - Use `upsert()` or specific update methods
808
+ 4. **Deletion** - Use `delete()` to permanently remove the agent
809
+
810
+ **Important Notes:**
811
+ - Agent IDs are permanent - once created, you cannot change the ID
812
+ - Deleting an agent does not delete associated chat history
813
+ - The built-in agent (accessed via `agent()` with no ID) cannot be deleted
814
+ - **Agent API Keys**: You can pass an options object with an `apiKey` when calling `agent()`:
815
+ - Allows calling agent methods without requiring an App API key
816
+ - Bypasses any `@secureAiAgent` security methods when calling agent ask functions
817
+ - Useful for direct agent access without app-level authentication
818
+
819
+ **Connecting Resources to Agents:**
820
+
821
+ Agents become powerful when connected to resources. You can connect agents to:
822
+ - **Other AI agents** (`connectedAgents`) - Agent collaboration
823
+ - **Backend functions** (`functions`) - Custom logic via `@aiFunction` decorators
824
+ - **Integrations** (`connectedIntegrations`) - Database/API queries
825
+ - **Knowledge bases** (`connectedKnowledgeBases`) - RAG (Retrieval Augmented Generation)
826
+
827
+ **1. Connected Agents (`connectedAgents`):**
828
+
829
+ Allow one agent to call another agent as a specialized sub-task handler.
830
+
831
+ ```typescript
832
+ // Define connected agents in upsert or per-request
833
+ const response = await agent.ask('Analyze our sales data and send report to team', {
834
+ connectedAgents: [
835
+ {
836
+ agentId: 'data-analyst-agent',
837
+ description: 'Analyzes sales data and generates reports'
838
+ },
839
+ {
840
+ agentId: 'email-sender-agent',
841
+ description: 'Sends emails to team members'
842
+ }
843
+ ]
844
+ });
845
+
846
+ // The main agent can now call these sub-agents when needed
847
+ // Agent orchestration happens automatically based on the prompt
848
+ ```
849
+
850
+ **2. Functions (`functions`):**
851
+
852
+ Connect backend functions decorated with `@aiFunction` that the agent can call.
853
+
854
+ ```typescript
855
+ // Backend function
856
+ @aiFunction('Gets current inventory for a product', [
857
+ { name: 'productId', type: 'string', required: true, description: 'Product ID' }
858
+ ])
859
+ async getInventory({ productId }: { productId: string }): Promise<number> {
860
+ return await db.inventory.get(productId);
861
+ }
862
+
863
+ // Client - Agent uses the function
864
+ const response = await agent.ask('How many units of product-123 do we have?', {
865
+ functions: ['getInventory'] // Function name or ID
866
+ });
867
+ // Agent automatically calls getInventory() and includes result in response
868
+
869
+ // With predefined parameters (hidden from AI)
870
+ const response = await agent.ask('Send notification', {
871
+ functions: [
872
+ {
873
+ functionId: 'sendEmail',
874
+ context: { apiKey: 'secret-key' } // Passed to function but hidden from AI
875
+ }
876
+ ]
877
+ });
878
+ ```
879
+
880
+ **3. Connected Integrations (`connectedIntegrations`):**
881
+
882
+ Connect database or API integrations so the agent can query them directly.
883
+
884
+ ```typescript
885
+ const response = await agent.ask('Show me active users from last week', {
886
+ connectedIntegrations: [
887
+ {
888
+ integrationId: 'postgres-db',
889
+ integrationType: 'database',
890
+ description: 'Main application database with users, orders, and products',
891
+ instructions: 'Only query the users table unless explicitly asked otherwise',
892
+ options: {
893
+ // Integration-specific options (varies by integration type)
894
+ }
895
+ },
896
+ {
897
+ integrationId: 'stripe-api',
898
+ integrationType: 'api',
899
+ description: 'Stripe payment API for retrieving payment information'
900
+ }
901
+ ]
902
+ });
903
+
904
+ // Agent can now query the database and API to answer questions
905
+ ```
906
+
907
+ **4. Connected Knowledge Bases (`connectedKnowledgeBases`):**
908
+
909
+ Connect knowledge bases for RAG (Retrieval Augmented Generation) - agent retrieves relevant context before answering.
910
+
911
+ ```typescript
912
+ const response = await agent.ask('What is our return policy?', {
913
+ connectedKnowledgeBases: [
914
+ {
915
+ knowledgeBaseId: 'company-policies-kb',
916
+ description: 'Use this when asked about company policies, procedures, or guidelines'
917
+ },
918
+ {
919
+ knowledgeBaseId: 'product-docs-kb',
920
+ description: 'Use this when asked about product features, specifications, or usage'
921
+ }
922
+ ]
923
+ });
924
+
925
+ // Agent searches knowledge bases for relevant information before answering
926
+ ```
927
+
928
+ **Complete Agent Chat Options:**
929
+
930
+ ```typescript
931
+ const agent = squid.ai().agent('my-agent');
932
+
933
+ // Chat (streaming) - Returns RxJS Observable
934
+ // IMPORTANT: Streaming behavior:
935
+ // - NO connected resources: streams token-by-token
936
+ // - HAS connected resources: emits ONCE with complete response
937
+ const chatObs = agent.chat('What is your return policy?', {
938
+ // Memory management
939
+ memoryOptions: {
940
+ memoryMode: 'read-write', // 'none' | 'read-only' | 'read-write'
941
+ memoryId: 'user-123', // Unique per user/session
942
+ expirationMinutes: 1440 // 24 hours
943
+ },
944
+
945
+ // Connected resources (can also be set on agent.upsert)
946
+ connectedAgents: [{ agentId: 'specialist-agent', description: 'Handles X' }],
947
+ functions: ['function1', 'function2'], // or with context: [{ functionId: 'fn', context: {...} }]
948
+ connectedIntegrations: [{ integrationId: 'db', integrationType: 'database', description: '...' }],
949
+ connectedKnowledgeBases: [{ knowledgeBaseId: 'kb1', description: 'When to use this KB' }],
950
+
951
+ // Model & generation
952
+ model: 'gpt-4o', // Override agent's default model
953
+ temperature: 0.7,
954
+ maxTokens: 4000,
955
+ maxOutputTokens: 2000,
956
+ instructions: 'Additional instructions for this request only',
957
+ responseFormat: 'json_object', // or 'text'
958
+ verbosity: 'medium', // 'low' | 'medium' | 'high' (OpenAI only)
959
+ reasoningEffort: 'high', // 'minimal' | 'low' | 'medium' | 'high' (for reasoning models)
960
+
961
+ // Context & RAG
962
+ disableContext: false, // Disable all context
963
+ enablePromptRewriteForRag: false, // Rewrite prompt for better RAG results
964
+ includeReference: false, // Include source references in response
965
+ contextMetadataFilterForKnowledgeBase: {
966
+ 'kb-id': { category: 'documentation' } // Filter KB contexts by metadata
967
+ },
968
+
969
+ // Guardrails & quotas
970
+ guardrails: ['no-harmful-content', 'no-pii'], // Preset safety rules
971
+ quotas: {
972
+ maxAiCallStackSize: 5 // Max depth for nested agent calls
973
+ },
974
+
975
+ // Files & voice
976
+ fileUrls: [
977
+ { id: 'file1', type: 'image', purpose: 'context', url: 'https://...', description: 'Product image' }
978
+ ],
979
+ voiceOptions: {
980
+ modelName: 'tts-1',
981
+ voice: 'alloy', // alloy, ash, ballad, coral, echo, fable, onyx, nova, sage, shimmer, verse
982
+ speed: 1.0
983
+ },
984
+
985
+ // Advanced
986
+ smoothTyping: true, // Smooth typing effect (default: true)
987
+ useCodeInterpreter: 'llm', // 'none' | 'llm' (OpenAI/Gemini only)
988
+ executionPlanOptions: {
989
+ enabled: true, // Agent plans which resources to use
990
+ model: 'gpt-4o',
991
+ reasoningEffort: 'high'
992
+ },
993
+ agentContext: { userId: '123', role: 'admin' }, // Global context passed to all functions
994
+ includeMetadata: false // Include context metadata in response
995
+ });
996
+
997
+ chatObs.subscribe(text => {
998
+ console.log(text);
999
+ });
1000
+
1001
+ // Ask (complete response - same options as chat)
1002
+ const response = await agent.ask('Question?', { /* same options */ });
1003
+
1004
+ // Ask with annotations (includes file references, citations)
1005
+ const result = await agent.askWithAnnotations('Question?');
1006
+ console.log(result.responseString);
1007
+ console.log(result.annotations);
1008
+
1009
+ // Voice responses
1010
+ const voiceResult = await agent.askWithVoiceResponse('Hello', {
1011
+ voiceOptions: { modelName: 'tts-1', voice: 'alloy', speed: 1.0 }
1012
+ });
1013
+ console.log(voiceResult.responseString);
1014
+ console.log(voiceResult.voiceResponseFile);
1015
+
1016
+ // Transcribe audio and chat
1017
+ const transcribeResult = await agent.transcribeAndChat(audioFile);
1018
+ console.log(transcribeResult.transcribedPrompt);
1019
+ transcribeResult.responseStream.subscribe(text => console.log(text));
1020
+
1021
+ // Get chat history
1022
+ const history = await agent.getChatHistory('memory-id-123');
1023
+
1024
+ // Semantic search across agent's knowledge
1025
+ const results = await agent.search({ prompt: 'product docs', limit: 10 });
1026
+
1027
+ // Provide feedback on responses
1028
+ await agent.provideFeedback('thumbs_up'); // or 'thumbs_down'
1029
+
1030
+ // Observe status updates (for long-running operations)
1031
+ agent.observeStatusUpdates().subscribe(status => {
1032
+ console.log(status.title, status.tags);
1033
+ });
1034
+ ```
1035
+
1036
+ **Agent Security (@secureAiAgent):**
1037
+
1038
+ Secure agent access on the backend:
1039
+
1040
+ **Backend:**
1041
+ ```typescript
1042
+ import { SquidService, secureAiAgent } from '@squidcloud/backend';
1043
+
1044
+ export class MyService extends SquidService {
1045
+ // Secure specific agent
1046
+ @secureAiAgent('customer-support-agent')
1047
+ allowCustomerAgent(): boolean {
1048
+ return this.isAuthenticated();
1049
+ }
1050
+
1051
+ // Secure all agents
1052
+ @secureAiAgent()
1053
+ allowAllAgents(): boolean {
1054
+ return this.isAuthenticated();
1055
+ }
1056
+ }
1057
+ ```
1058
+
1059
+ **Important Notes:**
1060
+ - Connected resources can be set on agent creation/update OR per-request
1061
+ - Per-request settings override agent-level settings
1062
+ - Agent automatically decides when to use connected resources based on the prompt
1063
+ - `executionPlanOptions` enables the agent to create a plan before using resources (improves accuracy)
1064
+ - `chatId` and `profileId` are deprecated - use `memoryOptions` instead
1065
+
1066
+ ### AI - Knowledge Bases
1067
+
1068
+ ```typescript
1069
+ const kb = squid.ai().knowledgeBase('product-docs');
1070
+
1071
+ // Upsert context (with or without file)
1072
+ await kb.upsertContext({
1073
+ contextId: 'doc-123',
1074
+ text: 'Product documentation...',
1075
+ metadata: { category: 'user-guide', version: '2.0' }
1076
+ }, optionalFile);
1077
+
1078
+ // Batch upsert
1079
+ await kb.upsertContexts([
1080
+ { contextId: 'doc-1', text: '...' },
1081
+ { contextId: 'doc-2', text: '...' }
1082
+ ], optionalFiles);
1083
+
1084
+ // Search with prompt
1085
+ const results = await kb.searchContextsWithPrompt({
1086
+ prompt: 'How do I reset password?',
1087
+ limit: 5,
1088
+ metadata: { category: 'user-guide' }
1089
+ });
1090
+
1091
+ // Search by context ID
1092
+ const related = await kb.searchContextsWithContextId({
1093
+ contextId: 'doc-123',
1094
+ limit: 10
1095
+ });
1096
+
1097
+ // Semantic search (returns chunks)
1098
+ const searchResults = await kb.search({
1099
+ prompt: 'authentication',
1100
+ limit: 10
1101
+ });
1102
+
1103
+ // Get context
1104
+ const context = await kb.getContext('doc-123');
1105
+
1106
+ // List contexts
1107
+ const allContexts = await kb.listContexts(1000); // truncateTextAfter
1108
+ const contextIds = await kb.listContextIds();
1109
+
1110
+ // Download context
1111
+ const download = await kb.downloadContext('doc-123');
1112
+
1113
+ // Delete contexts
1114
+ await kb.deleteContext('doc-123');
1115
+ await kb.deleteContexts(['doc-1', 'doc-2']);
1116
+
1117
+ // List all knowledge bases
1118
+ const allKBs = await squid.ai().listKnowledgeBases();
1119
+ ```
1120
+
1121
+ ### AI - Image & Audio
1122
+
1123
+ ```typescript
1124
+ // Image generation
1125
+ const imageUrl = await squid.ai().image().generate(
1126
+ 'A futuristic city',
1127
+ { size: '1024x1024', quality: 'hd' }
1128
+ );
1129
+
1130
+ // Remove background
1131
+ const noBgBase64 = await squid.ai().image().removeBackground(imageFile);
1132
+
1133
+ // Transcribe audio
1134
+ const text = await squid.ai().audio().transcribe(audioFile, {
1135
+ language: 'en'
1136
+ });
1137
+
1138
+ // Text-to-speech
1139
+ const audioFile = await squid.ai().audio().createSpeech(
1140
+ 'Hello world',
1141
+ { modelName: 'tts-1', voice: 'alloy', speed: 1.0 }
1142
+ );
1143
+ ```
1144
+
1145
+ ### AI - Query & API Call
1146
+
1147
+ AI Query allows you to query your database using natural language prompts. Squid's AI converts your prompt into database queries and returns the results.
1148
+
1149
+ ```typescript
1150
+ // Natural language database query (basic)
1151
+ const result = await squid.ai().executeAiQuery(
1152
+ 'built_in_db',
1153
+ 'Show me all active users who registered last month'
1154
+ );
1155
+
1156
+ // With options
1157
+ const result = await squid.ai().executeAiQuery(
1158
+ 'built_in_db',
1159
+ 'Show me all active users who registered last month',
1160
+ {
1161
+ instructions: 'Only query the users collection',
1162
+ generateWalkthrough: true, // Get step-by-step explanation
1163
+ enableRawResults: true, // Include raw query results
1164
+ selectCollectionsOptions: {
1165
+ collectionsToUse: ['users'] // Limit to specific collections
1166
+ },
1167
+ generateQueryOptions: {
1168
+ maxErrorCorrections: 3 // Number of retry attempts
1169
+ },
1170
+ analyzeResultsOptions: {
1171
+ enableCodeInterpreter: true // Enable code execution for result analysis
1172
+ }
1173
+ }
1174
+ );
1175
+
1176
+ // AI-powered API call
1177
+ const apiResult = await squid.ai().executeAiApiCall(
1178
+ 'stripe-api',
1179
+ 'Create a $50 charge for customer cus_123',
1180
+ ['create-charge'], // allowed endpoints
1181
+ true // provide explanation
1182
+ );
1183
+ ```
1184
+
1185
+ ### Storage
1186
+
1187
+ ```typescript
1188
+ const storage = squid.storage(); // built_in_storage
1189
+ const s3 = squid.storage('aws-s3-integration');
1190
+
1191
+ // Upload file
1192
+ await storage.uploadFile('/documents', file, 3600); // 1 hour expiration
1193
+
1194
+ // Get download URL
1195
+ const url = await storage.getDownloadUrl('/documents/report.pdf', 3600);
1196
+ console.log(url.url);
1197
+
1198
+ // Get file metadata
1199
+ const metadata = await storage.getFileMetadata('/documents/report.pdf');
1200
+ console.log(metadata.filename, metadata.size, metadata.lastModified);
1201
+
1202
+ // List directory
1203
+ const contents = await storage.listDirectoryContents('/documents');
1204
+ console.log(contents.files, contents.directories);
1205
+
1206
+ // Delete files
1207
+ await storage.deleteFile('/documents/report.pdf');
1208
+ await storage.deleteFiles(['/docs/file1.pdf', '/docs/file2.pdf']);
1209
+ ```
1210
+
1211
+ ### Queues
1212
+
1213
+ ```typescript
1214
+ const queue = squid.queue<Message>('notifications');
1215
+ const kafkaQueue = squid.queue<Event>('events', 'kafka-integration');
1216
+
1217
+ // Produce messages
1218
+ await queue.produce([
1219
+ { type: 'email', to: 'user@example.com' },
1220
+ { type: 'sms', to: '+1234567890' }
1221
+ ]);
1222
+
1223
+ // Consume messages (observable)
1224
+ const subscription = queue.consume<Message>().subscribe(message => {
1225
+ console.log('Received:', message);
1226
+ });
1227
+
1228
+ // Unsubscribe
1229
+ subscription.unsubscribe();
1230
+ ```
1231
+
1232
+ ### Distributed Locks
1233
+
1234
+ ```typescript
1235
+ // Acquire and release manually
1236
+ const lock = await squid.acquireLock('payment-processing');
1237
+ try {
1238
+ await processPayment();
1239
+ } finally {
1240
+ lock.release(); // Note: release() is synchronous
1241
+ }
1242
+
1243
+ // Check if released
1244
+ console.log(lock.isReleased()); // true after release
1245
+
1246
+ // Observe release (RxJS Observable)
1247
+ const sub = lock.observeRelease().subscribe(() => {
1248
+ console.log('Lock was released');
1249
+ });
1250
+
1251
+ // Automatic release with withLock (recommended)
1252
+ const result = await squid.withLock('payment-processing', async (lock) => {
1253
+ // Critical section - only one client can execute at a time
1254
+ await processPayment();
1255
+ return 'success';
1256
+ });
1257
+
1258
+ // Lock is automatically released even if callback throws
1259
+ ```
1260
+
1261
+ **Important notes:**
1262
+ - Lock is automatically released if WebSocket connection is lost
1263
+ - If lock is already held by another client, `acquireLock()` will reject
1264
+ - Use `@secureDistributedLock(mutexName?)` decorator to secure lock access
1265
+ - Without API key, you must define security rules for locks
1266
+
1267
+ ### API Integration
1268
+
1269
+ ```typescript
1270
+ const api = squid.api();
1271
+
1272
+ // HTTP methods
1273
+ const customer = await api.get<Customer>('stripe-api', 'get-customer', {
1274
+ pathParams: { customerId: 'cus_123' },
1275
+ queryParams: { expand: 'subscriptions' }
1276
+ });
1277
+
1278
+ const charge = await api.post<Charge, ChargeRequest>(
1279
+ 'stripe-api',
1280
+ 'create-charge',
1281
+ { amount: 1000, currency: 'usd' },
1282
+ { headers: { 'Idempotency-Key': 'unique' } }
1283
+ );
1284
+
1285
+ // Other methods: put, patch, delete
1286
+ await api.put(integrationId, endpointId, body, options);
1287
+ await api.patch(integrationId, endpointId, body, options);
1288
+ await api.delete(integrationId, endpointId, body, options);
1289
+ ```
1290
+
1291
+ ### Web
1292
+
1293
+ ```typescript
1294
+ const web = squid.web();
1295
+
1296
+ // AI-powered web search
1297
+ const results = await web.aiSearch('latest AI developments');
1298
+ console.log(results.markdownContent);
1299
+ console.log(results.citations);
1300
+
1301
+ // Get URL content (as markdown)
1302
+ const content = await web.getUrlContent('https://example.com/article');
1303
+
1304
+ // URL shortening
1305
+ const shortUrl = await web.createShortUrl('https://very-long-url.com', 86400);
1306
+ console.log(shortUrl.url, shortUrl.id);
1307
+
1308
+ const shortUrls = await web.createShortUrls(['url1', 'url2'], 86400);
1309
+
1310
+ await web.deleteShortUrl(shortUrlId);
1311
+ await web.deleteShortUrls([id1, id2]);
1312
+ ```
1313
+
1314
+ ### Schedulers
1315
+
1316
+ ```typescript
1317
+ const schedulers = squid.schedulers;
1318
+
1319
+ // List all
1320
+ const all = await schedulers.list();
1321
+
1322
+ // Enable/disable
1323
+ await schedulers.enable('daily-cleanup');
1324
+ await schedulers.enable(['scheduler-1', 'scheduler-2']);
1325
+
1326
+ await schedulers.disable('hourly-sync');
1327
+ await schedulers.disable(['scheduler-1', 'scheduler-2']);
1328
+ ```
1329
+
1330
+ ### Observability & Metrics
1331
+
1332
+ ```typescript
1333
+ const obs = squid.observability;
1334
+
1335
+ // Report metric
1336
+ await obs.reportMetric({
1337
+ name: 'api_request_count',
1338
+ value: 1,
1339
+ tags: { endpoint: '/users', method: 'GET' },
1340
+ timestamp: Date.now()
1341
+ });
1342
+
1343
+ // Query metrics
1344
+ const metrics = await obs.queryMetrics({
1345
+ metricNames: ['api_request_count'],
1346
+ startTime: startTimestamp,
1347
+ endTime: endTimestamp,
1348
+ groupBy: ['endpoint'],
1349
+ filters: { method: 'GET' },
1350
+ aggregation: 'sum'
1351
+ });
1352
+
1353
+ // Flush pending
1354
+ await obs.flush();
1355
+ ```
1356
+
1357
+ ### Jobs
1358
+
1359
+ Jobs allow you to track the status of long-running asynchronous operations. Each job has a unique ID that can be used to query its status or wait for completion.
1360
+
1361
+ **Important:** Job IDs must be globally unique and kept private. Anyone who knows a job ID can query its status or result.
1362
+
1363
+ ```typescript
1364
+ const jobClient = squid.job();
1365
+
1366
+ // Get job status (returns AsyncJob with status: 'pending' | 'completed' | 'failed')
1367
+ const job = await jobClient.getJob<Result>('job-123');
1368
+ if (job) {
1369
+ console.log('Status:', job.status);
1370
+ if (job.status === 'completed') {
1371
+ console.log('Result:', job.result);
1372
+ } else if (job.status === 'failed') {
1373
+ console.log('Error:', job.error);
1374
+ }
1375
+ }
1376
+
1377
+ // Wait for completion (resolves when job finishes or throws if job fails)
1378
+ const result = await jobClient.awaitJob<Result>('job-123');
1379
+ console.log('Job completed with result:', result);
1380
+ ```
1381
+
1382
+ ### Admin - Integrations
1383
+
1384
+ **Important:** Admin methods require initialization with an API key. They cannot be used with user authentication.
1385
+
1386
+ ```typescript
1387
+ // Squid must be initialized with apiKey
1388
+ const squid = new Squid({ appId: 'app-id', region: 'us-east-1.aws', apiKey: 'your-api-key' });
1389
+
1390
+ const integrations = squid.admin().integrations();
1391
+
1392
+ // List all or by type
1393
+ const all = await integrations.list();
1394
+ const databases = await integrations.list('database');
1395
+
1396
+ // Get one
1397
+ const integration = await integrations.get('postgres-db');
1398
+
1399
+ // Create/update
1400
+ await integrations.upsertIntegration({
1401
+ integrationId: 'my-postgres',
1402
+ type: 'postgres',
1403
+ connectionString: 'postgresql://...'
1404
+ });
1405
+
1406
+ // Delete
1407
+ await integrations.delete('old-integration');
1408
+ await integrations.deleteMany(['int-1', 'int-2']);
1409
+ ```
1410
+
1411
+ ### Admin - Secrets
1412
+
1413
+ ```typescript
1414
+ const secrets = squid.admin().secrets();
1415
+
1416
+ // Get secret
1417
+ const value = await secrets.get('STRIPE_KEY');
1418
+
1419
+ // Get all
1420
+ const all = await secrets.getAll();
1421
+
1422
+ // Create/update
1423
+ await secrets.upsert('API_KEY', 'secret-value');
1424
+ await secrets.upsertMany([
1425
+ { key: 'KEY1', value: 'val1' },
1426
+ { key: 'KEY2', value: 'val2' }
1427
+ ]);
1428
+
1429
+ // Delete
1430
+ await secrets.delete('OLD_KEY');
1431
+ await secrets.deleteMany(['KEY1', 'KEY2']);
1432
+
1433
+ // API Keys
1434
+ const apiKeys = secrets.apiKeys;
1435
+ await apiKeys.upsert('my-key');
1436
+ const key = await apiKeys.get('my-key');
1437
+ await apiKeys.delete('my-key');
1438
+ ```
1439
+
1440
+ ## Backend SDK
1441
+
1442
+ ### SquidService Base Class
1443
+
1444
+ All backend services extend `SquidService`:
1445
+
1446
+ ```typescript
1447
+ import { SquidService } from '@squidcloud/backend';
1448
+
1449
+ export class MyService extends SquidService {
1450
+ // Decorated methods
1451
+ }
1452
+ ```
1453
+
1454
+ **Important:** In backend code running on Squid's infrastructure, the Squid client is **already initialized and available** via `this.squid`. You don't need to manually create a new instance or provide configuration parameters - simply use the pre-configured client.
1455
+
1456
+ **Available properties:**
1457
+
1458
+ ```typescript
1459
+ this.region // 'local' during dev, region in production
1460
+ this.backendBaseUrl
1461
+ this.secrets // From Squid Console
1462
+ this.apiKeys
1463
+ this.context // RunContext (appId, clientId, sourceIp, headers, openApiContext)
1464
+ this.squid // Pre-initialized Squid client instance (ready to use)
1465
+ this.assetsDirectory // Path to public/ folder
1466
+ ```
1467
+
1468
+ **Auth methods:**
1469
+
1470
+ ```typescript
1471
+ // Get user authentication (JWT token)
1472
+ this.getUserAuth() // AuthWithBearer | undefined
1473
+ // Returns: { type: 'Bearer', userId: string, expiration: number, attributes: Record<string, any>, jwt?: string }
1474
+
1475
+ // Get API key authentication
1476
+ this.getApiKeyAuth() // AuthWithApiKey | undefined
1477
+ // Returns: { type: 'ApiKey', apiKey: string }
1478
+
1479
+ this.isAuthenticated() // boolean - true if user token OR API key
1480
+ this.assertIsAuthenticated() // throws if not authenticated
1481
+ this.assertApiKeyCall() // throws if not API key auth
1482
+ ```
1483
+
1484
+ **Helper methods:**
1485
+
1486
+ ```typescript
1487
+ // Create webhook response (for @webhook decorated functions)
1488
+ this.createWebhookResponse(body?, statusCode?, headers?)
1489
+ // Throws webhook response immediately (interrupts execution)
1490
+ this.throwWebhookResponse({ body?, statusCode?, headers? })
1491
+
1492
+ // Create OpenAPI response (for OpenAPI/tsoa decorated functions)
1493
+ this.createOpenApiResponse(body?, statusCode?, headers?)
1494
+ // Throws OpenAPI response immediately (interrupts execution)
1495
+ this.throwOpenApiResponse({ body?, statusCode?, headers? })
1496
+
1497
+ // Convert browser File to SquidFile for OpenAPI file returns
1498
+ await this.convertToSquidFile(file: File): Promise<SquidFile>
1499
+
1500
+ // Publish AI status update to specific client (used in @aiFunction)
1501
+ await this.publishAiStatusUpdate(update: AiStatusMessage, clientId: ClientId)
1502
+ ```
1503
+
1504
+ ### Backend Decorators
1505
+
1506
+ This section contains backend decorators that don't have corresponding client methods (triggers, schedulers, security rules, etc.).
1507
+
1508
+ #### @trigger(options)
1509
+ Responds to database collection changes.
1510
+
1511
+ ```typescript
1512
+ @trigger({ id: 'user-created', collection: 'users', mutationTypes: ['insert'] })
1513
+ async onUserCreated(request: TriggerRequest<User>): Promise<void> {
1514
+ console.log(request.mutationType); // 'insert' | 'update' | 'delete'
1515
+ console.log(request.docBefore);
1516
+ console.log(request.docAfter);
1517
+ }
1518
+ ```
1519
+
1520
+ #### @scheduler(options)
1521
+ Schedules periodic execution using cron expressions.
1522
+
1523
+ ```typescript
1524
+ import { CronExpression } from '@squidcloud/backend';
1525
+
1526
+ // Using cron string directly
1527
+ @scheduler({ id: 'daily-cleanup', cron: '0 0 * * *' }) // Daily at midnight UTC
1528
+ async cleanup(): Promise<void> {
1529
+ console.log('Running cleanup');
1530
+ }
1531
+
1532
+ // Using predefined CronExpression enum
1533
+ @scheduler({ id: 'hourly-sync', cron: CronExpression.EVERY_HOUR })
1534
+ async hourlySync(): Promise<void> {
1535
+ console.log('Running hourly sync');
1536
+ }
1537
+
1538
+ @scheduler({ id: 'weekday-morning', cron: CronExpression.MONDAY_TO_FRIDAY_AT_9AM })
1539
+ async weekdayTask(): Promise<void> {
1540
+ console.log('Running weekday morning task');
1541
+ }
1542
+
1543
+ @scheduler({ id: 'frequent', cron: CronExpression.EVERY_5_MINUTES, exclusive: false })
1544
+ async frequentTask(): Promise<void> {
1545
+ // exclusive: false allows concurrent runs
1546
+ }
1547
+ ```
1548
+
1549
+ **Common CronExpression values:**
1550
+ - `EVERY_SECOND`, `EVERY_5_SECONDS`, `EVERY_10_SECONDS`, `EVERY_30_SECONDS`
1551
+ - `EVERY_MINUTE`, `EVERY_5_MINUTES`, `EVERY_10_MINUTES`, `EVERY_30_MINUTES`
1552
+ - `EVERY_HOUR`, `EVERY_2_HOURS`, `EVERY_3_HOURS`, etc.
1553
+ - `EVERY_DAY_AT_MIDNIGHT`, `EVERY_DAY_AT_NOON`, `EVERY_DAY_AT_1AM`, etc.
1554
+ - `EVERY_WEEKDAY`, `EVERY_WEEKEND`, `EVERY_WEEK`
1555
+ - `MONDAY_TO_FRIDAY_AT_9AM`, `MONDAY_TO_FRIDAY_AT_5PM`, etc.
1556
+ - `EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT`, `EVERY_QUARTER`, `EVERY_YEAR`
1557
+
1558
+ #### @secureTopic(topicName, type, integrationId?)
1559
+ Secures queue topics.
1560
+
1561
+ ```typescript
1562
+ @secureTopic('notifications', 'read')
1563
+ allowRead(): boolean {
1564
+ return this.isAuthenticated();
1565
+ }
1566
+
1567
+ @secureTopic('notifications', 'write')
1568
+ allowWrite(context: TopicWriteContext<any>): boolean {
1569
+ return context.messages.length <= 100;
1570
+ }
1571
+ ```
1572
+
1573
+ Types: `'read'`, `'write'`, `'all'`
1574
+
1575
+ #### @secureStorage(type, integrationId?)
1576
+ Secures storage operations.
1577
+
1578
+ ```typescript
1579
+ @secureStorage('write')
1580
+ allowWrite(context: StorageContext): boolean {
1581
+ return !context.pathsInBucket.some(p => p.startsWith('/admin'));
1582
+ }
1583
+ ```
1584
+
1585
+ Types: `'read'`, `'write'`, `'update'`, `'insert'`, `'delete'`, `'all'`
1586
+
1587
+ #### @secureApi(integrationId, endpointId?)
1588
+ Secures API integrations.
1589
+
1590
+ ```typescript
1591
+ @secureApi('stripe-api', 'create-charge')
1592
+ allowCharge(context: ApiCallContext): boolean {
1593
+ const amount = (context.body as any)?.amount;
1594
+ return amount < 10000;
1595
+ }
1596
+
1597
+ @secureApi('external-api') // Secures entire API
1598
+ allowApi(): boolean {
1599
+ return this.isAuthenticated();
1600
+ }
1601
+ ```
1602
+
1603
+ #### @secureNativeQuery(integrationId)
1604
+ Secures native database queries.
1605
+
1606
+ ```typescript
1607
+ @secureNativeQuery('postgres-db')
1608
+ allowQuery(context: NativeQueryContext): boolean {
1609
+ if (context.type === 'relational') {
1610
+ return !context.query.toUpperCase().includes('DROP');
1611
+ }
1612
+ return true;
1613
+ }
1614
+ ```
1615
+
1616
+ #### @secureAiQuery(integrationId?)
1617
+ Secures AI query execution.
1618
+
1619
+ ```typescript
1620
+ @secureAiQuery()
1621
+ allowAiQuery(context: AiQueryContext): boolean {
1622
+ return this.isAuthenticated() && context.prompt.length < 1000;
1623
+ }
1624
+ ```
1625
+
1626
+ #### @secureAiAgent(agentId?)
1627
+ Secures AI agent access.
1628
+
1629
+ ```typescript
1630
+ @secureAiAgent('customer-bot')
1631
+ allowAgent(context: SecureAiAgentContext): boolean {
1632
+ return !context.prompt?.includes('admin');
1633
+ }
1634
+
1635
+ @secureAiAgent() // All agents
1636
+ allowAllAgents(): boolean {
1637
+ return this.isAuthenticated();
1638
+ }
1639
+ ```
1640
+
1641
+ #### @secureDistributedLock(mutexName?)
1642
+ Secures distributed lock access.
1643
+
1644
+ ```typescript
1645
+ // Secure specific mutex
1646
+ @secureDistributedLock('payment-processing')
1647
+ allowPaymentLock(context: DistributedLockContext): boolean {
1648
+ // context.mutex contains the mutex name
1649
+ return this.isAuthenticated() && context.mutex === 'payment-processing';
1650
+ }
1651
+
1652
+ // Secure all mutexes
1653
+ @secureDistributedLock()
1654
+ allowAllLocks(context: DistributedLockContext): boolean {
1655
+ return this.isAuthenticated();
1656
+ }
1657
+ ```
1658
+
1659
+ **Important:** Without API key, you must define `@secureDistributedLock` decorators to allow lock acquisition.
1660
+
1661
+ #### @aiFunction(options)
1662
+ Exposes function to AI agents. Agents automatically call these functions based on user prompts.
1663
+
1664
+ ```typescript
1665
+ // Simple form: description and params array
1666
+ @aiFunction('Returns the names of the pirates crew on a given ship name', [
1667
+ { name: 'shipName', type: 'string', required: true, description: 'The name of the ship' }
1668
+ ])
1669
+ async getShipCrew({ shipName }: { shipName: string }): Promise<string[]> {
1670
+ const crew = await getCrew(shipName);
1671
+ return crew.map(m => m.name);
1672
+ }
1673
+
1674
+ // Options object form with more control
1675
+ @aiFunction({
1676
+ id: 'custom-function-id', // Optional custom ID
1677
+ description: 'Get user profile',
1678
+ params: [
1679
+ { name: 'userId', type: 'string', required: true, description: 'User ID' }
1680
+ ],
1681
+ attributes: {
1682
+ integrationType: ['salesforce'] // Only available when specific integrations connected
1683
+ }
1684
+ })
1685
+ async getUserProfile(
1686
+ params: { userId: string },
1687
+ context: AiFunctionCallContext
1688
+ ): Promise<User> {
1689
+ console.log('Agent ID:', context.agentId);
1690
+ console.log('Integration ID:', context.integrationId);
1691
+ return { userId: params.userId, name: 'John' };
1692
+ }
1693
+
1694
+ // Parameter types: 'string' | 'number' | 'boolean' | 'date' | 'files'
1695
+ @aiFunction('Books a hotel room', [
1696
+ { name: 'hotelName', type: 'string', required: true, description: 'Name of hotel' },
1697
+ { name: 'checkInDate', type: 'date', required: true, description: 'Check-in date' },
1698
+ { name: 'numberOfGuests', type: 'number', required: true, description: 'Number of guests' },
1699
+ { name: 'breakfast', type: 'boolean', required: false, description: 'Include breakfast' },
1700
+ { name: 'roomType', type: 'string', required: true, description: 'Type of room',
1701
+ enum: ['single', 'double', 'suite'] }
1702
+ ])
1703
+ async bookHotel(args: BookingArgs): Promise<string> {
1704
+ return `Booked ${args.roomType} room at ${args.hotelName}`;
1705
+ }
1706
+
1707
+ // Using predefined parameters (hidden from AI)
1708
+ // Call from client with: agent.ask(prompt, {
1709
+ // functions: ['sendEmail'],
1710
+ // predefinedParams: { sendEmail: { apiKey: 'secret' } }
1711
+ // })
1712
+ @aiFunction('Sends an email', [
1713
+ { name: 'recipient', type: 'string', required: true, description: 'Email recipient' },
1714
+ { name: 'subject', type: 'string', required: true, description: 'Email subject' },
1715
+ { name: 'body', type: 'string', required: true, description: 'Email body' }
1716
+ ])
1717
+ async sendEmail(args: { recipient: string; subject: string; body: string; apiKey?: string }): Promise<string> {
1718
+ await emailService.send(args.apiKey, args.recipient, args.subject, args.body);
1719
+ return 'Email sent successfully';
1720
+ }
1721
+ ```
1722
+
1723
+ **Using functions from client:**
1724
+ ```typescript
1725
+ const response = await agent.ask('What is the crew of the Black Pearl?', {
1726
+ functions: ['getShipCrew', 'getShipDetails'] // Function names or custom IDs
1727
+ });
1728
+ ```
1729
+
1730
+ #### @limits(options)
1731
+ Rate/quota limiting.
1732
+
1733
+ ```typescript
1734
+ // Simple rate limit (5 QPS globally)
1735
+ @limits({ rateLimit: 5 })
1736
+ async limited(): Promise<void> {}
1737
+
1738
+ // Quota (100 calls/month per user)
1739
+ @limits({ quotaLimit: { value: 100, scope: 'user', renewPeriod: 'monthly' } })
1740
+ async quotaLimited(): Promise<void> {}
1741
+
1742
+ // Multiple limits
1743
+ @limits({
1744
+ rateLimit: [
1745
+ { value: 100, scope: 'global' },
1746
+ { value: 10, scope: 'user' }
1747
+ ],
1748
+ quotaLimit: [
1749
+ { value: 10000, scope: 'global', renewPeriod: 'monthly' },
1750
+ { value: 500, scope: 'user', renewPeriod: 'weekly' }
1751
+ ]
1752
+ })
1753
+ async multiLimited(): Promise<void> {}
1754
+ ```
1755
+
1756
+ Scopes: `'global'`, `'user'`, `'ip'`
1757
+ Periods: `'hourly'`, `'daily'`, `'weekly'`, `'monthly'`, `'quarterly'`, `'annually'`
1758
+
1759
+ ## Common Patterns
1760
+
1761
+ ### Authentication
1762
+
1763
+ Client:
1764
+ ```typescript
1765
+ const squid = new Squid({
1766
+ appId: 'YOUR_APP_ID',
1767
+ region: 'us-east-1.aws',
1768
+ authProvider: {
1769
+ integrationId: 'auth0',
1770
+ getToken: async () => await auth0.getAccessTokenSilently()
1771
+ }
1772
+ });
1773
+ ```
1774
+
1775
+ Backend:
1776
+
1777
+ ```typescript
1778
+ @executable()
1779
+ async getUserData(): Promise<UserData> {
1780
+ this.assertIsAuthenticated();
1781
+ const userId = this.getUserAuth()?.userId;
1782
+ if (!userId) throw new Error('User not authenticated');
1783
+ return await fetchUserData(userId);
1784
+ }
1785
+
1786
+ @secureCollection('users', 'read')
1787
+ allowRead(context: QueryContext): boolean {
1788
+ const userId = this.getUserAuth()?.userId;
1789
+ if (!userId) return false;
1790
+ return context.isSubqueryOf('id', '==', userId);
1791
+ }
1792
+ ```
1793
+
1794
+ ### File Upload
1795
+
1796
+ Client:
1797
+ ```typescript
1798
+ const file: File = ...; // from input
1799
+ await squid.storage().uploadFile('/uploads', file);
1800
+ // OR
1801
+ await squid.executeFunction('processFile', file);
1802
+ ```
1803
+
1804
+ Backend:
1805
+ ```typescript
1806
+ @executable()
1807
+ async processFile(file: SquidFile): Promise<Result> {
1808
+ console.log(file.originalName, file.size, file.mimetype);
1809
+ const content = new TextDecoder().decode(file.data);
1810
+ return { processed: true };
1811
+ }
1812
+ ```
1813
+
1814
+ ### Using Squid Client in Backend
1815
+
1816
+ ```typescript
1817
+ export class MyService extends SquidService {
1818
+ @executable()
1819
+ async aggregateStats(userId: string): Promise<Stats> {
1820
+ const orders = await this.squid.collection('orders')
1821
+ .query()
1822
+ .eq('userId', userId)
1823
+ .snapshot();
1824
+
1825
+ return { totalOrders: orders.data.length };
1826
+ }
1827
+ }
1828
+ ```
1829
+
1830
+ ## Best Practices
1831
+
1832
+ 1. **Always secure collections/topics/storage** - Default deny
1833
+ 2. **Validate input in executables** - Check auth and params
1834
+ 3. **Use transactions for multi-doc updates** - Atomic operations
1835
+ 4. **Limit query results** - Default 1000, max 20000
1836
+ 5. **Use snapshots for one-time data** - Use subscriptions for realtime
1837
+ 6. **Batch operations when possible** - insertMany, deleteMany
1838
+ 7. **Use memoryOptions for AI conversations** - Not deprecated chatId
1839
+ 8. **Test in dev before prod deployment** - `squid deploy --environmentId dev`
1840
+
1841
+ ## Documentation
1842
+
1843
+ - Docs: https://docs.getsquid.ai/
1844
+ - Console: https://console.getsquid.ai/
1845
+ - Samples: https://github.com/squid-cloud-samples
1846
+
1847
+ ---
1848
+
1849
+ **This skill is verified against the actual Squid source code and contains only accurate, tested APIs.**