diff --git a/.optimize-cache.json b/.optimize-cache.json index 162c4a82c6..7dd2f4f792 100644 --- a/.optimize-cache.json +++ b/.optimize-cache.json @@ -195,8 +195,10 @@ "images/blog/appwrite-1-8-0-self-hosted-release/cover.png": "c15a9d88ccd16c2dc8333dc74e715e1f4a6c7818d3b4a05f4d68342eacdc0523", "images/blog/appwrite-1-8-1-self-hosted-release/cover.png": "82f0a396c56b6b299b24133079acc6a317c66b2bf02fd91f4862bd3be0f8f373", "images/blog/appwrite-1.5-now-available-on-cloud/cloud15.png": "a1df7388572a9f08d0e315e4b6bc8c9464c1418768e7efbec22758fd728eb970", + "images/blog/appwrite-auth-methods/cover.png": "361513d8b59de8fde7b294dcc6688aada30c46e11933070c529733e486784690", "images/blog/appwrite-backups-and-restores/cover.png": "369b5d91f3dc515e7fb86588f8871aa5ffd788b40023e8373ac694840479c1ab", "images/blog/appwrite-competitor-comparison/cover.png": "e0b98679795c00fd6d2d304b17273eaa6847bb1fc5706efa6cc3f3040ec6b4bb", + "images/blog/appwrite-custom-domains/cover.png": "f1f52235c259731f28241259c66e6e5f3b27940e7e00c9ce67b60e4afb572ec0", "images/blog/appwrite-decoded-bradley/bradley-cover.png": "73577a2104024a2df85be14a397ca81f8f6130ff4206358b14547a9758dbf344", "images/blog/appwrite-decoded-dennis/dennis-career-update.png": "0a99617d99b92c60ef9b3d168dd09dc8e7dae167c6d494d9f2f6a845134e1a67", "images/blog/appwrite-decoded-dennis/dennis-conference.png": "67758d4b193af922d05ee1dd8744a289d2dcdfdf9e2630f19dbf06d62e66fa92", @@ -228,6 +230,7 @@ "images/blog/appwrite-decoded-khushboo/khushboo-with-eldad.png": "b358b6a53d2c5de662b7ddf66aeeb6886478d98c5585a5e2f7068422986cba60", "images/blog/appwrite-decoded/cover-sara.png": "03ef95d81d475dde4caae31c0b442271c8ae904f8655013a2dfe2f8878b97e44", "images/blog/appwrite-for-startups-ship-faster-without-backend-headaches/cover.png": "02e52496f4ed5a1526d16b7fd98e0b3b5ebc2ed8bcdca9cda84fe68609abb5d6", + "images/blog/appwrite-functions-guide/cover.png": "0f5ee2b51cc0082d1049be19ae8ea6e77f6082c6293a7df6406d53966043889c", "images/blog/appwrite-generate/cover.png": "01770d4d6124b317a5103ced1d69b791ef0ce1f1e3a47e4b5072a5fd33c4953d", "images/blog/appwrite-homepage-redesign/cover-image.png": "bc09d91c421f5967c8986eeaae6f7f001380bee686cd3371fd63a8392484647e", "images/blog/appwrite-homepage-redesign/iterations-top-part.png": "713613e719366db8d271ab58815ce5f6db476c0b6a764ce53b4884ddd483f66c", @@ -235,6 +238,11 @@ "images/blog/appwrite-homepage-redesign/old-homepage.png": "78e71a9a71f59c9f872afbfca91eca73dcc932e25cd83ef22505d314d636a26c", "images/blog/appwrite-homepage-redesign/summary-vs-deepdive1.png": "fbfed43d56afacb1eb3bbaffa26c002e4bd27a072d25783ae823de123b7d71f2", "images/blog/appwrite-homepage-redesign/summary-vs-deepdive2.png": "9f18d02a03cfe1f5646e6b585bbeafde584d89cd8a55bef72f1137df73c47b73", + "images/blog/appwrite-indexes/cover.png": "176c9662476a854a6d5aa655b562ec761c8c14d31d6ed7ccbbeca1465301448e", + "images/blog/appwrite-magic-link/cover.png": "d69b6ee225594af2fd84ded6bf43275f8c9105d2610ab851e0cd0b1270989841", + "images/blog/appwrite-messaging-push-email/cover.png": "f2f338968087d91ec10d248d37d41ba36c91dce31eaa9bc6c69822986d82cf05", + "images/blog/appwrite-oauth/cover.png": "43963cbfb499a3db937af52d822662cde32fb2a67bab2f7b24b16f7d24342c90", + "images/blog/appwrite-permissions/cover.png": "c63059c6f879d07312905807371fea29b23459cf757539956de4dc3bdcdc6d89", "images/blog/appwrite-pricing-update/add-ons.png": "7f7cf75b41114b5f14bda99087e6d9a6d3b39667fb4fc4479389ae3b1a666fb7", "images/blog/appwrite-pricing-update/bandwidth-project.png": "a34a4914fc6eff580267159cabc3110869c89c64bfd9a9ecb93fb3a6335060fd", "images/blog/appwrite-pricing-update/combined.png": "e0bdc6025ca29acc57423acfa028a3324a3478937226a5e06cf2ef501a320e4a", @@ -242,16 +250,22 @@ "images/blog/appwrite-pricing-update/large-project.png": "05ef34992cd56650ee121ca00a2ad666b6b65971058c53043afb0ebbe5dccfbd", "images/blog/appwrite-pricing-update/one-subscription.png": "70f5e8590a9fa96afb90ec24a20bd94fed8acecd07c2c342e08e0215a7c15773", "images/blog/appwrite-pricing-update/project.png": "862597aa4be7cb9f4dedebc924432882708ff227ae7c2f73d74c1ef60f60049d", + "images/blog/appwrite-query-api/cover.png": "487c4f1c2d7a4e6c06a66afd5f87e63b5bfaaff34cc091e8ca4d6cc03d0bdbb3", "images/blog/appwrite-realtime-with-flutter/1.png": "15165041f76b8d59f2f4313519a23d9e1a3820d8e1760b6394971babaa8b9709", "images/blog/appwrite-realtime-with-flutter/2.png": "44740ca35567eb456c922c1af4a4a44a7e22ff3cd5c53e38e83e32518326561a", "images/blog/appwrite-realtime-with-flutter/3.png": "c4304f0fa8c92e8a6b473e684139034df94ab2dc7732d1c9dccf9240a712f4f1", "images/blog/appwrite-realtime-with-flutter/4.png": "ea7d6dd933e62fdbd3b1913ce50de91ef3ddc4173915425d5d4db56cb77aaa70", "images/blog/appwrite-realtime-with-flutter/5.png": "49fe7599941b7f5702c310047d96ac6f664b498001cdd66a5ac335be96f580c0", "images/blog/appwrite-realtime-with-flutter/cover.png": "99376d2cf9983874f7e9238dee186f5098c9b7a23d6f8ea3550d518580c8bb6e", + "images/blog/appwrite-realtime/cover.png": "db1c1110935b9ad2a5675e80242914ec38e919004011b0e1060c4d8bc93db5d5", + "images/blog/appwrite-server-sdk-vs-client-sdk/cover.png": "3b674e35218dc6f203824b8ee53eb367e79c33755f64a5456f290768ad5db96f", + "images/blog/appwrite-storage-file-manager/cover.png": "11698e50fb864b4ff99d850290556143b281a988c7fabd23c26b98f653b9b305", + "images/blog/appwrite-teams-roles/cover.png": "34f333eb8ec3ef92514f67d365850a2054e9c9990671f6647ea575533e2954d1", "images/blog/appwrite-vs-auth0-b2c/appwrite-vs-auth0-chart.png": "bba9245370213f15d1d2066260b22a07fccc054b2847596ad66f57bd968e2d63", "images/blog/appwrite-vs-auth0-b2c/cover.png": "97e405da84a457a567b552dea23f10e2e4cc5894e90c36d386efb414623a1d9e", "images/blog/appwrite-vs-cloudinary/cover.png": "ce7d9211396f334c7d165458fe07ffa5fa124dd3e1441e8c993fd60126adb04c", "images/blog/appwrite-vs-vercel-vs-netlify/cover.png": "dbe40ef9cd2308555771129b95a6bfd7c7d6aa31fb88dab3a9071ad8a415284b", + "images/blog/appwrite-webhooks/cover.png": "3dfed85fb2fbe79c894b9f3808ba95a9533d9c3307879582e1e84d842e3d620f", "images/blog/avif-in-storage/cover.png": "23c26ec1a8f23f5bf6c55b19407d0738aa41cdc502dc3eef14a78f430a14447b", "images/blog/avoid-backend-overengineering/cover.png": "c586c235dd6d3f992980748ec7b15cd3411edefe2e71dffc080840540f6d3ba3", "images/blog/baa-explained/cover.png": "a7b144c7549498760cc2bfddda186b8182766ef72e308abc637dc4cbb5a2c853", diff --git a/src/routes/blog/post/appwrite-auth-methods/+page.markdoc b/src/routes/blog/post/appwrite-auth-methods/+page.markdoc new file mode 100644 index 0000000000..c8090fe780 --- /dev/null +++ b/src/routes/blog/post/appwrite-auth-methods/+page.markdoc @@ -0,0 +1,152 @@ +--- +layout: post +title: "Appwrite Auth explained: every auth method, compared" +description: A complete comparison of every Appwrite auth method, from email/password to OAuth2, magic URLs, OTP, and MFA, with guidance on when to use each. +date: 2026-03-23 +cover: /images/blog/appwrite-auth-methods/cover.png +timeToRead: 5 +author: aditya-oberai +category: product, tutorial, security +featured: false +unlisted: true +--- + +Picking the wrong auth method early in a project creates real pain later. You end up bolt-on patching SMS flows onto a system built only for email, or adding OAuth2 to an app that was never designed for external identity providers. Getting the choice right from the start saves you that work. + +[Appwrite](/docs/products/auth) supports ten authentication methods out of the box: email/password, phone SMS, magic URL, email OTP, OAuth2, anonymous sessions, JWT, SSR auth, custom tokens, and MFA. Each one fits a different use case. This post breaks them all down so you can choose with confidence. + +# Email and password + +Email and password is the baseline for most apps. Appwrite stores passwords hashed with Argon2, which is resistant to brute-force attacks and is widely considered the gold standard for password hashing today. + +**When to use it:** Any app where users are comfortable managing their own credentials. It works for most B2C and B2B products, especially when you want full control over the login experience without depending on a third-party identity provider. + +**Trade-offs:** You take on responsibility for the password reset flow, email verification, and account recovery. Users also have to remember yet another password, which drives adoption of weak credentials. Pairing email/password with MFA significantly mitigates this. + +**Security note:** Appwrite handles the Argon2 hashing, but you still need to enforce HTTPS, rate-limit login attempts, and prompt users to verify their email address after registration. + +[Read the Email/password auth docs](/docs/products/auth/email-password). + +# Phone SMS + +Phone authentication sends a one-time code via SMS. The user enters the code to confirm ownership of the phone number and complete sign-in. + +**When to use it:** Apps where phone number is the primary identifier, such as ride-sharing, food delivery, or any product that needs to tie an account to a real-world person. Also useful when users are unlikely to have or remember an email address. + +**Trade-offs:** SMS delivery depends on carrier reliability. Codes can be intercepted via SIM-swapping attacks, which makes SMS OTP less secure than TOTP-based MFA. You also pay per SMS message, so high sign-in frequency adds up. + +**Security note:** SMS auth is stronger than no second factor but weaker than authenticator app-based MFA. For high-security applications, layer it with additional verification. + +[Read the Phone SMS auth docs](/docs/products/auth/phone-sms). + +# Magic URL + +Magic URL sends a one-time link to the user's email address. Clicking the link signs them in without a password. + +**When to use it:** Apps where friction at login directly hurts retention. SaaS tools, newsletters, developer tools, and internal dashboards all benefit from passwordless auth. If your users reliably check email and you do not want them to manage passwords, magic URL is a strong choice. + +**Trade-offs:** Login depends entirely on email access. If a user loses access to their inbox, they lose access to your app. The user also has to switch to their email client to complete login, which adds one extra step compared to entering a password. + +**Security note:** Magic links are single-use and expire. Keep expiry times short (under 15 minutes) to reduce the window for link interception. + +[Read the Magic URL auth docs](/docs/products/auth/magic-url). + +# Email OTP + +Email OTP works like magic URL but sends a numeric code instead of a link. The user enters the code in your app to complete sign-in. + +**When to use it:** Environments where clicking a link is inconvenient, such as mobile apps where switching to email then returning to the app creates friction. A 6-digit code is faster to transcribe than navigating back from a browser. + +**Trade-offs:** Users have to switch to their email app and read the code. This is marginally more friction than magic URL on desktop but often less friction on mobile. Code expiry and single-use constraints are the same as magic URL. + +[Read the Email OTP auth docs](/docs/products/auth/email-otp). + +# OAuth2 + +OAuth2 lets users sign in with an existing account from a third-party provider. Appwrite supports over 30 OAuth2 providers, including Google, GitHub, Apple, Microsoft, and Discord. + +**When to use it:** Any app where reducing sign-up friction matters. OAuth2 removes the need for users to create and remember new credentials. It also gives you a verified email address and, depending on the provider, additional profile data without asking the user to fill out a form. + +**Trade-offs:** You depend on the provider. If Google or GitHub has an outage, users cannot log in. Provider account deletion also means loss of access to your app. Apple Sign-In obfuscates email addresses by default, which complicates email-based communication. + +**Security note:** Validate the OAuth2 state parameter to prevent CSRF. Let Appwrite handle the token exchange; do not expose your OAuth2 client secret in client-side code. + +[Read the OAuth2 auth docs](/docs/products/auth/oauth2). + +# Anonymous sessions + +Anonymous sessions create an account without any credentials. Users can interact with your app before committing to registration. + +**When to use it:** E-commerce carts, games, or any flow where you want users to start before they sign up. Anonymous sessions lower the barrier to entry significantly. + +**Trade-offs:** Anonymous accounts are not recoverable if the session is lost. You need to handle the conversion flow from anonymous to a named account, prompting the user to add credentials at the right moment before they lose data. + +[Read the Anonymous auth docs](/docs/products/auth/anonymous). + +# JWT auth + +JWT auth lets you attach a signed JSON Web Token to requests from your backend. Appwrite validates the token and executes the request in the context of the associated user. + +**When to use it:** Server-rendered apps or custom backends that need to make Appwrite requests on behalf of a specific user. JWTs bridge your existing auth layer with Appwrite when you are not using Appwrite's own session management. + +**Trade-offs:** JWTs carry an expiry risk. A compromised token remains valid until expiry unless you implement a revocation mechanism. Keep JWT lifetimes short and store them securely. + +[Read the JWT auth docs](/docs/products/auth/jwt). + +# SSR auth + +SSR auth is designed for server-side rendering frameworks like Next.js, SvelteKit, and Nuxt. It allows you to read and write session cookies on the server, keeping session management consistent between server and client renders. + +**When to use it:** Any SSR or hybrid app where session cookies need to be available during the initial server render. Without SSR auth, server-rendered pages do not have access to the client's session, causing hydration mismatches and requiring extra client-side fetches. + +[Read the SSR auth docs](/docs/products/auth/server-side-rendering). + +# Custom tokens + +Custom tokens let your backend generate a short-lived token that a client can exchange for a full Appwrite session. You control who gets a token and under what conditions. + +**When to use it:** Migrating users from an existing auth system without forcing a password reset. Also useful for auth flows not covered by the built-in methods, such as biometrics handled outside Appwrite or a proprietary SSO system. + +**Trade-offs:** You are building and maintaining the token issuance logic. Security is your responsibility on the issuance side. + +[Read the Custom token auth docs](/docs/products/auth/custom-token). + +# Multi-factor authentication (MFA) + +MFA adds a second verification step on top of any primary auth method. Appwrite supports TOTP-based authenticator apps and email OTP as MFA factors. + +**When to use it:** Any app handling sensitive data, financial information, or user accounts with elevated privileges. For B2B SaaS, offering MFA is often a requirement for enterprise customers. + +**Trade-offs:** MFA adds a step to the login flow. Some users will find it annoying. Making MFA optional but encouraged (rather than mandatory) often works better for consumer apps. For admin accounts, mandatory MFA is worth the friction. + +[Read the MFA auth docs](/docs/products/auth/mfa). + +# Teams and labels for access control + +Authentication is about proving identity. Authorization is about what that identity can do. Appwrite separates the two cleanly. + +Teams let you group users and assign roles within the group. A user can belong to multiple teams with different roles in each. Labels let you tag individual users with arbitrary strings and then use those tags in permission rules. Together, teams and labels give you a flexible access control layer without writing custom middleware. + +[Read the Teams docs](/docs/products/auth/teams). [Read the Labels docs](/docs/products/auth/labels). + +# Choosing the right method + +Here is a quick reference: + +- **Email/password:** Solid default for most apps. Add MFA for sensitive accounts. +- **Phone SMS:** Best when phone number is the primary identifier. +- **Magic URL:** Low-friction desktop web flows where users check email. +- **Email OTP:** Mobile apps where switching to a browser link is inconvenient. +- **OAuth2:** Fastest sign-up experience. Use when third-party provider dependency is acceptable. +- **Anonymous sessions:** Pre-registration flows, shopping carts, demos. +- **JWT:** Server-to-Appwrite requests on behalf of a user. +- **SSR auth:** Server-rendered frameworks requiring session access on first render. +- **Custom tokens:** Auth migrations and external SSO integrations. +- **MFA:** Second layer for any method, strongly recommended for sensitive data. + +# Start building with Appwrite Auth + +Appwrite Auth handles the complexity of credential storage, session management, and provider integrations so you can focus on your product. Every method described here is available in Appwrite Cloud with no additional setup. + +- [Appwrite Auth overview](/docs/products/auth) +- [Sign up for Appwrite Cloud](https://cloud.appwrite.io) diff --git a/src/routes/blog/post/appwrite-custom-domains/+page.markdoc b/src/routes/blog/post/appwrite-custom-domains/+page.markdoc new file mode 100644 index 0000000000..04a2da8c4b --- /dev/null +++ b/src/routes/blog/post/appwrite-custom-domains/+page.markdoc @@ -0,0 +1,92 @@ +--- +layout: post +title: "Appwrite custom domains: setting up and managing endpoints" +description: Learn why custom domains matter for Appwrite security, how to configure a CNAME record, verify your domain, and update your SDK endpoint for same-domain cookies. +date: 2026-03-27 +cover: /images/blog/appwrite-custom-domains/cover.png +timeToRead: 4 +author: aditya-oberai +category: product, tutorial, security +featured: false +unlisted: true +--- + +If your web app runs on `my-app.com` and your Appwrite instance is on `cloud.appwrite.io`, the browser sees them as different domains. That distinction has a direct effect on how your users' sessions are stored, and it is not in your users' favor. + +[Custom domains](/docs/advanced/platform/custom-domains) let you point a subdomain of your own domain at the Appwrite API, so the browser treats sessions as first-party. This is not just branding. It is a meaningful security improvement that takes about ten minutes to set up. + +# Why this matters: third-party cookies and session storage + +Modern browsers restrict third-party cookies to protect users from cross-site tracking. When your app is on `my-app.com` and calls an API on `cloud.appwrite.io`, the browser classifies Appwrite's cookies as third-party and blocks them. + +When cookies are blocked, Appwrite falls back to storing sessions in `localStorage`. This keeps your app functional, but `localStorage` has a significant weakness: any JavaScript running on your page can read it. If your app has an XSS vulnerability, or a compromised dependency injects a script, that script can read the session token directly from `localStorage` and use it to make authenticated requests. + +Cookies with the `HttpOnly` flag cannot be read by JavaScript at all. An attacker with XSS access cannot steal a session stored in a cookie the way they can steal one stored in `localStorage`. This is the practical security difference between running Appwrite on a third-party domain versus your own domain. + +# How custom domains fix the problem + +When your app runs on `my-app.com` and your Appwrite API is on `appwrite.my-app.com`, the browser sees both as the same domain. Cookies set by Appwrite are now first-party cookies. They get `HttpOnly` protection, they are not blocked, and they do not fall back to `localStorage`. + +You get better session security without changing a single line of application logic, just a DNS record and a configuration step. + +# Setting up a custom domain + +## Step 1: Open the Custom domains settings + +In the Appwrite Console, navigate to your project, then go to **Settings** > **Custom domains** > **Create domain**. + +Enter the subdomain you want to use. A common convention is `appwrite.your-domain.com`, but any subdomain works as long as you control the DNS for it. + +## Step 2: Add the CNAME record + +After entering your domain, Appwrite will display a CNAME record value. Log in to your DNS provider and add a CNAME record pointing your chosen subdomain to the value Appwrite provides. + +The exact steps vary by provider. Most registrars have documentation for adding CNAME records. Common providers include: + +- Cloudflare: **DNS** > **Records** > **Add record** > Type: CNAME +- GoDaddy: **DNS Management** > **Add** > Type: CNAME +- Namecheap: **Advanced DNS** > **Add New Record** > Type: CNAME +- AWS Route 53: **Hosted Zones** > **Create record** > Record type: CNAME + +If your provider is not listed, search for "add CNAME record" in their help documentation. + +## Step 3: Verify the domain + +Back in the Appwrite Console, click **Verify** after adding the CNAME record. DNS changes can take up to 48 hours to propagate globally, so verification may not succeed immediately. + +If verification fails on the first try, wait a few hours and try again. You can check whether your DNS change has propagated using a tool like [dnschecker.org](https://dnschecker.org) before retrying. + +## Step 4: Generate the SSL certificate + +Once verification succeeds, Appwrite automatically provisions an SSL certificate for your domain. No configuration needed. Your custom domain is immediately served over HTTPS. + +# Update your SDK endpoint + +With the custom domain configured, update your Appwrite client initialization to use it: + +```js +import { Client } from "appwrite"; + +const client = new Client() + .setEndpoint('https://appwrite.my-app.com/v1') + .setProject(''); +``` + +Replace `appwrite.my-app.com` with your actual subdomain. The `/v1` path suffix is required. Everything else in your app stays the same. + +For mobile apps, you can also set a custom domain, though the third-party cookie issue is primarily a concern for web apps. The session security benefits of using a known, controlled endpoint still apply. + +# DNS propagation and debugging + +DNS changes take time. The 48-hour figure is a worst case, but 15 to 30 minutes is typical for most providers. If verification is not working after an hour, check these things: + +- Confirm the CNAME record was saved correctly by checking your DNS provider's dashboard. +- Use a DNS lookup tool to verify the CNAME is resolving to the Appwrite target. +- Some DNS providers cache aggressively. Log out and log back in to see the latest record state. + +If the SSL certificate generation fails after verification, try again. Certificate issuance occasionally requires a retry, especially if DNS was still propagating during the first attempt. + +# Resources + +- [Custom domains documentation](/docs/advanced/platform/custom-domains) +- [Sign up for Appwrite Cloud](https://cloud.appwrite.io) diff --git a/src/routes/blog/post/appwrite-functions-guide/+page.markdoc b/src/routes/blog/post/appwrite-functions-guide/+page.markdoc new file mode 100644 index 0000000000..93d445981c --- /dev/null +++ b/src/routes/blog/post/appwrite-functions-guide/+page.markdoc @@ -0,0 +1,201 @@ +--- +layout: post +title: "Appwrite Functions: everything you need to know" +description: A complete guide to Appwrite Functions covering triggers, execution modes, deployment options, permissions, and practical use cases for server-side logic. +date: 2026-03-24 +cover: /images/blog/appwrite-functions-guide/cover.png +timeToRead: 5 +author: aditya-oberai +category: product, tutorial +featured: false +unlisted: true +--- + +Every app eventually needs server-side logic that does not fit neatly into a database write or a storage upload. Sending an email after a user signs up, resizing an image after it is uploaded, charging a customer after an order is placed. This is the gap that Appwrite Functions fills. + +[Appwrite Functions](/docs/products/functions) run your custom code inside isolated containers, triggered by HTTP requests, events, schedules, or SDK calls. They give you full control over server-side logic without managing servers, and they integrate directly with the rest of your Appwrite project. + +# What Appwrite Functions are + +Each function is a self-contained unit of code. You define the runtime (Node.js, Python, Go, Dart, PHP, Ruby, and more), write your handler, and deploy. Appwrite wraps it in an isolated container with its own environment variables, its own URL, and its own execution history. + +Functions are not long-running services. They start, execute, and stop. For persistent workloads, you would use a separate server. For discrete tasks triggered by an event or a schedule, functions are the right tool. + +# Triggers + +Appwrite Functions support five trigger types. Choosing the right trigger is the first design decision for any function. + +## HTTP requests + +Every function gets a unique URL. Any HTTP request to that URL triggers the function. The function receives the full request (method, headers, body, query parameters) and returns a response. + +This is the most flexible trigger. Use it to build webhooks, REST endpoints, API proxies, or any server-side endpoint your client needs to call directly. + +## SDK execution + +Client and server SDKs can trigger a function programmatically: + +```js +import { Functions } from "appwrite"; + +const functions = new Functions(client); + +const execution = await functions.createExecution({ + functionId: "[FUNCTION_ID]", + body: JSON.stringify({ userId: "abc123" }), + async: false, + path: "/", + method: "POST" +}); + +console.log(execution.responseBody); +``` + +SDK-triggered functions behave identically to HTTP-triggered functions. The difference is that SDK calls go through Appwrite's auth layer, so the function context includes the calling user's session. + +## Server events + +Appwrite emits events when things happen inside your project: a row is created, a file is uploaded, a user signs up. You can configure a function to fire on any of these events. + +Event-triggered functions receive the event name and the full resource payload in the request body. The `x-appwrite-event` header tells the function which event fired. + +Examples of events you can react to: + +- `databases.[DATABASE_ID].tables.[TABLE_ID].rows.*.create` fires when any row is created in a table +- `storage.[BUCKET_ID].files.*.create` fires when a file is uploaded +- `users.*.create` fires when a new user registers +- `users.*.sessions.*.create` fires on every new login + +This is the right trigger for side effects: post-processing uploads, sending notifications, syncing data to external systems. + +## Scheduled executions (cron) + +Functions can run on a cron schedule, as frequently as once every minute. The cron syntax is standard: + +``` +* * * * * every minute +0 * * * * every hour +0 9 * * 1 every Monday at 9am UTC +``` + +Use scheduled functions for recurring jobs: daily reports, cleanup tasks, cache refreshes, subscription renewals, and anything else that needs to run on a timer rather than in response to an event. + +## Delayed executions + +SDK execution supports a delay parameter that schedules the function to run at a future time. This is useful for deferred processing, follow-up emails a set number of hours after a user action, or any task that should happen later rather than immediately. + +# Execution modes: synchronous vs asynchronous + +Every function execution is either synchronous or asynchronous. This matters for how your calling code behaves. + +## Synchronous execution + +The caller waits for the function to complete and receives the response body directly. Synchronous executions have a hard 30-second timeout. If the function does not complete within 30 seconds, the execution is terminated. + +Synchronous mode is the right choice when: + +- The caller needs the function's output to proceed +- The function is fast enough to complete within 30 seconds +- You are building an API endpoint that must return data + +```js +const execution = await functions.createExecution({ + functionId: "[FUNCTION_ID]", + body, + async: false +}); +// execution.responseBody contains the function's output +``` + +## Asynchronous execution + +The caller receives an execution ID immediately and does not wait for the function to finish. The function runs in the background. You can check its status later by fetching the execution by ID. + +Asynchronous mode is the right choice when: + +- The task takes longer than 30 seconds +- The caller does not need the result immediately +- You are processing uploads, sending emails, or running batch jobs + +```js +const execution = await functions.createExecution({ + functionId: "[FUNCTION_ID]", + body, + async: true +}); +// execution.status will be "waiting" or "processing" +// poll later with functions.getExecution(functionId, execution.$id) +``` + +HTTP-triggered functions always behave synchronously from the HTTP client's perspective: the connection stays open until the function returns a response or times out. + +# Deployment + +## Manual deployment + +You can upload a compressed archive of your function code directly in the Appwrite Console or via the CLI. This is the fastest way to test a function, but it does not version your code or integrate with your development workflow. + +## Deploy from Git + +Appwrite Functions support continuous deployment from GitHub, GitLab, and Bitbucket. Connect a repository, specify the branch and build command, and Appwrite automatically deploys a new version every time you push. + +Git deployment is the recommended approach for production functions. It gives you: + +- Version history tied to your commits +- Automatic redeployment on push +- Rollback to any previous deployment +- Code review via pull requests before deployment + +[Deploy from Git docs](/docs/products/functions/deploy-from-git) + +## Build commands and environment variables + +Each function has its own build command (for installing dependencies and compiling code) and its own set of environment variables. Environment variables are encrypted at rest and injected into the function at runtime. Never hard-code secrets; always use environment variables. + +# Permissions + +## Client SDK execution + +When a client calls a function via the SDK, Appwrite checks whether the function has the `execute` permission for the calling user's role. By default, no client can execute a function. You must explicitly grant execute permission. + +To allow all authenticated users to execute a function: + +```js +[Permission.execute(Role.users())] +``` + +To restrict execution to a specific team: + +```js +[Permission.execute(Role.team("teamABC"))] +``` + +## Server SDK execution + +Server SDK calls with an API key bypass function-level permissions, subject to the API key's scope. The API key must have the `functions.executions.write` scope to trigger a function. + +## HTTP endpoint execution + +Direct HTTP calls to the function URL also go through the permission check. If the function is not publicly executable (`Role.any()`), unauthenticated HTTP calls will be rejected. + +# Use cases + +**Webhooks.** Functions are natural webhook receivers. Point a Stripe, Twilio, or GitHub webhook at your function URL. The function receives the payload, validates the signature, and updates your Appwrite database. + +**Post-processing uploads.** Trigger a function on `storage.*.files.*.create` to resize images, extract metadata, run virus scans, or generate thumbnails immediately after upload. + +**Email and notification delivery.** Trigger a function on user creation or order completion to send transactional email via SendGrid, Resend, or any email API. Keep your email credentials in function environment variables. + +**Scheduled reports.** Run a function every morning to aggregate the previous day's data, write a summary row, and send a report email to stakeholders. + +**Third-party API proxies.** Expose a function as an HTTP endpoint that calls a third-party API with a secret key. Clients call your function; the function calls the external API. API keys never leave the server. + +**Data validation and enrichment.** Trigger a function on row creation to validate fields, enrich data from external sources, or enforce business rules that cannot be expressed as database constraints. + +# Get started with Appwrite Functions + +Appwrite Functions give you server-side execution that integrates directly with your Appwrite project, scales automatically, and requires no server management. Every trigger type, execution mode, and runtime is available on Appwrite Cloud. + +- [Appwrite Functions overview](/docs/products/functions) +- [Function execution docs](/docs/products/functions/execute) +- [Sign up for Appwrite Cloud](https://cloud.appwrite.io) diff --git a/src/routes/blog/post/appwrite-indexes/+page.markdoc b/src/routes/blog/post/appwrite-indexes/+page.markdoc new file mode 100644 index 0000000000..f3c83834e6 --- /dev/null +++ b/src/routes/blog/post/appwrite-indexes/+page.markdoc @@ -0,0 +1,167 @@ +--- +layout: post +title: "Appwrite Indexes: how to speed up your database queries" +description: Learn what indexes are in Appwrite Databases, which types to use, when to create them, and when adding more indexes actually hurts performance. +date: 2026-03-26 +cover: /images/blog/appwrite-indexes/cover.png +timeToRead: 5 +author: aditya-oberai +category: product, tutorial +featured: false +unlisted: true +--- + +If your Appwrite queries are slow, or getting slower as your data grows, the most likely fix is an index. Without indexes, every filtered or sorted query requires a full table scan: the database reads every row and checks whether it matches your conditions. That is fine for a table with 100 rows. It becomes a serious problem at 100,000. + +This post explains what indexes are, the types Appwrite supports, how to decide which columns to index, and the one rule most developers ignore until their write performance degrades. + +# What an index actually is + +Appwrite uses MariaDB under the hood, even though the API surface looks like a row store. Your tables are tables, your rows are rows, and your columns are columns. An index is a separate data structure that MariaDB maintains alongside the table. It stores the values of one or more columns in a sorted, searchable form, along with pointers back to the rows they belong to. + +When you run a query with `Query.equal('status', ['published'])`, the database can either: + +1. Read every row in the table and check the `status` column (full scan), or +2. Look up `'published'` in a pre-built index on `status` and jump directly to the matching rows + +The second path is orders of magnitude faster for large tables. The index is the thing that makes option 2 possible. + +# Index types in Appwrite + +Appwrite exposes four index types when you create an index on a table. + +## Key index + +A key index is the standard index for equality and range queries. Use it for columns you filter with `Query.equal`, `Query.notEqual`, `Query.greaterThan`, `Query.lessThan`, `Query.between`, and similar operators. It is also the right index for columns you use in `Query.orderAsc` or `Query.orderDesc`. + +If you frequently query rows by `userId`, `status`, `createdAt`, or any column you filter or sort on, add a key index. + +## Unique index + +A unique index does everything a key index does, plus it enforces that no two rows can have the same value for that column. Use it when a column must be unique across all rows: usernames, email addresses, external IDs, slugs. + +Unique indexes are enforced at the database level, not just in your application code. That means even if two concurrent requests try to create rows with the same value, only one will succeed. + +## Fulltext index + +A fulltext index is required for [`Query.search`](/docs/products/databases/queries). It enables word-level text search across string columns. Without a fulltext index on the target column, `Query.search` returns an error. + +Fulltext indexes are suited for content columns like `title`, `body`, or `description`. They are not appropriate for short identifiers or numeric fields. + +## Spatial index + +A spatial index is required for geographic columns: Point, Line, and Polygon. It uses optimized data structures designed for geographic operations and is what makes geo queries like `Query.geoWithin`, `Query.geoIntersects`, and distance-based queries performant. + +Without a spatial index, geo queries will still run but will perform a full scan. If you are storing location data and plan to query it, a spatial index is strongly recommended. + +# Creating an index + +You can create indexes from the Appwrite Console or programmatically using the Databases API. In the Console, navigate to your database, open the table, and go to the Indexes tab. Select the index type, choose the column, and save. + +Using the Appwrite Node.js SDK: + +```js +import { Client, TablesDB } from "node-appwrite"; + +const client = new Client() + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + .setKey(""); + +const tablesDB = new TablesDB(client); + +await tablesDB.createIndex({ + databaseId: "", + tableId: "", + key: "status_index", // Index ID + type: "key", // Index type: "key", "unique", or "fulltext" + attributes: ["status"], // Columns to index + orders: ["ASC"], // Sort order per column +}); +``` + +For a unique index on `email`: + +```js +await tablesDB.createIndex({ + databaseId: "", + tableId: "", + key: "email_unique", + type: "unique", + attributes: ["email"], + orders: ["ASC"], +}); +``` + +Indexes are created asynchronously. After you call `createIndex`, the index will show as `processing` until it is ready. For large tables this can take some time. + +# Which columns to index + +A practical checklist: + +- Index every column you use in `Query.equal` or `Query.notEqual` on large tables +- Index columns you pass to `Query.orderAsc` or `Query.orderDesc` +- Index columns used in `Query.between`, `Query.greaterThan`, or `Query.lessThan` +- Add a fulltext index on every column you use with `Query.search` +- Add a unique index on columns that must be unique + +A real-world example: a blog posts table. You query by `status` (published, draft), sort by `createdAt`, and search by `title`. You would want: + +1. A key index on `status` +2. A key index on `createdAt` +3. A fulltext index on `title` + +That covers your three most common query patterns. If you also frequently filter by both `status` and `authorId` together, read the section on composite indexes below. + +## Composite indexes + +A composite index covers multiple columns at once. When your queries consistently filter on two columns together, a composite index is more efficient than two separate single-column indexes. + +```js +await tablesDB.createIndex({ + databaseId: "", + tableId: "", + key: "status_author_index", + type: "key", + attributes: ["status", "authorId"], + orders: ["ASC", "ASC"], +}); +``` + +This index is useful when you run queries like: + +```js +[Query.equal("status", ["published"]), Query.equal("authorId", [""])] +``` + +The order of columns in a composite index matters. The index above is efficient for filtering on `status` alone, or on `status` plus `authorId` together. It is not efficient for filtering on `authorId` alone. Design composite indexes based on the most common query pattern. + +# When NOT to add an index + +Indexes are not free. Every index on a table adds overhead to every write operation. When you insert, update, or delete a row, MariaDB must update all indexes on that table. A table with 10 indexes on it will have noticeably slower writes than one with 2. + +Avoid indexing: + +- Columns with very low cardinality that you rarely query on. A boolean column with only two possible values rarely benefits from a key index unless the table is very large and one value is rare. +- Columns you never filter or sort on. An index that no query ever uses just adds write overhead. +- Every column by default. Only index based on actual query patterns. + +A good signal that you have over-indexed: your write throughput drops significantly as insert volume increases and your read patterns have not changed. + +# Indexes and the Query API together + +Indexes and the Query API work as a pair. The Query API expresses what data you want; indexes are what make retrieving that data fast. Adding `Query.orderDesc('createdAt')` without a key index on `createdAt` will work, but it forces a full scan followed by a sort in memory. + +Before putting a table into production, review every query pattern you will run on it and verify the needed indexes are in place. Adding indexes retroactively on large tables takes time and temporarily affects performance during the build phase. + +The [Appwrite Databases queries reference](/docs/products/databases/queries) rows every `Query` method and makes it clear which ones require or benefit from specific index types. Keep it open when designing your schema. + +# Build faster queries with the right indexes + +Indexes are one of the highest-leverage things you can do for database performance. A single well-placed index can turn a multi-second query into a millisecond one. The tradeoff is write overhead, so index deliberately based on real query patterns rather than speculatively on every column. + +Start with the columns you filter and sort on most often, add fulltext indexes for search, and use unique indexes to enforce data integrity at the database level. + +- [Appwrite Databases tables docs](/docs/products/databases/tables) +- [Appwrite Databases queries docs](/docs/products/databases/queries) +- [Start building on Appwrite Cloud](https://cloud.appwrite.io) diff --git a/src/routes/blog/post/appwrite-magic-link/+page.markdoc b/src/routes/blog/post/appwrite-magic-link/+page.markdoc new file mode 100644 index 0000000000..70331c890f --- /dev/null +++ b/src/routes/blog/post/appwrite-magic-link/+page.markdoc @@ -0,0 +1,120 @@ +--- +layout: post +title: "How Appwrite's Magic Link auth improves user experience" +description: Learn how Appwrite's Magic Link authentication works, how to implement the two-step flow, and when to use it over passwords or email OTP for better UX. +date: 2026-03-27 +cover: /images/blog/appwrite-magic-link/cover.png +timeToRead: 4 +author: aditya-oberai +category: product, tutorial, security +featured: false +unlisted: true +--- + +Password-based authentication adds friction at every step. Users forget passwords, pick weak ones, or reuse them across sites. Your app pays the cost in support tickets, reset flows, and abandoned signups. + +Magic Link authentication removes the password entirely. A user enters their email address, gets a link, clicks it, and is logged in. No password to create, no password to remember, no reset flow to build. + +Appwrite supports [Magic Link](/docs/products/auth/magic-url) authentication out of the box, with a clean two-step implementation that integrates with your existing auth setup. + +# How Magic Link authentication works + +The flow has two phases: sending the magic link, and creating the session after the user clicks it. + +When a user requests a magic link, Appwrite generates a short-lived token and sends it to their email address embedded in a URL. The URL points to a page you control in your app. When the user clicks the link, your app reads the token and user ID from the query parameters and exchanges them for a session. + +That is the complete flow. There are no cookies to manage on the initial request, no passwords to hash, and no password reset infrastructure to maintain separately. + +# Implementing the two-step flow + +## Step 1: Request the magic link + +Call `account.createMagicURLToken` with a unique user ID, the user's email address, and the URL in your app where you want to handle verification: + +```js +import { Client, Account, ID } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(''); + +const account = new Account(client); + +const token = await account.createMagicURLToken({ + userId: ID.unique(), + email: 'user@example.com', + url: 'https://yourapp.com/verify' +}); +``` + +Appwrite appends `secret` and `userId` as query parameters to the redirect URL before sending it in the email. The user receives a message with a link like: + +``` +https://yourapp.com/verify?userId=abc123&secret=xyz789 +``` + +If the email address is new, Appwrite creates a new account using the `userId` you supplied. If the email is already attached to an existing account, Appwrite ignores the `userId` you provided and sends the magic link to the existing account owner. + +## Step 2: Create the session + +Your verification page reads the query parameters and calls `account.createSession`: + +```js +import { Client, Account } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(''); + +const account = new Account(client); + +const urlParams = new URLSearchParams(window.location.search); +const secret = urlParams.get('secret'); +const userId = urlParams.get('userId'); + +const session = await account.createSession({ userId, secret }); +``` + +After this call succeeds, the user is authenticated. Appwrite sets the session cookie and the user can access protected resources. The token is single-use and expires, so replaying the URL after the session is created will fail. + +# UX benefits that compound over time + +Removing passwords is not just a convenience improvement. It affects multiple points in the user journey: + +**Onboarding** is faster. New users do not need to choose and confirm a password before they can see the app. The signup and login flows merge into a single action. + +**Support load drops.** Password reset is one of the most common support requests for any app with user accounts. Magic Link eliminates it entirely for users who choose that method. + +**Security improves by default.** There is no password database to breach, no weak passwords to crack, and no credential stuffing attacks against your users. The only attack surface is the user's email inbox, which is already a trust anchor for most authentication flows anyway. + +# When to use Magic Link vs other methods + +Magic Link is not the right choice for every app or every user. + +**Use Magic Link when:** +- Your users check email regularly, typically on desktop or in a professional context. +- You want to reduce support tickets from forgotten passwords. +- You are building a tool where the email inbox is already part of the workflow. +- You want fast onboarding for infrequent users who would otherwise forget their password between sessions. + +**Consider other methods when:** +- Your users are on mobile and may not have easy access to their email client during the login flow. +- Low latency matters, such as in gaming or real-time collaboration apps. Waiting for an email introduces friction that passwords do not. +- Users are offline or in environments where email delivery is unreliable. + +For mobile-first apps or users who want a faster in-app experience, email OTP is a close alternative. Instead of clicking a link, the user receives a short numeric code they enter directly in your app. The authentication logic is similar, but the UX fits mobile better and does not require leaving the app to check email. + +# Magic Link vs email OTP + +Both Magic Link and Email OTP are passwordless and use the user's email as a trust anchor. The implementation is also similar: both involve a token that expires after a short window. + +The difference is user experience. Magic Link sends users out of your app and back, which works well on desktop where switching to an email client is a single click. Email OTP keeps the user in the app, which is smoother on mobile. + +Appwrite supports both. You can offer Magic Link for desktop users and Email OTP for mobile users, or let users choose their preferred method. Neither requires maintaining separate token infrastructure since Appwrite handles generation, delivery, and validation for both. + +# Build passwordless authentication with Appwrite + +- [Magic URL authentication docs](/docs/products/auth/magic-url) +- [Email OTP authentication docs](/docs/products/auth/email-otp) +- [Appwrite Authentication overview](/docs/products/auth) +- [Sign up for Appwrite Cloud](https://cloud.appwrite.io) diff --git a/src/routes/blog/post/appwrite-messaging-push-email/+page.markdoc b/src/routes/blog/post/appwrite-messaging-push-email/+page.markdoc new file mode 100644 index 0000000000..87676d829e --- /dev/null +++ b/src/routes/blog/post/appwrite-messaging-push-email/+page.markdoc @@ -0,0 +1,143 @@ +--- +layout: post +title: "Using Appwrite Messaging for push notifications and email" +description: Learn how to set up Appwrite Messaging to send push notifications and emails, when to use each channel, and how to target users and topics effectively. +date: 2026-03-26 +cover: /images/blog/appwrite-messaging-push-email/cover.png +timeToRead: 5 +author: aditya-oberai +category: product, tutorial +featured: false +unlisted: true +--- + +Most apps eventually need to reach users outside the app itself. A background sync completes, a purchase is confirmed, a security alert fires. The question is not whether you need messaging, it is which channel to use and how to wire it up without managing separate services for each one. + +[Appwrite Messaging](/docs/products/messaging) handles push notifications, email, and SMS from a single API. You configure providers once, define targets per user, and send or schedule messages to individuals, groups, or topics. This post covers the two highest-traffic channels: push notifications and email. + +# Why keeping messaging in one place matters + +The alternative is common and painful: a push library from Firebase, a transactional email service with its own SDK, an SMS provider on top of that. Each has separate authentication, separate dashboards, and separate retry logic to maintain. + +When everything lives in Appwrite Messaging, your targeting logic (which user, which channel, which topic) is written once. You can also schedule messages from the same API, so a single call handles "send now" and "send in 48 hours" identically. + +# Choosing the right channel + +Before writing any code, match the message to the channel. + +**Push notifications** are best for time-sensitive information that the user should see quickly: a new chat message, a delivery update, a security alert. They appear on the lock screen and interrupt whatever the user is doing, so reserve them for content that earns that interruption. + +**Email** is better for content with longer shelf life or more detail: receipts, invoices, newsletters, password resets, weekly digests. Users expect to find email later; they do not expect a push notification from three days ago to still be relevant. + +**SMS** sits between the two. It is high-visibility like push, but more appropriate for one-time passcodes and delivery notifications where the user may not have the app installed. + +# Setting up push notifications + +Push notifications on mobile require a provider. Apple devices use APNs (Apple Push Notification service); Android devices use FCM (Firebase Cloud Messaging). Appwrite supports both. + +Start by adding a provider in the Appwrite Console under **Messaging** > **Providers** > **Add provider** > **Push notification**. Select APNs or FCM and supply your credentials. For APNs, you need an authentication key from your Apple Developer account. For FCM, you need a service account JSON from the Firebase Console. + +Once the provider is configured, your app needs to register a push target for each logged-in user. A push target links a device token to an Appwrite account. + +For Android with FCM, fetch the registration token after Firebase initializes and create a push target when the user logs in: + +```kotlin +val session = account.createEmailPasswordSession(email, password) +val target = account.createPushTarget( + targetId = ID.unique(), + identifier = fcmToken +) +``` + +For iOS with APNs, register for remote notifications in your app delegate, capture the device token, and create the push target the same way: + +```swift +let target = try await account.createPushTarget( + targetId: ID.unique(), + identifier: apnsToken +) +``` + +FCM tokens can rotate, so you also need to handle the refresh event and call `updatePushTarget` with the new token to keep targeting accurate. + +## Sending a push notification + +With a provider and targets set up, you can send from the Console or via the Server SDK. Here is a Node.js example: + +```js +const message = await messaging.createPush({ + messageId: ID.unique(), + title: 'Your order has shipped', + body: 'Estimated delivery: tomorrow by 5pm.', + users: ['user-id-here'], +}); +``` + +The `users` array accepts Appwrite user IDs. You can also pass `targets` (specific device tokens) or `topics` (groups of subscribers) depending on how broadly you want to reach. + +# Setting up email + +Email in Appwrite Messaging requires an SMTP provider. Appwrite supports Mailgun and SendGrid. Add one under **Messaging** > **Providers** > **Add provider** > **Email**, then follow the configuration wizard. + +Users who signed up with email and password already have an email target attached to their account. For users who signed up another way, you can add an email target programmatically: + +```js +const target = await users.createTarget({ + userId, + targetId: ID.unique(), + providerType: sdk.MessagingProviderType.Email, + identifier: 'user@example.com', + providerId +}); +``` + +## Sending an email + +```js +const message = await messaging.createEmail({ + messageId: ID.unique(), + subject: 'Your invoice from Acme', + content: '

Invoice #1042

Due: April 1, 2026

', + users: ['user-id-here'], +}); +``` + +For HTML email, pass the full markup in the `content` field. If you are sending plain text, leave out HTML tags. Appwrite handles delivery through the configured SMTP provider. + +# Targeting with topics + +For broadcast scenarios, like pushing a feature announcement to all users on a specific plan, topics are the right tool. Create a topic in the Console or via the API, subscribe users or targets to it, then send to the topic ID instead of individual user IDs: + +```js +const message = await messaging.createPush({ + messageId: ID.unique(), + title: 'New feature: dark mode', + body: 'Dark mode is now available in settings.', + topics: ['pro-plan-users'], +}); +``` + +Appwrite resolves the topic to all subscribed targets automatically. You do not need to paginate through user lists or build your own fanout logic. + +# Scheduling messages + +Both push and email support scheduled delivery. Add a `scheduledAt` field with an ISO 8601 timestamp and Appwrite handles the rest: + +```js +const message = await messaging.createEmail({ + messageId: ID.unique(), + subject: 'Weekly digest', + content: digest, + topics: ['all-users'], + scheduledAt: '2026-04-01T09:00:00+00:00', +}); +``` + +This is useful for newsletters, reminder sequences, and any situation where you want to queue messages in advance. + +# Get started with Appwrite Messaging + +- [Appwrite Messaging overview](/docs/products/messaging) +- [Send push notifications](/docs/products/messaging/send-push-notifications) +- [Send email messages](/docs/products/messaging/send-email-messages) +- [Sign up for Appwrite Cloud](https://cloud.appwrite.io) diff --git a/src/routes/blog/post/appwrite-oauth/+page.markdoc b/src/routes/blog/post/appwrite-oauth/+page.markdoc new file mode 100644 index 0000000000..23349db17b --- /dev/null +++ b/src/routes/blog/post/appwrite-oauth/+page.markdoc @@ -0,0 +1,261 @@ +--- +layout: post +title: "How Appwrite handles OAuth: Google, GitHub, and beyond" +description: Learn how the createOAuth2Token flow works in Appwrite, how to configure providers, retrieve user profile data, and handle access token refresh for 30+ providers. +date: 2026-03-25 +cover: /images/blog/appwrite-oauth/cover.png +timeToRead: 5 +author: aditya-oberai +category: product, tutorial, security +featured: false +unlisted: true +--- + +OAuth login is table stakes for most apps. Users expect to sign in with Google, GitHub, or another provider they already trust. But wiring up OAuth yourself means managing authorization codes, token exchanges, refresh flows, and session state. Get any of it wrong and you've either broken logins or created a security hole. + +Appwrite handles the entire OAuth2 flow server-side. You call one SDK method to get an authorization URL, Appwrite does the redirect and token exchange, and you get back credentials to finalize the session. This post explains exactly how that flow works using `createOAuth2Token`, how to configure providers, how to access user profile data, and how to keep access tokens fresh. + +# How OAuth2 works in Appwrite + +Appwrite's `createOAuth2Token` flow separates the authorization redirect from session creation. This gives you explicit control over when and how a session is established, which is essential for server-side rendered apps, mobile clients, and any scenario where you need to set your own cookies or store tokens explicitly. + +Here's the full sequence: + +1. Your app calls `account.createOAuth2Token()` with the provider name, redirect URLs, and optional scopes. It returns an authorization URL. +2. You redirect the user to that URL. Appwrite sends them to the provider's authorization page (Google, GitHub, etc.). +3. The user grants permission. The provider redirects back to Appwrite with an authorization code. +4. Appwrite exchanges the code for an access token and refresh token with the provider. +5. Appwrite redirects the user to your success URL with `userId` and `secret` appended as query parameters. +6. Your app reads those parameters and calls `account.createSession({ userId, secret })` to create the Appwrite session. + +The access and refresh tokens from the OAuth provider are stored alongside the Appwrite session. Your app never handles raw OAuth tokens directly, which keeps them off the client. + +Importantly, OAuth login creates an **[identity](/docs/products/auth/identities)** attached to the user's Appwrite account. A single Appwrite account can have multiple identities: the same user could log in with Google on one device and GitHub on another, and both sessions point to the same account as long as the email address matches. + +# Enabling a provider + +Every OAuth provider needs to be enabled and configured in your Appwrite project before it can be used. + +1. Open the [Appwrite Console](https://cloud.appwrite.io) and select your project. +2. Go to **Auth** > **Settings**. +3. Scroll to **OAuth2 Providers** and click the provider you want to enable. +4. Toggle it on and enter the **App ID** and **App Secret** from the OAuth app you registered with that provider. +5. Copy the **Redirect URI** shown in Appwrite and paste it into your OAuth app's allowed redirect URLs. + +Each provider has a slightly different name for these values. Google calls them "Client ID" and "Client Secret." GitHub uses "Client ID" and "Client Secret" as well. The Appwrite docs for each provider include screenshots and step-by-step instructions. + +Appwrite supports over 30 providers including Google, GitHub, Microsoft, Apple, Discord, Slack, Spotify, Twitter/X, LinkedIn, and many more. + +# Initiating the OAuth flow + +Call `createOAuth2Token()` to get the authorization URL, then redirect the user to it: + +```js +import { Client, Account, OAuthProvider } from 'appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(''); + +const account = new Account(client); + +async function loginWithGitHub() { + const authUrl = await account.createOAuth2Token({ + provider: OAuthProvider.Github, + success: 'https://yourapp.com/callback', + failure: 'https://yourapp.com/login?error=oauth', + scopes: ['read:user', 'user:email'] + }); + + window.location.href = authUrl; +} +``` + +The `scopes` array is optional. If you only need basic authentication (name, email), you usually don't need custom scopes. If you want to call the provider's API on behalf of the user, add the scopes required for those API calls. + +For Google with Calendar access: + +```js +const authUrl = await account.createOAuth2Token({ + provider: OAuthProvider.Google, + success: 'https://yourapp.com/callback', + failure: 'https://yourapp.com/login?error=oauth', + scopes: ['https://www.googleapis.com/auth/calendar.readonly'] +}); + +window.location.href = authUrl; +``` + +# Completing the flow by creating session + +When Appwrite redirects the user to your success URL, it appends `userId` and `secret` as query parameters. Read these and exchange them for a session: + +```js +// On your callback page (e.g. https://yourapp.com/callback) +const params = new URLSearchParams(window.location.search); +const userId = params.get('userId'); +const secret = params.get('secret'); + +if (userId && secret) { + const session = await account.createSession({ userId, secret }); + // User is now logged in, redirect to your app + window.location.href = '/dashboard'; +} +``` + +This two-step approach means you decide when and how the session is stored. In a Next.js or SvelteKit app, you can handle this server-side and set an HTTP-only cookie. In a React Native app, you control token storage explicitly. + +# Getting user and provider data + +After the session is created, fetch the Appwrite account to get the user's name and email, which Appwrite populates from the OAuth provider during login: + +```js +const user = await account.get(); + +console.log(user.name); // display name from provider +console.log(user.email); // email from provider +``` + +Provider-specific details (the provider name, the user's ID on the provider, and the OAuth access token) are not on the session object in this flow. With `createOAuth2Token`, the session is created separately by your app via `createSession`, decoupled from the OAuth exchange itself. With `createOAuth2Session`, Appwrite creates the session directly as part of the OAuth flow, so provider info is embedded in the session response. Because `createOAuth2Token` separates these steps, provider details live on the identity instead. Use the [Identities API](/docs/products/auth/identities) to retrieve them: + +```js +const { identities } = await account.listIdentities(); +const identity = identities[0]; + +console.log(identity.provider); // 'github' +console.log(identity.providerUid); // the user's ID on the provider +console.log(identity.providerEmail); // the user's email on the provider +console.log(identity.providerAccessToken); // OAuth access token +``` + +# Calling the provider's API + +If you requested extra scopes, you can use `identity.providerAccessToken` to call the provider's own API directly from your frontend or backend. + +For example, fetching a user's GitHub repositories: + +```js +const { identities } = await account.listIdentities(); +const identity = identities.find(i => i.provider === 'github'); + +const response = await fetch('https://api.github.com/user/repos', { + headers: { + Authorization: `Bearer ${identity.providerAccessToken}`, + Accept: 'application/vnd.github.v3+json' + } +}); + +const repos = await response.json(); +``` + +Or listing Google Calendar events: + +```js +const { identities } = await account.listIdentities(); +const identity = identities.find(i => i.provider === 'google'); + +const response = await fetch( + 'https://www.googleapis.com/calendar/v3/calendars/primary/events', + { + headers: { + Authorization: `Bearer ${identity.providerAccessToken}` + } + } +); + +const events = await response.json(); +``` + +# Refreshing access tokens + +OAuth access tokens expire. Check `identity.providerAccessTokenExpiry` before making provider API calls, and refresh the token when it's close to expiring. Refreshing is done via the session using `account.updateSession`: + +```js +async function getFreshAccessToken(provider) { + const { identities } = await account.listIdentities(); + let identity = identities.find(i => i.provider === provider); + + const expiry = new Date(identity.providerAccessTokenExpiry); + const minutesUntilExpiry = (expiry - new Date()) / 1000 / 60; + + if (minutesUntilExpiry < 5) { + await account.updateSession({ sessionId: 'current' }); + // Re-fetch identities to get the updated token + const refreshed = await account.listIdentities(); + identity = refreshed.identities.find(i => i.provider === provider); + } + + return identity.providerAccessToken; +} +``` + +`account.updateSession` uses the stored refresh token to get a new access token from the provider. The Appwrite session itself remains valid throughout; only the provider access token is refreshed. + +Note that not all providers issue refresh tokens. Google does by default. GitHub access tokens don't expire (unless the OAuth app is configured with expiring tokens). Check the provider's documentation to understand their token lifecycle. + +# Managing identities + +A user's connected OAuth providers appear as identities on their account. You can list them: + +```js +const { identities } = await account.listIdentities(); + +identities.forEach(identity => { + console.log(identity.provider, identity.providerEmail); +}); +``` + +Users can connect additional providers by going through the `createOAuth2Token` flow again while already logged in. This links the new provider to the existing account. + +To disconnect a provider: + +```js +await account.deleteIdentity({ identityId }); +``` + +# Mobile and native app setup + +On mobile platforms, the OAuth redirect needs special handling because there's no browser URL to redirect to. `createOAuth2Token` is the right choice here: you get the authorization URL, open it in a browser session, and catch the redirect back to your app. + +For React Native with Expo: + +```js +import { makeRedirectUri } from 'expo-auth-session'; +import * as WebBrowser from 'expo-web-browser'; + +const deepLink = new URL(makeRedirectUri({ preferLocalhost: true })); +const scheme = `${deepLink.protocol}//`; + +const authUrl = await account.createOAuth2Token({ + provider: OAuthProvider.Github, + success: `${deepLink}`, + failure: `${deepLink}` +}); + +const result = await WebBrowser.openAuthSessionAsync(`${authUrl}`, scheme); + +const url = new URL(result.url); +const secret = url.searchParams.get('secret'); +const userId = url.searchParams.get('userId'); + +await account.createSession({ userId, secret }); +``` + +For other platforms: + +- **Flutter**: Set the success and failure URLs to your app's deep link scheme, such as `appwrite-callback-://`. +- **Android**: Register an intent filter for the callback scheme in `AndroidManifest.xml`. +- **Apple (iOS/macOS)**: Register a custom URL scheme in your app's Info.plist. + +The Appwrite docs for each platform include exact configuration steps. + +One thing to note: OAuth login is not available via the GraphQL API. Use the REST API or SDK. + +# Start building with Appwrite OAuth + +Appwrite's `createOAuth2Token` flow gives you full control over session creation while handling the hard parts: authorization redirects, token exchange, and refresh. You get a consistent API across 30+ providers without managing any of the OAuth complexity yourself. + +- [Appwrite OAuth2 docs](/docs/products/auth/oauth2) +- [Identities docs](/docs/products/auth/identities) +- [Appwrite Auth overview](/docs/products/auth) +- [Start building on Appwrite Cloud](https://cloud.appwrite.io) diff --git a/src/routes/blog/post/appwrite-permissions/+page.markdoc b/src/routes/blog/post/appwrite-permissions/+page.markdoc new file mode 100644 index 0000000000..4888cda678 --- /dev/null +++ b/src/routes/blog/post/appwrite-permissions/+page.markdoc @@ -0,0 +1,167 @@ +--- +layout: post +title: "Understanding Appwrite permissions: a complete breakdown" +description: A complete breakdown of Appwrite's permission system, covering permission types, role types, common patterns, and pitfalls to avoid in your app. +date: 2026-03-23 +cover: /images/blog/appwrite-permissions/cover.png +timeToRead: 5 +author: aditya-oberai +category: product, tutorial, security +featured: false +unlisted: true +--- + +Permissions are one of those things that feel obvious until something goes wrong. A row that should be private turns out to be readable by anyone. A file that should be editable by a team member throws a 401. An admin endpoint that works fine in development breaks in production because the API key scope was wrong. + +[Appwrite's permission system](/docs/advanced/platform/permissions) is explicit and composable. Once you understand the building blocks, it becomes predictable. This post covers all of them. + +# The two dimensions of permissions + +Every permission in Appwrite has two dimensions: the **permission type** (what action is allowed) and the **role** (who is allowed to perform it). You combine them when setting permissions on any resource. + +# Permission types + +Appwrite defines five permission types: + +- **read** grants the ability to fetch or list a resource. A user with read permission on a row can retrieve it; without read, the row is invisible to them. + +- **create** grants the ability to create new resources within a container. On a table, create permission lets a user add new rows. On a bucket, it lets them upload files. + +- **update** grants the ability to modify an existing resource. On a row, update permission lets a user change field values. + +- **delete** grants the ability to remove a resource permanently. + +- **write** is a convenience alias that combines create, update, and delete. Granting write permission is equivalent to granting all three mutation permissions at once. Use it when you want a role to have full control over a resource without read restrictions. + +Note that **write does not include read**. A role with write permission but not read permission can create, update, and delete resources without being able to list or fetch them. This is rarely what you want, but it is a valid configuration for certain write-only patterns. + +# Role types + +Roles define who a permission applies to. Appwrite provides several role types that cover most access patterns. + +- **`any()`** matches every request, authenticated or not. This is the role you use for public resources. A row with `Permission.read(Role.any())` is readable by anyone who can reach your Appwrite endpoint. + +- **`guests()`** matches only unauthenticated requests. Use this when you want to allow access to a resource only before a user has logged in, such as a public landing page row that logged-in users should not see (rare, but valid). + +- **`users()`** matches all authenticated users, regardless of which user or what account status they have. You can optionally filter by status: `Role.users("verified")` matches only users who have verified their email address. + +- **`user([USER_ID])`** matches a single specific user by their ID. This is the most granular user-level permission. You can also filter by status: `Role.user("user123", "verified")`. + +- **`team([TEAM_ID])`** matches any member of a specific team. All team members share this role regardless of what role they have within the team. + +- **`team([TEAM_ID], [ROLE])`** matches only members of a team who have been assigned a specific role within that team. For example, `Role.team("teamABC", "admin")` only matches team members with the "admin" role. This is the basis for fine-grained team-based access control. + +- **`member([MEMBERSHIP_ID])`** matches a specific team membership. This is more precise than matching by team and role, but also more fragile since membership IDs change if a user leaves and rejoins a team. + +- **`label([LABEL_ID])`** matches any user who has been assigned a specific label. Labels are arbitrary strings you assign to users from your server-side code. They are a flexible way to grant permissions based on user columns without creating a new team for each column. + +# Where permissions are set + +Permissions can be set at multiple levels in Appwrite's data hierarchy. + +- **Databases:** You can set permissions on a table (controlling who can create rows and who can list them) and on individual rows (controlling who can read, update, or delete that specific row). + +- **Storage:** You can set permissions on a bucket (controlling who can upload files) and on individual files. + +Table-level and bucket-level permissions act as gates. If a user does not have create permission on a table, they cannot create rows in it even if they somehow construct a valid request. Row-level and file-level permissions control access to specific resources after they exist. + +# Default permission behavior + +The defaults differ between Server SDK and Client SDK usage, and this is a common source of confusion. + +- **Server SDK (API key):** When you create a resource using a Server SDK with an API key and do not set explicit permissions, nobody has access to that resource except via the same server-side API key. No client can read or modify it. This is intentional: server-created resources are locked down by default. + +- **Client SDK (user session):** When a logged-in user creates a resource using a Client SDK and does not set explicit permissions, Appwrite automatically grants read, update, and delete to that specific user. The creator retains control over what they created. + +**Server SDK bypasses permissions.** Any request made with a valid API key bypasses row-level and file-level permissions entirely. The API key scope (what services and actions it can access) is the only constraint. This means server-side code has full access to data, which is appropriate for backend operations but means you should never expose API keys to clients. + +# Common patterns + +## Private user data + +To create a row that only the owner can access: + +```js +import { TablesDB, Permission, Role } from "appwrite"; + +const tablesDB = new TablesDB(client); + +await tablesDB.createRow({ + databaseId: "[DATABASE_ID]", + tableId: "[TABLE_ID]", + rowId: ID.unique(), + data: { content: "private note" }, + permissions: [ + Permission.read(Role.user(userId)), + Permission.update(Role.user(userId)), + Permission.delete(Role.user(userId)), + ] +}); +``` + +Only the user with that ID can read, update, or delete this row. + +## Team-shared resources + +To create a row readable by all team members but only editable by team admins: + +```js +[ + Permission.read(Role.team("teamABC")), + Permission.update(Role.team("teamABC", "admin")), + Permission.delete(Role.team("teamABC", "admin")), +] +``` + +Every team member can read. Only admins can modify or delete. + +## Public read, restricted write + +A common pattern for content-heavy apps: anyone can read, but only authenticated and verified users can create: + +```js +// On the table +[ + Permission.read(Role.any()), + Permission.create(Role.users("verified")), +] +``` + +Rows in this table are publicly readable. Only verified users can add new ones. Row-level update and delete can be further restricted to the creator or to an admin team. + +## Label-based access + +Labels work well when you need user-level flags without creating dedicated teams. For example, granting early access to beta features: + +```js +// Assigned server-side when a user joins the beta +await users.updateLabels({ userId, labels: ["beta"] }); + +// Permission on a beta-only resource +[ + Permission.read(Role.label("beta")), +] +``` + +Any user with the "beta" label can read this resource. Removing the label from a user removes their access immediately. + +# Pitfalls to avoid + +- **Forgetting that write does not include read.** If you grant `Permission.write(Role.user(userId))` and forget `Permission.read(Role.user(userId))`, the user can create and modify resources but cannot fetch them. Always check both dimensions. + +- **Using `any()` on mutable resources unintentionally.** `Role.any()` grants access to unauthenticated users. If you use it on a table with create permission, anonymous users can insert rows. This is sometimes intentional (public form submissions) but often is not. + +- **Server SDK creating resources with no permissions.** If your backend creates rows that clients need to read, you must explicitly set permissions at creation time. The locked-down default means clients will see empty results or 401 errors until you add explicit permissions. + +- **Over-relying on table-level permissions.** Table-level create permission controls who can add rows. But read, update, and delete on existing rows are controlled at the row level. Setting read on the table does not automatically make all rows in it readable. + +- **Not using team roles for collaborative apps.** A common early mistake is giving all team members full write access via `Role.team("teamABC")`. When you eventually need to differentiate admins from regular members, you have to update permissions on every existing row. Model team roles from the start. + +# Build with Appwrite permissions + +Appwrite's permission system rewards explicit, intentional design. Decide who needs what access before you start creating resources, set it at creation time, and use teams and labels to express group-level access cleanly. + +- [Appwrite Permissions docs](/docs/advanced/platform/permissions) +- [Appwrite Teams docs](/docs/products/auth/teams) +- [Appwrite Databases docs](/docs/products/databases) +- [Sign up for Appwrite Cloud](https://cloud.appwrite.io) diff --git a/src/routes/blog/post/appwrite-query-api/+page.markdoc b/src/routes/blog/post/appwrite-query-api/+page.markdoc new file mode 100644 index 0000000000..00cbcbac3d --- /dev/null +++ b/src/routes/blog/post/appwrite-query-api/+page.markdoc @@ -0,0 +1,179 @@ +--- +layout: post +title: "Appwrite's Query API: filtering, sorting, and pagination" +description: Learn how to use Appwrite's Query API to filter, sort, and paginate database results efficiently, including offset and cursor pagination. +date: 2026-03-25 +cover: /images/blog/appwrite-query-api/cover.png +timeToRead: 5 +author: aditya-oberai +category: product, tutorial +featured: false +unlisted: true +--- + +Every app that reads data from a database eventually needs to answer the same three questions: which rows do I want, in what order, and how many at a time. Appwrite's Query API is the answer to all three. + +The [Query class](/docs/products/databases/queries) gives you a set of methods you pass as an array to any `listRows` (or equivalent) call. Each method generates a filter, sort directive, or pagination instruction. Multiple queries are combined with AND logic, so every condition must match. If you need OR logic, pass multiple values to the same method instead. + +# Filtering rows + +The most common filters are equality checks. `Query.equal('status', ['active'])` returns only rows where `status` is `active`. To match several values at once, pass them all in the array: `Query.equal('status', ['active', 'pending'])`. That single call behaves like an OR across those values, while still being ANDed with any other queries in your array. + +Beyond equality, you have: + +- `Query.notEqual('status', ['banned'])` — excludes specific values +- `Query.greaterThan('year', 1999)` — numeric or date comparisons +- `Query.lessThan('price', 50)` +- `Query.greaterThanEqual('score', 100)` +- `Query.lessThanEqual('age', 65)` +- `Query.contains('tags', ['javascript'])` — checks if an array column contains a value +- `Query.search('body', 'open source')` — full-text search (requires a fulltext index on the column) +- `Query.isNull('deletedAt')` / `Query.isNotNull('deletedAt')` +- `Query.startsWith('username', 'admin')` / `Query.endsWith('email', '.edu')` +- `Query.between('price', 10, 50)` — inclusive range check + +A practical example combining several filters: + +```js +import { Client, TablesDB, Query } from "appwrite"; + +const client = new Client() + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject(""); + +const tablesDB = new TablesDB(client); + +const result = await tablesDB.listRows({ + databaseId: "", + tableId: "", + queries: [ + Query.equal("status", ["published"]), + Query.greaterThan("year", 1999), + Query.contains("genres", ["sci-fi"]), + ], +}); +``` + +All three conditions must be true for a row to appear in the result. + +# Sorting results + +Two methods control sort order: `Query.orderAsc('fieldName')` and `Query.orderDesc('fieldName')`. You can chain them to sort by multiple fields: + +```js +const result = await tablesDB.listRows({ + databaseId: "", + tableId: "", + queries: [ + Query.equal("status", ["published"]), + Query.orderDesc("createdAt"), + Query.orderAsc("title"), + ], +}); +``` + +This returns published rows sorted newest-first, with ties broken alphabetically by title. Sorting on a column without an index will work for small datasets but becomes slow as the table grows. Adding an index on columns you frequently sort by is covered in the [Appwrite Indexes guide](/docs/products/databases). + +# Pagination: offset vs cursor + +Appwrite supports two pagination strategies. Choose based on your use case. + +## Offset pagination + +Offset pagination uses `Query.limit(n)` and `Query.offset(n)` together: + +```js +const page = 3; +const perPage = 25; + +const result = await tablesDB.listRows({ + databaseId: "", + tableId: "", + queries: [ + Query.limit(perPage), + Query.offset(perPage * (page - 1)), + ], +}); +``` + +The default limit when you omit `Query.limit` is 25. The maximum is 5000. + +Offset pagination is easy to reason about and supports jumping directly to any page. The downside: for large datasets, a high offset forces the database to scan and discard many rows before returning results. Page 1 is fast; page 5000 is not. It also has consistency issues if rows are inserted or deleted between requests, which can cause items to appear twice or be skipped. + +Use offset pagination when: +- Your dataset is small to medium (under a few thousand rows after filtering) +- You need numbered pages or a "go to page N" feature +- Your data changes infrequently + +## Cursor pagination + +Cursor pagination uses `Query.cursorAfter(rowId)` or `Query.cursorBefore(rowId)` with a `Query.limit`: + +```js +// First page +const firstPage = await tablesDB.listRows({ + databaseId: "", + tableId: "", + queries: [Query.limit(25)], +}); + +// Get the last row ID from the first page +const lastId = firstPage.rows[firstPage.rows.length - 1].$id; + +// Next page +const secondPage = await tablesDB.listRows({ + databaseId: "", + tableId: "", + queries: [ + Query.limit(25), + Query.cursorAfter(lastId), + ], +}); +``` + +For going backwards, pass the first row ID of the current page to `Query.cursorBefore`. + +Cursor pagination is significantly more efficient for large datasets because the database uses the cursor row as an anchor rather than scanning from the beginning. Performance stays consistent regardless of how deep into the dataset you are. The tradeoff is that you cannot jump to an arbitrary page and must navigate sequentially. + +Use cursor pagination when: +- Your dataset is large +- You are building an infinite scroll or "next page / previous page" UI +- You want consistent performance across all pages + +## Mixing limit with filters and sort + +`Query.limit` and pagination queries work alongside filter and sort queries. All queries in the same array apply together: + +```js +const result = await tablesDB.listRows({ + databaseId: "", + tableId: "", + queries: [ + Query.equal("status", ["published"]), + Query.orderDesc("createdAt"), + Query.limit(10), + Query.cursorAfter(lastRowId), + ], +}); +``` + +# Performance tips + +A few practical rules when working with the Query API: + +- Add indexes on columns used in `Query.equal`, `Query.orderAsc`, and `Query.orderDesc`. Without an index, Appwrite performs a full table scan for every request. +- `Query.search` requires a fulltext index on the column. Calling it without one returns an error. +- Prefer cursor pagination for any list that could grow beyond a few hundred rows. +- Avoid combining a very high `Query.offset` with a wide `Query.limit`. This is the fastest way to make your database slow. +- Be selective with `Query.between` on unindexed columns. Index the column first if you use it frequently. + +# Start building with Appwrite Databases + +The Query API covers the full range of what you need for data retrieval: precise filtering, flexible sorting, and two pagination strategies suited to different scales. The same `Query` class works across all Appwrite Database APIs. + +To go further: + +- [Appwrite Databases queries reference](/docs/products/databases/queries) +- [Appwrite Databases pagination guide](/docs/products/databases/pagination) +- [Appwrite Databases overview](/docs/products/databases) +- [Start building on Appwrite Cloud](https://cloud.appwrite.io) diff --git a/src/routes/blog/post/appwrite-realtime/+page.markdoc b/src/routes/blog/post/appwrite-realtime/+page.markdoc new file mode 100644 index 0000000000..ea766b5c73 --- /dev/null +++ b/src/routes/blog/post/appwrite-realtime/+page.markdoc @@ -0,0 +1,97 @@ +--- +layout: post +title: "How Appwrite Realtime works (and when to use it)" +description: Learn how Appwrite Realtime uses WebSockets to push live updates to clients, what channels are available, and when polling is a better fit. +date: 2026-03-23 +cover: /images/blog/appwrite-realtime/cover.png +timeToRead: 4 +author: aditya-oberai +category: product, tutorial +featured: false +unlisted: true +--- + +Most apps start with a simple request-response model. A user submits a form, the server responds, the page updates. That model works fine until users need to see changes made by someone else, in real time, without refreshing the page. At that point you have a choice: poll the server on a timer, or open a persistent connection and let the server push updates. + +[Appwrite Realtime](/docs/apis/realtime) takes the second approach. It uses WebSocket connections to stream events from your Appwrite backend directly to subscribed clients, with latency measured in milliseconds rather than however often you remember to set a polling interval. + +# How Appwrite Realtime works + +When a client subscribes to a Realtime channel, Appwrite opens a single WebSocket connection for that client. All subscribed channel updates travel over that connection. When data changes in your Appwrite project (a row is updated, a file is uploaded, a function execution completes), Appwrite emits an event and pushes it to every client subscribed to the relevant channel. + +Subscriptions look like this: + +```js +import { Client, Realtime } from "appwrite"; + +const client = new Client() + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject(""); + +const realtime = new Realtime(client); + +const unsubscribe = realtime.subscribe("tablesdb.[DATABASE_ID].tables.[TABLE_ID].rows", (response) => { + console.log(response.events, response.payload); +}); + +// To stop listening +unsubscribe(); +``` + +The callback fires every time an event on the subscribed channel reaches the client. The `response` object includes the event type (such as `tablesdb.*.rows.*.create`) and the full payload of the changed resource. + +Permissions are enforced at the subscription level. A user only receives events for resources they have permission to read. If a row update is pushed but the subscribed user does not have read access to that row, they do not receive the event. This means Realtime does not require you to write extra filtering logic on the client side. + +# Channels you can subscribe to + +Appwrite Realtime channels map directly to its services: + +- **Databases:** row-level changes across any table +- **Storage:** file creation, updates, and deletions within buckets +- **Functions:** execution status updates +- **Teams and memberships:** membership changes +- **Account:** changes to the authenticated user's own account + +Channels follow a hierarchical pattern. You can subscribe broadly (all rows in a database) or narrowly (a single row by ID). Subscribing to a broader channel means more events but also more filtering work on the client. + +# When to use Realtime + +- **Live chat and messaging.** Chat is the canonical Realtime use case. Each message is a new row in a table. Clients subscribe to that table and receive new messages as they are created, without any polling. + +- **Collaborative editing and presence.** When multiple users edit the same resource, Realtime lets each client see changes from others as they happen. Presence indicators (who is online, who is typing) work the same way: write a presence row per user, subscribe to the presence table, and render the current state on every update. + +- **Live dashboards and analytics.** Metrics that change frequently (order counts, active sessions, transaction totals) are a good fit for Realtime. Each update to the underlying row triggers an event that the dashboard subscribes to, keeping numbers current without polling. + +- **Notifications.** Instead of polling a notifications table, clients subscribe to it. New notification rows appear in the client's UI immediately after they are created on the server. + +- **Multiplayer game state.** Lightweight game state (player positions, scores, turn changes) can be synced via Realtime. This works well for turn-based games and simple real-time games where WebSocket latency is acceptable. + +# When NOT to use Realtime + +- **Server SDKs.** Appwrite Realtime is not available for Server SDKs using API keys. It is designed for client-side use. If you need to react to data changes on the server, use Appwrite Functions triggered by events instead. Those functions receive the same event payloads that Realtime delivers to clients. + +- **Infrequent updates where polling is simpler.** If your data changes once every few minutes and you have only a handful of users, a 30-second polling interval is easier to implement, easier to debug, and carries no persistent connection overhead. Realtime shines when updates are frequent or the delay from polling is genuinely noticeable to users. + +- **High-frequency writes where the client cannot keep up.** If a channel emits hundreds of events per second, the client may struggle to process them all. In those cases, consider batching writes on the server side or aggregating data before exposing it to clients. + +- **Offline-first apps.** Realtime requires an active connection. If your app needs to work reliably offline and sync when reconnected, you need a dedicated offline sync strategy beyond what Realtime provides. + +# Connection behavior and limits + +Each call to `realtime.subscribe()` with a new channel modifies the WebSocket connection. Appwrite efficiently manages the underlying connection, but be aware that subscribing to many channels simultaneously keeps a persistent connection open. On mobile, this has battery and data implications if the subscription runs in the background without any updates. + +The `unsubscribe()` function returned by `realtime.subscribe()` cleanly removes the listener and, if there are no remaining subscriptions, closes the WebSocket. Always call it when a component unmounts or a view navigates away to avoid memory leaks and unnecessary network usage. + +# Realtime and Appwrite Events + +Realtime events map directly to Appwrite's broader event system. The same events that trigger Appwrite Functions are the ones delivered over Realtime channels. This means you can use the events documentation to understand exactly which event strings correspond to which actions, and subscribe or trigger accordingly. + +This symmetry is useful when designing event-driven features. A file upload triggers `storage.*.files.*.create`. You can subscribe to that event in a client via Realtime, trigger a Function via the event system, or both simultaneously. + +# Build live features with Appwrite Realtime + +Realtime removes the need to write and maintain polling loops, manage WebSocket servers, or handle reconnection logic yourself. The subscription API is a few lines of code, permissions are automatically enforced, and the connection infrastructure scales with Appwrite Cloud. + +- [Appwrite Realtime docs](/docs/apis/realtime) +- [Appwrite Events reference](/docs/advanced/platform/events) +- [Sign up for Appwrite Cloud](https://cloud.appwrite.io) diff --git a/src/routes/blog/post/appwrite-server-sdk-vs-client-sdk/+page.markdoc b/src/routes/blog/post/appwrite-server-sdk-vs-client-sdk/+page.markdoc new file mode 100644 index 0000000000..98fc6847d3 --- /dev/null +++ b/src/routes/blog/post/appwrite-server-sdk-vs-client-sdk/+page.markdoc @@ -0,0 +1,203 @@ +--- +layout: post +title: "How to use Appwrite's Server SDK vs Client SDK" +description: Understand the difference between Appwrite's Client and Server SDKs, when to use each one, and how to avoid common security mistakes in your app. +date: 2026-03-26 +cover: /images/blog/appwrite-server-sdk-vs-client-sdk/cover.png +timeToRead: 5 +author: aditya-oberai +category: product, tutorial +featured: false +unlisted: true +--- + +One of the most common points of confusion when starting with Appwrite is which SDK to use. Appwrite ships two distinct sets of [SDKs](/docs/sdks): one for client-side code running in browsers and mobile apps, and one for server-side code. They share a similar structure, but they authenticate differently and have different levels of access. Using the wrong one in the wrong context is a real security problem. + +This post breaks down how each SDK authenticates, what it can access, and which one belongs in each part of your architecture. + +# The core difference: sessions vs API keys + +The Client SDK authenticates on behalf of a user using a session. When a user logs in through your app, Appwrite creates a session for that user. All subsequent requests made with the Client SDK carry that session and are subject to the permissions you have set on your tables, buckets, and functions. The Client SDK can only do what the currently logged-in user is allowed to do. + +The Server SDK authenticates using an API key. API keys are created in the Appwrite Console and scoped to specific capabilities. A request made with a Server SDK and a valid API key can bypass the permissions system entirely, depending on the key's scope. This is intentional: server-side code often needs to perform operations across users, run scheduled jobs, or do administrative work that no individual user session should be able to authorize. + +This distinction matters a great deal. An API key embedded in a mobile app or browser JavaScript is a serious security risk because anyone who extracts it gains the access level of that key. API keys belong on the server, never in client code. + +# Client SDKs: for browsers and mobile + +The available Client SDKs are: + +- Web (JavaScript/TypeScript) +- Flutter +- React Native (beta) +- Apple (Swift) +- Android (Kotlin/Java) + +These SDKs are built around the `Account` service. Users sign up, log in, manage their sessions, and interact with resources they own. Typical client-side operations include: + +- Creating and managing user accounts +- Reading and writing rows the user owns or has been granted access to +- Uploading and downloading files from buckets with appropriate permissions +- Subscribing to real-time events on resources the user can access + +A basic client-side login and data fetch in JavaScript looks like this: + +```js +import { Client, Account, TablesDB, Query } from "appwrite"; + +const client = new Client() + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject(""); + +const account = new Account(client); +const tablesDB = new TablesDB(client); + +// Log in +await account.createEmailPasswordSession("user@example.com", "password"); + +// Read data — scoped to what this user can access +const docs = await tablesDB.listRows({ + databaseId: "", + tableId: "", + queries: [Query.equal("userId", [""])], +}); +``` + +The session is maintained automatically (as a cookie in browsers, or in secure storage on mobile). There is nothing you need to pass manually after login. + +# Server SDKs: for backends and functions + +The available Server SDKs are: Node.js, Python, Dart, PHP, Ruby, .NET, Go, Swift, Kotlin + +Server SDKs are initialized with an API key rather than a user session. The key is created in the Appwrite Console under your project settings, and you assign it only the scopes it needs. For example, a function that sends emails only needs `messaging.write`, not `databases.write`. + +A Node.js server-side example: + +```js +import { Client, TablesDB, Users, Query } from "node-appwrite"; + +const client = new Client() + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + .setKey(""); + +const tablesDB = new TablesDB(client); +const users = new Users(client); + +// List all users — not possible from the Client SDK +const allUsers = await users.list(); + +// Write a row on behalf of the system +await tablesDB.createRow({ + databaseId: "", + tableId: "", + rowId: "unique()", + data: { type: "system_event", payload: "job_completed" }, +}); +``` + +This code has no user session. It operates at the API key level, which means it can access any resource within the key's scopes regardless of row permissions. + +## Inside Appwrite Functions + +Appwrite Functions run on the server, so they use the Server SDK. When a function is triggered (by an HTTP request, an event, or a schedule), Appwrite injects a context object that includes a pre-authenticated client with elevated privileges. You do not need to manage an API key manually in most cases: + +```js +export default async ({ req, res, log, error }) => { + const client = new Client() + .setEndpoint(process.env.APPWRITE_FUNCTION_API_ENDPOINT) + .setProject(process.env.APPWRITE_FUNCTION_PROJECT_ID) + .setKey(req.headers["x-appwrite-key"]); + + const tablesDB = new TablesDB(client); + + // Perform elevated operations + const result = await tablesDB.listRows({ + databaseId: "", + tableId: "", + }); + + return res.json(result); +}; +``` + +Functions are the right place for any operation that requires elevated access but should not be exposed directly to users: processing payments, sending notifications to all users, syncing data with external services, or running periodic cleanup jobs. + +# Server SDKs with sessions for SSR + +Server-side rendering introduces a third authentication mode that does not fit cleanly into either category above. Frameworks like Next.js, SvelteKit, Nuxt, and Astro render pages on the server, which means you cannot use the Client SDK (it assumes a browser context). But you also cannot use an API key, because you want requests to respect the permissions of the specific user loading the page. Using an API key here would bypass the permissions system entirely, exposing every user's data to every server-rendered request. + +The solution is to use the Server SDK but authenticate it with the user's session token instead of an API key. Appwrite stores session tokens in a cookie named `a_session_`. Your server reads that cookie from the incoming request and passes it to the client using `setSession()`. The result is a server-side client that acts exactly as that user, with no elevated access. + +In practice, an SSR setup requires two separate clients: + +**Admin client:** initialized with an API key scoped to `sessions.write`. Used only to create and manage sessions (login, logout). This client bypasses rate limits, which matters when all requests originate from the same server IP. + +**Session client:** initialized with the user's session token from the request cookie. Used for all data fetching on behalf of the user. This client respects permissions the same way the browser Client SDK would. + +```js +import { Client, Account } from "node-appwrite"; + +// Admin client: used to create sessions (e.g. on login) +const adminClient = new Client() + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + .setKey(""); // sessions.write scope + +// Session client: used to fetch data as the current user +const sessionClient = new Client() + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject(""); + +const session = req.cookies["a_session_"]; +if (session) { + sessionClient.setSession(session); +} + +// Fetch data as the user — permissions are enforced +const account = new Account(sessionClient); +const user = await account.get(); +``` + +A few important rules for this pattern: + +- **Never reuse the session client across requests.** Create a new client per request. Sharing a client between requests will mix up user sessions. +- **Never set a session on the admin client.** Keep the two clients entirely separate. +- **Set the cookie with `httpOnly`, `secure`, and `sameSite: strict`.** This prevents JavaScript from reading the session token and protects against CSRF. +- **Use a custom domain for your Appwrite endpoint** if you want the session cookie to also work with client-side Appwrite calls on the same domain. See [custom domains](/docs/advanced/platform/custom-domains). + +When a user logs in through your server, create the session with the admin client and store the `session.secret` value in the cookie. On subsequent requests, read the cookie and pass it to a fresh session client. That client will behave identically to a browser-based Client SDK authenticated as that user. + +# Common patterns + +**Pattern 1: User-owned data.** The user creates and reads their own rows. Use the Client SDK. Set table permissions so users can only read and write their own rows using the `user()` role. + +**Pattern 2: Admin operations.** A dashboard that lists all users, or a job that resets expired sessions. Use the Server SDK with an API key scoped to what you need. + +**Pattern 3: Triggered server logic.** A user submits a form, and you need to send a confirmation email and write a log entry with admin-level access. Use the Client SDK for the user-facing submission, trigger an Appwrite Function from the event, and use the Server SDK inside that function for the privileged steps. + +**Pattern 4: Scheduled jobs.** A cron-triggered function that archives old records. Server SDK only, no user session involved. + +# Security considerations + +A few rules that save you from common mistakes: + +- Never put an API key in a mobile app or browser-side JavaScript bundle. It will be extracted and abused. +- Scope your API keys narrowly. If a key only needs to read from one table, do not give it `databases.write` or `users.read`. +- Use the Client SDK for anything user-facing. The permissions system is your first line of defense for user data. +- Prefer using the injected key inside Appwrite Functions over hardcoding API keys in function environment variables when possible. +- The Client SDK cannot use the Realtime API when authenticated with an API key. Real-time subscriptions are session-based only. + +# Choosing the right SDK + +Both SDKs share the same `ID`, `Query`, `Permission`, and `Role` utility classes, so the programming model feels consistent. The key question is always: who is making this request, and what should they be allowed to do? + +If the request comes from a user interacting with your app, use the Client SDK and let the permissions system enforce access. If the request comes from your server, a background job, or an Appwrite Function, use the Server SDK with a properly scoped API key. + +Getting this right from the start avoids a whole class of access control bugs. + +- [Appwrite SDKs overview](/docs/sdks) +- [API keys and scopes](/docs/advanced/platform/api-keys) +- [SSR authentication with Appwrite](/docs/products/auth/server-side-rendering) +- [Appwrite Functions](/docs/products/functions) +- [Start building on Appwrite Cloud](https://cloud.appwrite.io) diff --git a/src/routes/blog/post/appwrite-storage-file-manager/+page.markdoc b/src/routes/blog/post/appwrite-storage-file-manager/+page.markdoc new file mode 100644 index 0000000000..ddfcb865d8 --- /dev/null +++ b/src/routes/blog/post/appwrite-storage-file-manager/+page.markdoc @@ -0,0 +1,221 @@ +--- +layout: post +title: How to build a file manager with Appwrite Storage +description: Learn how to create buckets, upload and download files, set permissions, and build a complete file manager UI using Appwrite Storage. +date: 2026-03-24 +cover: /images/blog/appwrite-storage-file-manager/cover.png +timeToRead: 5 +author: aditya-oberai +category: product, tutorial +featured: false +unlisted: true +--- + +File management is one of those features that sounds simple until you're deep in the weeds: S3 bucket policies, pre-signed URLs, CORS headers, permission checks. [Appwrite Storage](/docs/products/storage) is designed to cut through that complexity with a clean API and bucket-based organization that maps naturally to most app architectures. + +This post walks through creating buckets, uploading and downloading files, listing files in a bucket, and setting permissions, then ties it all together with a simple file manager UI flow. + +# What Appwrite Storage gives you + +Appwrite Storage handles file uploads, downloads, deletions, and listings through a REST API (and SDK wrappers for all major platforms). Files live in buckets, which are isolated containers with their own permission settings, size limits, and allowed MIME types. + +Storage is intentionally separate from Appwrite Databases. Databases store structured data: user profiles, product records, settings. Storage stores files: images, PDFs, videos, rows. Both can be used together, and it's common to store a file ID in a database row to link them. + +# Creating a bucket + +Before you can store anything, you need a bucket. You can create one in the Appwrite Console or via the Server SDK. + +In the Console, go to **Storage**, click **Create bucket**, give it a name, and configure the settings: maximum file size, allowed file extensions, and whether files are encrypted at rest. + +Via the Node.js Server SDK: + +```js +import { Client, Storage, ID } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const storage = new Storage(client); + +const bucket = await storage.createBucket( + ID.unique(), // bucketId + 'User Uploads', // name + ['any'], // permissions (covered below) + true, // fileSecurity + true, // enabled + 10000000, // maximumFileSize (10 MB) + ['image/jpeg', 'image/png', 'application/pdf'] // allowedFileExtensions (MIME types or extensions, e.g. 'jpg') +); +``` + +The `fileSecurity` flag is important. When set to `true`, each file can have its own permissions in addition to bucket-level permissions. When `false`, only bucket-level permissions apply. For a multi-user file manager, you'll want `fileSecurity: true`. + +# Uploading files + +From the browser, use the Client SDK: + +```js +import { Client, Storage, ID } from 'appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(''); + +const storage = new Storage(client); + +async function uploadFile(bucketId, file) { + const result = await storage.createFile({ + bucketId, + fileId: ID.unique(), + file, + // Permissions for this specific file + permissions: [ + 'read("user:")', + 'update("user:")', + 'delete("user:")' + ] + }); + return result; +} +``` + +The `file` argument is a standard browser `File` object from an `` element. The SDK handles chunked uploads automatically for large files, so you don't need to manage that yourself. + +For a file manager UI, wire this up to a file input or drag-and-drop zone: + +```js +const fileInput = document.getElementById('file-input'); + +fileInput.addEventListener('change', async (e) => { + const file = e.target.files[0]; + if (!file) return; + + const uploaded = await uploadFile('', file); + console.log('Uploaded:', uploaded.$id); + await refreshFileList(); +}); +``` + +# Listing files + +To display uploaded files, use `listFiles`: + +```js +async function listFiles(bucketId) { + const files = await storage.listFiles({ bucketId }); + return files.files; // array of file objects +} +``` + +Each file object contains metadata: `$id`, `name`, `mimeType`, `sizeOriginal`, `$createdAt`, and `$updatedAt`. You can use this to render a table or grid view without downloading the actual file content. + +```js +async function renderFileList(bucketId) { + const files = await listFiles(bucketId); + const list = document.getElementById('file-list'); + + list.innerHTML = files.map(file => ` +
+ ${file.name} + ${(file.sizeOriginal / 1024).toFixed(1)} KB + ${new Date(file.$createdAt).toLocaleDateString()} + + +
+ `).join(''); +} +``` + +# Downloading files + +To download a file, get the file view URL and trigger a browser download: + +```js +async function downloadFile(bucketId, fileId, fileName) { + const url = storage.getFileDownload({ bucketId, fileId }); + const a = document.createElement('a'); + a.href = url; + a.download = fileName; + a.click(); +} +``` + +`getFileDownload` returns a URL that forces a download. If you want to display images inline instead, use `getFileView`, which serves the file with appropriate content headers. + +For image previews, Appwrite also offers built-in transformations. You can request a resized thumbnail without any third-party service: + +```js +const thumbnailUrl = storage.getFilePreview({ + bucketId, + fileId, + width: 200, + height: 200, + gravity: 'center', + quality: 80 +}); +``` + +# Setting permissions + +Permissions in Appwrite follow a consistent pattern across all resources. For storage, you set permissions at two levels: the bucket and (when `fileSecurity` is enabled) the individual file. + +Permission strings take the form `action("role")`. Common patterns: + +```js +// Only authenticated users can read +'read("users")' + +// Only a specific user can read, update, and delete +'read("user:")' +'update("user:")' +'delete("user:")' + +// A team with a specific role +'read("team:/editor")' +'update("team:/editor")' + +// Public read access (anyone, including guests) +'read("any")' +``` + +For a typical file manager where each user owns their files: + +- Set the bucket to allow authenticated users to create files: `'create("users")'` +- Set each file's permissions to the uploading user only: `'read("user:")'`, `'update("user:")'`, `'delete("user:")'` + +This way, users can only see and manage their own files, even though all files live in the same bucket. + +# Deleting files + +```js +async function deleteFile(bucketId, fileId) { + await storage.deleteFile({ bucketId, fileId }); + await renderFileList(bucketId); +} +``` + +# Putting it together: a minimal file manager + +Here's a sketch of the full flow: + +1. On page load, call `listFiles` and render the file list. +2. A file input or drop zone calls `uploadFile` when the user selects a file, using `Permission.write(Role.user(currentUser.$id))` for ownership. +3. Each row in the list has download and delete buttons wired to `getFileDownload` and `deleteFile`. +4. For image files, show a thumbnail using `getFilePreview`. + +The result is a functional, permissioned file manager backed entirely by Appwrite, no custom file storage server required. + +# Start building with Appwrite Storage + +Appwrite Storage handles the infrastructure so you can focus on building the experience. Buckets, permissions, image transformations, and chunked uploads are all included out of the box. + +- [Appwrite Storage docs](/docs/products/storage) +- [Storage quick start](/docs/products/storage/quick-start) +- [Permissions reference](/docs/advanced/platform/permissions) +- [Start building on Appwrite Cloud](https://cloud.appwrite.io) diff --git a/src/routes/blog/post/appwrite-teams-roles/+page.markdoc b/src/routes/blog/post/appwrite-teams-roles/+page.markdoc new file mode 100644 index 0000000000..a53e637064 --- /dev/null +++ b/src/routes/blog/post/appwrite-teams-roles/+page.markdoc @@ -0,0 +1,165 @@ +--- +layout: post +title: "Appwrite Teams and Roles: managing multi-tenant access" +description: Learn how Appwrite Teams work, how to create and manage teams with roles, and how to use team-based permissions for multi-tenant SaaS apps. +date: 2026-03-24 +cover: /images/blog/appwrite-teams-roles/cover.png +timeToRead: 5 +author: aditya-oberai +category: product, tutorial +featured: false +unlisted: true +--- + +Access control in multi-tenant apps is one of the first things that gets messy. You need users to belong to organizations, those organizations to have different members with different roles, and data to be strictly isolated between tenants. Rolling this yourself means custom tables, middleware, and a lot of edge cases. + +Appwrite Teams provides a first-class model for this. A team is a group of users with named roles, and you can use team membership to gate access to any Appwrite resource: databases, storage, functions. This post covers how teams work, how to create and manage them, and the patterns that work best for multi-tenant apps. + +# How teams work + +A team is a table of users with roles. When you create a team, the creating user automatically becomes a member with the `owner` role. Only members with the `owner` role can invite or remove other members. + +Teams have two parts: + +- **Membership**: who belongs to the team +- **Roles**: what role each member has within the team + +Roles are arbitrary strings you define. Common examples: `owner`, `admin`, `editor`, `viewer`. You can define whatever roles make sense for your app's permission model. + +When you assign permissions to a resource, you reference the team ID and optionally a specific role: + +- `Role.team(TEAM_ID)` grants access to all team members +- `Role.team(TEAM_ID, 'editor')` grants access only to members with the `editor` role + +This lets you build fine-grained access control without managing any custom tables. + +# Creating a team + +Use the Teams API from the Server SDK (with an API key) or the Client SDK (from a logged-in user's session): + +```js +import { Client, Teams, ID } from 'appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(''); + +const teams = new Teams(client); + +// Create a team, defining the available roles +const team = await teams.create({ + teamId: ID.unique(), + name: 'Acme Corp', + roles: ['owner', 'admin', 'editor', 'viewer'] +}); + +console.log(team.$id); // save this as your tenant ID +``` + +The `roles` array defines which roles are valid for this team. A user can only be assigned a role that exists in this list. + +# Inviting members + +The owner invites users by email. Appwrite sends an invitation email, and the invited user must accept before they become an active member: + +```js +const membership = await teams.createMembership({ + teamId: team.$id, + roles: ['editor'], + email: 'jane@example.com', + url: 'https://yourapp.com/accept-invite' +}); +``` + +Once accepted, `membership.confirm` becomes `true` and the user gains the permissions associated with their roles. + +To list current members: + +```js +const memberships = await teams.listMemberships({ teamId: team.$id }); +memberships.memberships.forEach(m => { + console.log(m.userName, m.roles, m.confirm); +}); +``` + +To remove a member: + +```js +await teams.deleteMembership({ teamId: team.$id, membershipId }); +``` + +# Using teams for permissions + +Once you have a team, you use it in [permission](/docs/advanced/platform/permissions) strings when creating or updating any Appwrite resource. + +Here's how to create a database row that only team members can read, but only editors and admins can update: + +```js +import { TablesDB, ID, Permission, Role } from 'appwrite'; + +const tablesDB = new TablesDB(client); + +await tablesDB.createRow({ + databaseId: '', + tableId: '', + rowId: ID.unique(), + data: { title: 'Project Plan', content: '...' }, + permissions: [ + Permission.read(Role.team(team.$id)), // all members can read + Permission.update(Role.team(team.$id, 'editor')), // editors can update + Permission.update(Role.team(team.$id, 'admin')), // admins can update + Permission.delete(Role.team(team.$id, 'admin')), // only admins can delete + ] +}); +``` + +The same pattern applies to storage files. Create a bucket with `fileSecurity: true`, then assign team-based permissions when uploading files. + +# Multi-tenancy patterns + +The cleanest way to model multi-tenancy with Appwrite Teams is one team per tenant. Each organization, workspace, or account in your app maps to a single Appwrite team. All data belonging to that tenant gets team-based permissions. + +The flow looks like this: + +1. User signs up and creates an organization. Your backend creates an Appwrite team with `teams.create`, stores the team ID alongside the organization record. +2. The creating user is automatically the team `owner`. +3. When the owner invites colleagues, call `teams.createMembership` with appropriate roles. +4. All database rows and storage files created for that organization use `Permission.read(Role.team(teamId))` and role-specific write permissions. +5. Appwrite enforces isolation automatically. A user from team A cannot read rows that only grant access to team B. + +This eliminates the need for tenant ID columns in every table or row-level security policies. The access control layer is in Appwrite, not in your SQL schema. + +# Membership privacy + +By default, team members can see the names and emails of other members when listing memberships. If your app has privacy requirements, you can configure which fields are hidden from non-owner members. + +In the Appwrite Console under your project's Auth settings, you can restrict whether `userName`, `userEmail`, and `mfa` status are visible to non-owner team members. This is useful for platforms where users shouldn't be able to enumerate other members' contact details. + +# Role escalation and management + +Only `owner` role members can invite others, change roles, or remove members. This is enforced by Appwrite, not something you need to implement yourself. + +To update an existing member's roles, delete the membership and re-create it with the new roles. There is no in-place role update endpoint, so your app should handle this as a remove-and-reinvite flow when a role change is needed. + +If your app needs an `admin` role that can also invite members, that capability isn't built into the Teams API directly. In that case, use Appwrite Functions on the server side: when an admin triggers an invite, call `teams.createMembership` using a server-side API key (which has full access), rather than relying on the client SDK. + +# Real-world use cases + +Teams maps cleanly to several common app patterns: + +- **SaaS workspaces**: Each company account is a team. Members have `owner`, `admin`, or `member` roles. Data is isolated per team with team-based permissions. + +- **Educational platforms**: Each class or cohort is a team. Instructors get `instructor` role, students get `student` role. Course materials use `Permission.read(Role.team(teamId))` so only enrolled users can access them. + +- **Collaborative tools**: Each project is a team. The creator is `owner`. Collaborators are `editor` or `viewer`. Rows in the project use role-based update and delete permissions. + +- **Agency client portals**: Each client is a team. Agency staff and client contacts are both members with different roles, sharing access to relevant project files and rows. + +# Build multi-tenant apps with Appwrite Teams + +Appwrite Teams gives you a complete multi-tenancy primitive without any custom access control logic. Create a team per tenant, assign roles to members, and use team-based permissions on your data. + +- [Appwrite Teams docs](/docs/products/auth/teams) +- [Multi-tenancy guide](/docs/products/auth/multi-tenancy) +- [Permissions reference](/docs/advanced/platform/permissions) +- [Start building on Appwrite Cloud](https://cloud.appwrite.io) diff --git a/src/routes/blog/post/appwrite-webhooks/+page.markdoc b/src/routes/blog/post/appwrite-webhooks/+page.markdoc new file mode 100644 index 0000000000..5c5a197ff2 --- /dev/null +++ b/src/routes/blog/post/appwrite-webhooks/+page.markdoc @@ -0,0 +1,192 @@ +--- +layout: post +title: "Appwrite Webhooks: triggering events the right way" +description: Learn how to configure Appwrite Webhooks, select the right events, verify payloads with HMAC-SHA1, and build reliable real-world integrations. +date: 2026-03-25 +cover: /images/blog/appwrite-webhooks/cover.png +timeToRead: 4 +author: aditya-oberai +category: product, tutorial +featured: false +unlisted: true +--- + +Webhooks are the simplest way to react to things happening in your Appwrite project without polling. A user signs up. A file gets uploaded. A database row is updated. Appwrite fires an HTTP POST to a URL you control, and your server handles it. + +The mechanics are straightforward, but there are details worth getting right: which events to subscribe to, how to verify that a request actually came from Appwrite, and how to structure your handler to handle retries gracefully. + +# Setting up a webhook + +Webhooks are configured at the project level in the Appwrite Console: + +1. Open your project and go to **Settings**. +2. Click **Webhooks** in the sidebar. +3. Click **Add Webhook**. +4. Give it a name, enter your endpoint URL, and select the events you want to subscribe to. +5. Optionally, enable **HTTP Basic Authentication** to add an extra credential layer on your endpoint. +6. Click **Create**. + +That's it. Appwrite will now send a POST request to your URL every time one of the selected events fires. + +You can also configure webhooks to send requests with a custom HTTP signature for verification, covered in the security section below. + +# Choosing events + +Appwrite's [event system](/docs/advanced/platform/events) covers everything that happens in your project. Events are grouped by resource type: + +{% accordion %} +{% accordion_item title="Authentication events" %} +{% partial file="auth-events.md" /%} +{% /accordion_item %} +{% accordion_item title="Databases events" %} +{% partial file="databases-events.md" /%} +{% /accordion_item %} +{% accordion_item title="Storage events" %} +{% partial file="storage-events.md" /%} +{% /accordion_item %} +{% accordion_item title="Functions events" %} +{% partial file="functions-events.md" /%} +{% /accordion_item %} +{% accordion_item title="Messaging events" %} +{% partial file="messaging-events.md" /%} +{% /accordion_item %} +{% /accordion %} + +The `*` wildcard matches any resource ID. You can use specific IDs instead of wildcards to subscribe only to events from a particular table, bucket, or function. + +For example, to trigger only when rows are created in a specific table: + +``` +databases..tables..rows.*.create +``` + +Keep your subscriptions specific. Subscribing to `*` (all events) on a busy project will result in a high volume of requests to your endpoint. + +# What the webhook payload looks like + +The webhook body is JSON. The payload mirrors the API response for the event type. For a row create event, you get the full row object. For a user create event, you get the user object. + +Example payload for a `users.*.create` event: + +```json +{ + "$id": "user_abc123", + "$createdAt": "2026-03-26T10:00:00.000+00:00", + "name": "Jane Smith", + "email": "jane@example.com", + "status": true, + "emailVerification": false, + "labels": [] +} +``` + +Appwrite also sends several headers with every webhook request: + +| Header | Description | +|--------|-------------| +| `X-Appwrite-Webhook-Id` | The webhook's ID in your project | +| `X-Appwrite-Webhook-Events` | Comma-separated list of matching events | +| `X-Appwrite-Webhook-Name` | The name you gave the webhook | +| `X-Appwrite-Webhook-User-Id` | ID of the user who triggered the event (if any) | +| `X-Appwrite-Webhook-Project-Id` | Your Appwrite project ID | +| `X-Appwrite-Webhook-Signature` | HMAC-SHA1 signature for verification | +| `User-Agent` | Always `Appwrite-Server` | + +# Verifying webhook signatures + +Anyone who knows your endpoint URL could send fake webhook requests. Appwrite signs every webhook payload with HMAC-SHA1 using a secret key, and you should verify this signature on every request. + +The signature is computed as: + +``` +HMAC-SHA1(webhookUrl + rawBody, signingKey) +``` + +Where `webhookUrl` is the full URL of your endpoint (including protocol and path), `rawBody` is the raw request body string, and `signingKey` is the signing key shown in the webhook's configuration in the Appwrite Console. + +Here's how to verify in Node.js: + +```js +const crypto = require('crypto'); + +function verifyWebhookSignature(req, signingKey) { + const receivedSignature = req.headers['x-appwrite-webhook-signature']; + const webhookUrl = 'https://yourapp.com/webhooks/appwrite'; // must match exactly + const rawBody = req.rawBody; // ensure you have the raw body, not parsed JSON + + const expectedSignature = crypto + .createHmac('sha1', signingKey) + .update(webhookUrl + rawBody) + .digest('base64'); + + return receivedSignature === expectedSignature; +} + +app.post('/webhooks/appwrite', express.raw({ type: 'application/json' }), (req, res) => { + const rawBody = req.body.toString('utf8'); + + if (!verifyWebhookSignature({ headers: req.headers, rawBody }, process.env.WEBHOOK_SIGNING_KEY)) { + return res.status(401).json({ error: 'Invalid signature' }); + } + + const payload = JSON.parse(rawBody); + const events = req.headers['x-appwrite-webhook-events']; + + // handle the event + handleWebhookEvent(events, payload); + + res.status(200).json({ received: true }); +}); +``` + +Two things to watch for: + +- Use the **raw body** before JSON parsing. Once parsed, the byte-for-byte representation may differ. +- The URL must match exactly, including any trailing slash. + +Always return a `200` response quickly. Appwrite will retry failed deliveries, so if your handler takes too long or returns a non-2xx status, you'll receive duplicate events. Acknowledge receipt immediately and process asynchronously if needed. + +# Real use cases + +**CDN cache invalidation**: Subscribe to `storage.buckets.*.files.*.update` and purge the CDN cache for the affected file URL when an asset is updated. + +**Slack notifications**: Subscribe to `users.*.create` and post a message to a Slack channel whenever a new user signs up. Useful for tracking growth in early-stage apps. + +```js +async function handleWebhookEvent(events, payload) { + if (events.includes('users') && events.includes('create')) { + await fetch('https://hooks.slack.com/services/...', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + text: `New user: ${payload.name} (${payload.email})` + }) + }); + } +} +``` + +**Data sync to external systems**: Subscribe to row create, update, and delete events and mirror changes to an external analytics database, a search index like Meilisearch or Algolia, or a data warehouse. + +**Automated emails**: Subscribe to `users.*.sessions.*.create` and send a "new sign-in" security notification via your email provider when a session is created from a new location. + +**Audit logging**: Subscribe broadly across databases and storage events and write every event to an append-only audit log table with the user ID from `X-Appwrite-Webhook-User-Id`. + +# Debugging webhooks + +If your endpoint isn't receiving requests, check: + +1. The webhook is enabled in the Appwrite Console (there's an active/inactive toggle). +2. Your endpoint URL is publicly reachable. Localhost won't work unless you're using a tunnel like ngrok or Cloudflare Tunnel. +3. The events you subscribed to are actually firing. Use the Appwrite Console to manually trigger an action and confirm the event matches your subscription. +4. Your server returns a 2xx response. Non-2xx responses are treated as failures. + +For local development, tools like [ngrok](https://ngrok.com) or [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) give your local server a public HTTPS URL you can paste directly into the webhook configuration. + +# Add webhooks to your Appwrite project + +Webhooks connect Appwrite events to any external system without polling. Configure them in the Console, verify signatures to ensure authenticity, and return fast responses to handle retries cleanly. + +- [Appwrite Webhooks docs](/docs/advanced/platform/webhooks) +- [Appwrite Events reference](/docs/advanced/platform/events) +- [Start building on Appwrite Cloud](https://cloud.appwrite.io) diff --git a/src/routes/docs/advanced/platform/webhooks/+page.markdoc b/src/routes/docs/advanced/platform/webhooks/+page.markdoc index a133a28a3a..e8bf8902ca 100644 --- a/src/routes/docs/advanced/platform/webhooks/+page.markdoc +++ b/src/routes/docs/advanced/platform/webhooks/+page.markdoc @@ -61,4 +61,4 @@ You can specify one or many events to subscribe to with webhooks. {% /accordion_item %} {% /accordion %} -[Learn more about events](/docs/advanced/platform/api-keys) +[Learn more about events](/docs/advanced/platform/events) diff --git a/static/images/blog/appwrite-auth-methods/cover.png b/static/images/blog/appwrite-auth-methods/cover.png new file mode 100644 index 0000000000..b7a9f445f8 Binary files /dev/null and b/static/images/blog/appwrite-auth-methods/cover.png differ diff --git a/static/images/blog/appwrite-custom-domains/cover.png b/static/images/blog/appwrite-custom-domains/cover.png new file mode 100644 index 0000000000..800dba7b69 Binary files /dev/null and b/static/images/blog/appwrite-custom-domains/cover.png differ diff --git a/static/images/blog/appwrite-functions-guide/cover.png b/static/images/blog/appwrite-functions-guide/cover.png new file mode 100644 index 0000000000..5fe8e8c002 Binary files /dev/null and b/static/images/blog/appwrite-functions-guide/cover.png differ diff --git a/static/images/blog/appwrite-indexes/cover.png b/static/images/blog/appwrite-indexes/cover.png new file mode 100644 index 0000000000..5e7f04a4c5 Binary files /dev/null and b/static/images/blog/appwrite-indexes/cover.png differ diff --git a/static/images/blog/appwrite-magic-link/cover.png b/static/images/blog/appwrite-magic-link/cover.png new file mode 100644 index 0000000000..a0a64d8e49 Binary files /dev/null and b/static/images/blog/appwrite-magic-link/cover.png differ diff --git a/static/images/blog/appwrite-messaging-push-email/cover.png b/static/images/blog/appwrite-messaging-push-email/cover.png new file mode 100644 index 0000000000..02419fffdc Binary files /dev/null and b/static/images/blog/appwrite-messaging-push-email/cover.png differ diff --git a/static/images/blog/appwrite-oauth/cover.png b/static/images/blog/appwrite-oauth/cover.png new file mode 100644 index 0000000000..4a9271bdd2 Binary files /dev/null and b/static/images/blog/appwrite-oauth/cover.png differ diff --git a/static/images/blog/appwrite-permissions/cover.png b/static/images/blog/appwrite-permissions/cover.png new file mode 100644 index 0000000000..3a1fc03a51 Binary files /dev/null and b/static/images/blog/appwrite-permissions/cover.png differ diff --git a/static/images/blog/appwrite-query-api/cover.png b/static/images/blog/appwrite-query-api/cover.png new file mode 100644 index 0000000000..93a88bb719 Binary files /dev/null and b/static/images/blog/appwrite-query-api/cover.png differ diff --git a/static/images/blog/appwrite-realtime/cover.png b/static/images/blog/appwrite-realtime/cover.png new file mode 100644 index 0000000000..cad26cc9fa Binary files /dev/null and b/static/images/blog/appwrite-realtime/cover.png differ diff --git a/static/images/blog/appwrite-server-sdk-vs-client-sdk/cover.png b/static/images/blog/appwrite-server-sdk-vs-client-sdk/cover.png new file mode 100644 index 0000000000..897341e030 Binary files /dev/null and b/static/images/blog/appwrite-server-sdk-vs-client-sdk/cover.png differ diff --git a/static/images/blog/appwrite-storage-file-manager/cover.png b/static/images/blog/appwrite-storage-file-manager/cover.png new file mode 100644 index 0000000000..829835ed82 Binary files /dev/null and b/static/images/blog/appwrite-storage-file-manager/cover.png differ diff --git a/static/images/blog/appwrite-teams-roles/cover.png b/static/images/blog/appwrite-teams-roles/cover.png new file mode 100644 index 0000000000..4790ed6ad6 Binary files /dev/null and b/static/images/blog/appwrite-teams-roles/cover.png differ diff --git a/static/images/blog/appwrite-webhooks/cover.png b/static/images/blog/appwrite-webhooks/cover.png new file mode 100644 index 0000000000..47b9ae51e3 Binary files /dev/null and b/static/images/blog/appwrite-webhooks/cover.png differ