|
1 | | -The @convex_rules.md file doesn't contain examples or use cases for the client-side sdks. |
| 1 | +# SvelteKit and Convex Integration Pattern |
| 2 | + |
| 3 | +This document outlines the specific architectural pattern used in this project to integrate the SvelteKit frontend with the Convex backend. The pattern ensures that data access is secure and efficient. |
2 | 4 |
|
3 | 5 | In this project we use Svelte and the convex-svelte package together. |
4 | 6 |
|
5 | | -Here's the documentation/github repo for this package: https://github.com/get-convex/convex-svelte |
| 7 | +Here's the documentation/github repo for this package: https://github.com/get-convex/convex-svelte |
| 8 | + |
| 9 | +## Overview |
| 10 | + |
| 11 | +The core of the pattern is a **client-driven authorization model**. The SvelteKit client is responsible for obtaining the current user's identity (whether that be through a +page.server.ts/+layout.server.ts loader or a front-end auth client) and passing the `user_id` to Convex queries and mutations. The Convex backend then uses this `user_id` to both filter data and, crucially, to authorize access to specific documents. |
| 12 | + |
| 13 | +This creates a clear separation of concerns: |
| 14 | +- **SvelteKit (Client):** Manages UI, user authentication state, and calls Convex functions with the necessary user context. |
| 15 | +- **Convex (Backend):** Defines data schema, business logic, and enforces strict data access rules based on the user identity provided by the client. |
| 16 | + |
| 17 | +## Key Components of the Pattern |
| 18 | + |
| 19 | +### 1. Client-Side User Identity |
| 20 | + |
| 21 | +- The user's ID is obtained on the client through PageData or a client-auth library (in the examples below, it happens to be Clerk). |
| 22 | +- This `user_id` is then passed as a prop to any components that need to interact with user-specific data. |
| 23 | + |
| 24 | +*Example (`TaskCard.svelte`):* |
| 25 | +```svelte |
| 26 | +<script lang="ts"> |
| 27 | + import { useClerkContext } from 'svelte-clerk/client'; |
| 28 | + // ... |
| 29 | + const ctx = useClerkContext(); |
| 30 | + const user_id = $derived(ctx.auth.userId as string); |
| 31 | +</script> |
| 32 | +
|
| 33 | +<!-- user_id is passed down to child components --> |
| 34 | +<TaskInput {user_id} /> |
| 35 | +<TaskList {user_id} /> |
| 36 | +``` |
| 37 | + |
| 38 | +### 2. Fetching Data (Queries) |
| 39 | + |
| 40 | +- The `convex-svelte` package's `useQuery` hook is used to subscribe to Convex queries. |
| 41 | +- The `user_id` is passed as an argument to the query, which the backend uses to fetch only the relevant data. |
| 42 | + |
| 43 | +*Example (`TaskList.svelte`):* |
| 44 | +```svelte |
| 45 | +<script lang="ts"> |
| 46 | + import { useQuery } from 'convex-svelte'; |
| 47 | + import { api } from '@/convex/_generated/api'; |
| 48 | + // ... |
| 49 | + const { user_id } = $props<{ user_id: string }>(); |
| 50 | + const query = $derived(useQuery(api.tasks.get, { user_id })); |
| 51 | +</script> |
| 52 | +
|
| 53 | +<!-- UI reacts to query state (isLoading, error, data) --> |
| 54 | +``` |
| 55 | + |
| 56 | +### 3. Modifying Data (Mutations) |
| 57 | + |
| 58 | +- The `useConvexClient` hook provides a raw client instance. |
| 59 | +- Mutations are called using `client.mutation()`, passing the `user_id` along with other arguments. |
| 60 | + |
| 61 | +*Example (`TaskInput.svelte`):* |
| 62 | +```svelte |
| 63 | +<script lang="ts"> |
| 64 | + import { useConvexClient } from 'convex-svelte'; |
| 65 | + import { api } from '@/convex/_generated/api'; |
| 66 | + // ... |
| 67 | + const client = useConvexClient(); |
| 68 | + const { user_id } = $props<{ user_id: string }>(); |
| 69 | +
|
| 70 | + const createTask = async () => { |
| 71 | + // ... |
| 72 | + await client.mutation(api.tasks.create, { body: taskBody, user_id }); |
| 73 | + // ... |
| 74 | + }; |
| 75 | +</script> |
| 76 | +``` |
| 77 | + |
| 78 | +### 4. Backend Authorization |
| 79 | + |
| 80 | +This is the most critical piece of the pattern for ensuring security. |
| 81 | + |
| 82 | +- **For Queries:** The backend uses the `user_id` to filter results using a database index. This is efficient and ensures users only see their own data. |
| 83 | + |
| 84 | +*Example (`tasks.ts` - `get` query):* |
| 85 | +```typescript |
| 86 | +export const get = query({ |
| 87 | + args: { user_id: v.string() }, |
| 88 | + handler: async (ctx, args) => { |
| 89 | + return await ctx.db |
| 90 | + .query('tasks') |
| 91 | + .withIndex('by_user_id', (q) => q.eq('user_id', args.user_id)) |
| 92 | + .collect(); |
| 93 | + } |
| 94 | +}); |
| 95 | +``` |
| 96 | + |
| 97 | +- **For Mutations/Actions:** For any operation that modifies or deletes a *specific* document, a dedicated authorization helper (`authorizeTaskAccess`) is used. This function retrieves the document and verifies that the `user_id` from the client matches the `user_id` stored on the document. If they don't match, it throws an error, preventing the operation from proceeding. |
| 98 | + |
| 99 | +*Example (`tasks.ts` - `authorizeTaskAccess` and `update` mutation):* |
| 100 | +```typescript |
| 101 | +async function authorizeTaskAccess( |
| 102 | + ctx: { db: DatabaseReader }, |
| 103 | + taskId: Id<'tasks'>, |
| 104 | + userId: string |
| 105 | +) { |
| 106 | + const task = await ctx.db.get(taskId); |
| 107 | + if (!task) throw new Error('Task not found'); |
| 108 | + if (task.user_id !== userId) throw new Error('Not authorized to access this task'); |
| 109 | + return task; |
| 110 | +} |
| 111 | + |
| 112 | +export const update = mutation({ |
| 113 | + args: { id: v.id('tasks'), isCompleted: v.boolean(), user_id: v.string() }, |
| 114 | + handler: async (ctx, args) => { |
| 115 | + const { id, isCompleted, user_id } = args; |
| 116 | + await authorizeTaskAccess(ctx, id, user_id); // Authorization check |
| 117 | + await ctx.db.patch(id, { isCompleted }); |
| 118 | + } |
| 119 | +}); |
| 120 | +``` |
| 121 | + |
| 122 | +## Summary |
| 123 | + |
| 124 | +This pattern provides a robust and scalable way to build secure applications with SvelteKit and Convex. By passing the user identity from the client and verifying it on the backend for every relevant operation, we ensure that users can only access and modify their own data. |
| 125 | + |
| 126 | +## Alternative Authentication Patterns |
| 127 | + |
| 128 | +While these examples use Clerk for authentication, it's worth noting that Convex provides its own built-in authentication solutions that may be more streamlined for certain use cases. |
| 129 | + |
| 130 | +### Convex Auth (Beta) |
| 131 | + |
| 132 | +Convex offers its own native solution, [Convex Auth](https://docs.convex.dev/auth/convex-auth), which is currently in beta. It simplifies adding authentication and offers a significant advantage over the client-driven pattern described in this document. |
| 133 | + |
| 134 | +- **Primary Benefit:** With Convex Auth, the user's authentication information is automatically available in the backend context (`ctx`) of any query or mutation. This eliminates the need to manually pass the `user_id` from the client to every backend function, leading to cleaner and less error-prone code. |
| 135 | +- **Pro:** Tightly integrated with the Convex environment and simplifies backend authorization logic. |
| 136 | +- **Con:** As it is a managed service (similar to Clerk), you don't fully own your user data. It is also a beta feature and may evolve. |
| 137 | + |
| 138 | +### Database Auth Pattern |
| 139 | + |
| 140 | +Convex also supports a pattern for integrating with external auth providers by storing their user information directly in your Convex database. |
| 141 | + |
| 142 | +- **Documentation:** [https://docs.convex.dev/auth/database-auth](https://docs.convex.dev/auth/database-auth) |
| 143 | +- **Pro:** Gives you more control over user data within your own database schema. |
| 144 | +- **Con:** This approach can require splitting user information between two systems (the auth provider and your Convex database), which can add complexity to data management. |
| 145 | + |
| 146 | +The choice of authentication provider depends on the specific needs of the project, including requirements for data ownership, development speed, and integration complexity. |
0 commit comments