From 5e0bd7827b79c48b9e2243c229c03572c88f1bdb Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 16 Jan 2026 10:47:26 +0530 Subject: [PATCH 1/5] feat: type safety in sdks rfc 21 --- 021-type-safety-in-sdks.md | 192 +++++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 021-type-safety-in-sdks.md diff --git a/021-type-safety-in-sdks.md b/021-type-safety-in-sdks.md new file mode 100644 index 0000000..dcd51ea --- /dev/null +++ b/021-type-safety-in-sdks.md @@ -0,0 +1,192 @@ +# Type Safety in SDKs + +- Implementation Owner: @ChiragAgg5k +- Start Date: 15-01-2026 +- Target Date: 26-01-2026 +- Appwrite Issue: + No specific issue. + +## Summary + +[summary]: #summary + +Appwrite's SDKs are technically typed but not type safe. There are places where Type Safety does not exist completely, or only partially exists. This RFC proposes various changes not just in the SDKs itself, but in the CLI to help generate a workflow which is completely end-to-end type safe. + +We are going to take inspiration from projects like Prisma and Drizzle which are very popular in the community for their type safety and ease of use. + +## Problem Statement (Step 1) + +[problem-statement]: #problem-statement + +**What problem are you trying to solve?** + +Appwrite's SDKs are only partially typed. For eg. `databaseId` and `tableId` cannot be typed and need to made sure that correct values are passed by the developer. + +## Design proposal (Step 2) + +[design-proposal]: #design-proposal + +### Workflow decisions + +1. **Appwrite init**: A complete workflow to initially setup an appwrite project using the CLI. Not just the project (which is what `appwrite init project` does). +2. **Typed Appwrite config**: Currently appwrite config is plain json, we can make it fully typed with zod. +2. **Push/Pull Sync**: Push and Pull commands will sync your entire configuration (currently it does this in a semi-automated manner). +3. **Generate**: `appwrite generate` will create a `generated` folder (can be a customized by param) that exposes a fully typed SDK for the project. + +### API Endpoints + +No new API endpoints are required. + +### Data Structure + +We will follow the exisiting appwrite config's data structure: + +```json +{ + "projectId": "6839a26e003262977966", + "projectName": "Testing Project", + "endpoint": "https://fra.cloud.appwrite.io/v1", + "tablesDB": [ + { + "$id": "test-db", + "name": "Testing Database", + "$createdAt": "2026-01-15T05:47:28.583+00:00", + "$updatedAt": "2026-01-15T05:47:28.583+00:00", + "enabled": true, + "type": "tablesdb", + "policies": [], + "archives": [] + } + ], + "tables": [ + { + "$id": "users", + "$permissions": [], + "databaseId": "test-db", + "name": "users", + "enabled": true, + "rowSecurity": false, + "columns": [ + { + "key": "username", + "type": "string", + "status": "available", + "error": "", + "required": true, + "array": false, + "$createdAt": "2026-01-15T05:49:30.850+00:00", + "$updatedAt": "2026-01-15T05:49:31.196+00:00", + "size": 32, + "default": null, + "encrypt": false + } + ] + } + ] +} +``` + +Structure of the typed sdk will be as follows: + +```text +generated/ +├── appwrite.databases.ts +├── appwrite.auth.ts +├── appwrite.storage.ts +├── appwrite.functions.ts +├── appwrite.buckets.ts +├── appwrite.messaging.ts +├── appwrite.teams.ts +├── appwrite.types.ts +├── appwrite.ts +``` + +Usage - + +```typescript +import { databases } from './generated/appwrite'; + +const db = databases.from('test-db'); // <-- typed out database options the user has + +await db.users.create({ username: 'testuser' }); +await db.users.get('6968e1d100160eb1a115'); +await db.users.update('6968e1d100160eb1a115', { username: 'testuser2' }); +await db.users.delete('6968e1d100160eb1a115'); +await db.users.list({ limit: 10, offset: 0 }); +await db.users.listWithTotal({ limit: 10, offset: 0 }); + + +await db.users.createMany([{ username: 'testuser3' }, { username: 'testuser4' }]); +await db.users.updateMany([{ id: '6968e1d100160eb1a115', username: 'testuser2' }, { id: '6968e1d100160eb1a116', username: 'testuser3' }]); +await db.users.deleteMany(['6968e1d100160eb1a115', '6968e1d100160eb1a116']); + +await db.users.upsert('6968e1d100160eb1a115', { username: 'testuser2' }); +``` + +The command will auto detect if server sdk is being used or not (with ability to manually select either), and generate server side methods too - + +```typescript +await db.create({ id: 'books', name: 'Books' }); +await db.update('books', { name: 'Books Table' }); +await db.delete('books'); +``` + +### Supporting Libraries + +No new libraries required, uses existing: +- appwrite-cli +- typescript sdks + +### Breaking Changes + +No breaking changes. Existing type safety works as it is. Users who want the type-safe version can opt in by using the new command. + +### Reliability (Tests & Benchmarks) + +#### Scaling + +Code generation runs locally via CLI - no server-side scaling concerns. Generation time is negligible (< 1 second) for typical projects. + +#### Benchmarks + +N/A - client-side code generation with no performance-critical paths. + +#### Tests (UI, Unit, E2E) + +- Unit tests for type generation from various `appwrite.json` configurations +- E2E tests verifying generated SDK compiles and works against live Appwrite instance +- Type tests using `tsd` to verify autocomplete and type errors + +### Documentation & Content + +- Add "Type-Safe SDK Generation" page under SDK docs +- Update CLI reference with `appwrite generate` command +- Blog post showcasing developer experience +- TypeScript starter template with type generation pre-configured + +### Prior art + +[prior-art]: #prior-art + +- [Prisma](https://www.prisma.io/) - `prisma generate` creates typed client from schema file +- [Supabase](https://supabase.com/docs/guides/api/rest/generating-types) - `supabase gen types typescript` generates types from database +- [Drizzle](https://orm.drizzle.team/) - TypeScript-native ORM with inferred types + +### Unresolved questions + +[unresolved-questions]: #unresolved-questions + +- Output directory naming: `generated/`, `appwrite-generated/`, or `.appwrite/`? +- Multi-language support priority order (Dart, Python, Swift/Kotlin)? +- How to handle relationship types - nested objects or IDs? +- Handling of table/column names that conflict with TypeScript reserved words? + +### Future possibilities + +[future-possibilities]: #future-possibilities + +- Type-safe query builders with typed filters and field selection +- Type-safe real-time subscriptions +- Generate Zod schemas for runtime validation +- Watch mode for auto-regeneration on config changes +- IDE extensions for auto-generation From 3b766822e7e7c56469b94e8aad30c3e3aae7e3db Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sun, 18 Jan 2026 17:06:14 +0530 Subject: [PATCH 2/5] update implementation --- 021-type-safety-in-sdks.md | 69 ++++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/021-type-safety-in-sdks.md b/021-type-safety-in-sdks.md index dcd51ea..3bb75be 100644 --- a/021-type-safety-in-sdks.md +++ b/021-type-safety-in-sdks.md @@ -90,45 +90,64 @@ Structure of the typed sdk will be as follows: ```text generated/ -├── appwrite.databases.ts -├── appwrite.auth.ts -├── appwrite.storage.ts -├── appwrite.functions.ts -├── appwrite.buckets.ts -├── appwrite.messaging.ts -├── appwrite.teams.ts -├── appwrite.types.ts -├── appwrite.ts +└── appwrite/ + ├── databases.ts + ├── types.ts + └── index.ts ``` +The generator auto-detects which Appwrite SDK is being used by checking `package.json` dependencies: +- `node-appwrite` - Server SDK (supports bulk methods) +- `appwrite` - Client SDK +- `react-native-appwrite` - React Native SDK +- `@appwrite.io/console` - Console SDK (supports bulk methods) +- `npm:node-appwrite` - Deno (supports bulk methods) + Usage - ```typescript -import { databases } from './generated/appwrite'; +import { Client } from 'node-appwrite'; +import { createDatabases } from './generated/appwrite'; + +const client = new Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject('your-project-id'); -const db = databases.from('test-db'); // <-- typed out database options the user has +const databases = createDatabases(client); +const db = databases.from('test-db'); // <-- typed database options +// Basic CRUD operations await db.users.create({ username: 'testuser' }); await db.users.get('6968e1d100160eb1a115'); await db.users.update('6968e1d100160eb1a115', { username: 'testuser2' }); await db.users.delete('6968e1d100160eb1a115'); -await db.users.list({ limit: 10, offset: 0 }); -await db.users.listWithTotal({ limit: 10, offset: 0 }); - -await db.users.createMany([{ username: 'testuser3' }, { username: 'testuser4' }]); -await db.users.updateMany([{ id: '6968e1d100160eb1a115', username: 'testuser2' }, { id: '6968e1d100160eb1a116', username: 'testuser3' }]); +// Type-safe queries with QueryBuilder +await db.users.list({ + queries: (q) => [ + q.equal('username', 'testuser'), + q.greaterThan('createdAt', '2026-01-01'), + q.limit(10), + q.offset(0), + ] +}); + +// Bulk operations (available with node-appwrite, npm:node-appwrite, @appwrite.io/console) +await db.users.createMany([ + { data: { username: 'testuser3' } }, + { data: { username: 'testuser4' } } +]); +await db.users.updateMany([ + { rowId: '6968e1d100160eb1a115', data: { username: 'testuser2' } }, + { rowId: '6968e1d100160eb1a116', data: { username: 'testuser3' } } +]); await db.users.deleteMany(['6968e1d100160eb1a115', '6968e1d100160eb1a116']); -await db.users.upsert('6968e1d100160eb1a115', { username: 'testuser2' }); -``` - -The command will auto detect if server sdk is being used or not (with ability to manually select either), and generate server side methods too - - -```typescript -await db.create({ id: 'books', name: 'Books' }); -await db.update('books', { name: 'Books Table' }); -await db.delete('books'); +// Optional parameters +await db.users.create( + { username: 'testuser' }, + { rowId: 'custom-id', permissions: [Permission.read(Role.any())], transactionId: 'tx-123' } +); ``` ### Supporting Libraries From 4b2c90d7ff0313b3dafb5feedc7b2b3be08a8e1d Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sun, 18 Jan 2026 18:00:20 +0530 Subject: [PATCH 3/5] update usage --- 021-type-safety-in-sdks.md | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/021-type-safety-in-sdks.md b/021-type-safety-in-sdks.md index 3bb75be..087f4f0 100644 --- a/021-type-safety-in-sdks.md +++ b/021-type-safety-in-sdks.md @@ -133,17 +133,30 @@ await db.users.list({ }); // Bulk operations (available with node-appwrite, npm:node-appwrite, @appwrite.io/console) +// Note: Bulk operations are NOT available for tables with relationship columns + +// createMany - create multiple rows at once await db.users.createMany([ - { data: { username: 'testuser3' } }, - { data: { username: 'testuser4' } } -]); -await db.users.updateMany([ - { rowId: '6968e1d100160eb1a115', data: { username: 'testuser2' } }, - { rowId: '6968e1d100160eb1a116', data: { username: 'testuser3' } } -]); -await db.users.deleteMany(['6968e1d100160eb1a115', '6968e1d100160eb1a116']); - -// Optional parameters + { username: 'testuser3', $id: 'custom-id-1', $permissions: ['read("any")'] }, + { username: 'testuser4' }, // $id auto-generated by server +], { transactionId: 'tx-123' }); + +// updateMany - update all rows matching query with same data +await db.users.updateMany( + { status: 'active' }, + { + queries: (q) => [q.equal('status', 'pending')], + transactionId: 'tx-123' + } +); + +// deleteMany - delete all rows matching query +await db.users.deleteMany({ + queries: (q) => [q.equal('status', 'archived')], + transactionId: 'tx-123' +}); + +// Optional parameters for single operations await db.users.create( { username: 'testuser' }, { rowId: 'custom-id', permissions: [Permission.read(Role.any())], transactionId: 'tx-123' } From b70faaebbea299099d103e1148e1bc8a9b5c3c2b Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 19 Jan 2026 14:42:18 +0530 Subject: [PATCH 4/5] update rfc --- 021-type-safety-in-sdks.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/021-type-safety-in-sdks.md b/021-type-safety-in-sdks.md index 087f4f0..fc65fa9 100644 --- a/021-type-safety-in-sdks.md +++ b/021-type-safety-in-sdks.md @@ -103,6 +103,29 @@ The generator auto-detects which Appwrite SDK is being used by checking `package - `@appwrite.io/console` - Console SDK (supports bulk methods) - `npm:node-appwrite` - Deno (supports bulk methods) +### Multi-Language Support + +The `appwrite generate` command will support multiple languages beyond TypeScript: +- **Phase 1**: TypeScript/JavaScript (Node.js, Browser, React Native, Deno) +- **Phase 2**: Dart/Flutter, Python, Swift, Kotlin + +Language detection via config files (`pubspec.yaml`, `requirements.txt`, etc.), explicit `--language` flag, or project structure analysis. Each language will have idiomatic generator templates. + +### CLI and SDK Bundling + +Exploring bundling strategies to improve DX: +- **Hybrid Approach (Recommended)**: CLI remains standalone; SDK packages include lightweight generator utilities. CLI uses SDK type definitions for validation. +- **Alternatives**: CLI as SDK dependency, standalone with integration hooks, or monorepo workspace support. + +### Enhanced Type Safety + +The generated code will include: +- Type-safe column names in queries (autocomplete for valid column keys) +- Type-safe relationship traversal (typed foreign key lookups) +- Type-safe enum/select column values (literal types for allowed values) +- Type-safe permissions (typed permission strings with validation) +- Type-safe query builder with field-specific filter methods + Usage - ```typescript @@ -173,6 +196,10 @@ No new libraries required, uses existing: No breaking changes. Existing type safety works as it is. Users who want the type-safe version can opt in by using the new command. +### Deprecations + +The `appwrite types` command will be deprecated in favor of `appwrite generate`. The new command provides a more comprehensive solution with full type-safe SDK generation, not just type definitions. Migration path: `appwrite types` → `appwrite generate --types-only` (if needed) or full `appwrite generate`. + ### Reliability (Tests & Benchmarks) #### Scaling From b99162906e3aaca8fe363306471d75e7c89512a5 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 21 Jan 2026 14:58:33 +0530 Subject: [PATCH 5/5] add rc release --- 021-type-safety-in-sdks.md | 123 ++++++++++++++++++++++--------------- 1 file changed, 72 insertions(+), 51 deletions(-) diff --git a/021-type-safety-in-sdks.md b/021-type-safety-in-sdks.md index fc65fa9..3afbe84 100644 --- a/021-type-safety-in-sdks.md +++ b/021-type-safety-in-sdks.md @@ -14,6 +14,14 @@ Appwrite's SDKs are technically typed but not type safe. There are places where We are going to take inspiration from projects like Prisma and Drizzle which are very popular in the community for their type safety and ease of use. +### Implementation Status + +An initial working version is available: + +```bash +npm install -g appwrite-cli@13.1.0 +``` + ## Problem Statement (Step 1) [problem-statement]: #problem-statement @@ -50,12 +58,7 @@ We will follow the exisiting appwrite config's data structure: { "$id": "test-db", "name": "Testing Database", - "$createdAt": "2026-01-15T05:47:28.583+00:00", - "$updatedAt": "2026-01-15T05:47:28.583+00:00", - "enabled": true, - "type": "tablesdb", - "policies": [], - "archives": [] + "enabled": true } ], "tables": [ @@ -71,11 +74,7 @@ We will follow the exisiting appwrite config's data structure: "key": "username", "type": "string", "status": "available", - "error": "", "required": true, - "array": false, - "$createdAt": "2026-01-15T05:49:30.850+00:00", - "$updatedAt": "2026-01-15T05:49:31.196+00:00", "size": 32, "default": null, "encrypt": false @@ -94,6 +93,7 @@ generated/ ├── databases.ts ├── types.ts └── index.ts + └── constants.ts ``` The generator auto-detects which Appwrite SDK is being used by checking `package.json` dependencies: @@ -123,66 +123,87 @@ The generated code will include: - Type-safe column names in queries (autocomplete for valid column keys) - Type-safe relationship traversal (typed foreign key lookups) - Type-safe enum/select column values (literal types for allowed values) -- Type-safe permissions (typed permission strings with validation) +- Type-safe permissions with callback pattern (typed permission and role builders) - Type-safe query builder with field-specific filter methods Usage - ```typescript -import { Client } from 'node-appwrite'; -import { createDatabases } from './generated/appwrite'; - -const client = new Client() - .setEndpoint('https://cloud.appwrite.io/v1') - .setProject('your-project-id'); - -const databases = createDatabases(client); -const db = databases.from('test-db'); // <-- typed database options - -// Basic CRUD operations -await db.users.create({ username: 'testuser' }); -await db.users.get('6968e1d100160eb1a115'); -await db.users.update('6968e1d100160eb1a115', { username: 'testuser2' }); -await db.users.delete('6968e1d100160eb1a115'); - -// Type-safe queries with QueryBuilder -await db.users.list({ - queries: (q) => [ - q.equal('username', 'testuser'), - q.greaterThan('createdAt', '2026-01-01'), - q.limit(10), - q.offset(0), - ] +import { databases } from "./generated/appwrite/index.js"; +import { CategoriesColor } from "./generated/appwrite/types.js"; + +const USER_ID = "685ba24de34c453eb58c"; + +const mydb = databases.use("main-db"); // <-- typed database options +const categoriesTable = mydb.use("categories"); // <-- typed table options + +// Basic CRUD operations with type-safe permissions callback +const category = await categoriesTable.create({ + name: "Category Name", + slug: "category-name-slug", + description: "Category Description", + color: CategoriesColor.F5FF33, // <-- typed enum values + icon: "category-icon", + sortOrder: 1, + isActive: true, + posts: ["c4d70ad8-6f0a-4947-a0de-e034098602a5"] +}, { + permissions: (permission, role) => [ + permission.read(role.user(USER_ID)), + permission.update(role.user(USER_ID)), + permission.delete(role.user(USER_ID)), + ] +}); + +await categoriesTable.get("6968e1d100160eb1a115"); +await categoriesTable.update("6968e1d100160eb1a115", { name: "Updated Name" }); +await categoriesTable.delete("6968e1d100160eb1a115"); + +// Type-safe queries with callback pattern +await categoriesTable.list({ + queries: (query) => [ + query.equal("description", "Category Description"), + query.greaterThan("sortOrder", 0), + query.limit(10), + query.offset(0), + ] }); // Bulk operations (available with node-appwrite, npm:node-appwrite, @appwrite.io/console) // Note: Bulk operations are NOT available for tables with relationship columns // createMany - create multiple rows at once -await db.users.createMany([ - { username: 'testuser3', $id: 'custom-id-1', $permissions: ['read("any")'] }, - { username: 'testuser4' }, // $id auto-generated by server -], { transactionId: 'tx-123' }); +await categoriesTable.createMany([ + { name: "Category 1", slug: "cat-1", color: CategoriesColor.F5FF33 }, + { name: "Category 2", slug: "cat-2", color: CategoriesColor.F5FF33, $id: "custom-id" }, +], { transactionId: "tx-123" }); // updateMany - update all rows matching query with same data -await db.users.updateMany( - { status: 'active' }, - { - queries: (q) => [q.equal('status', 'pending')], - transactionId: 'tx-123' - } +await categoriesTable.updateMany( + { isActive: true }, + { + queries: (query) => [query.equal("isActive", false)], + transactionId: "tx-123" + } ); // deleteMany - delete all rows matching query -await db.users.deleteMany({ - queries: (q) => [q.equal('status', 'archived')], - transactionId: 'tx-123' +await categoriesTable.deleteMany({ + queries: (query) => [query.equal("isActive", false)], + transactionId: "tx-123" }); // Optional parameters for single operations -await db.users.create( - { username: 'testuser' }, - { rowId: 'custom-id', permissions: [Permission.read(Role.any())], transactionId: 'tx-123' } +await categoriesTable.create( + { name: "Test", slug: "test", color: CategoriesColor.F5FF33 }, + { + rowId: "custom-id", + permissions: (permission, role) => [ + permission.read(role.any()), + permission.write(role.users()), + ], + transactionId: "tx-123" + } ); ```