Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds
Torna alle collezioni
Guide35 min read

Appwrite

Appwrite is an open-source Backend-as-a-Service with complete auth system, database, storage, functions, and realtime. Self-hosted Firebase alternative.

Appwrite - Kompletny Przewodnik po Open-Source BaaS

Czym jest Appwrite?

Appwrite to open-source Backend-as-a-Service (BaaS), który dostarcza kompletny zestaw narzędzi backendowych dla aplikacji webowych i mobilnych. Jest to self-hosted alternatywa dla Firebase, która daje pełną kontrolę nad danymi i infrastrukturą. Appwrite oferuje authentication, bazy danych dokumentów, storage plików, Cloud Functions, realtime subscriptions i messaging - wszystko w jednym pakiecie.

Założony w 2019 roku przez Eldada Fux, Appwrite szybko zyskał popularność w społeczności open-source. Projekt ma ponad 40,000 gwiazdek na GitHub i aktywną społeczność deweloperów. W 2023 roku Appwrite uruchomił Appwrite Cloud - zarządzaną wersję chmurową, która eliminuje konieczność samodzielnego hostowania.

Appwrite wyróżnia się podejściem "developer-first" - każda funkcja jest zaprojektowana z myślą o prostocie użycia i doskonałej dokumentacji. SDK są dostępne dla wszystkich popularnych platform: Web, Flutter, iOS, Android, i wielu frameworków backendowych.

Dlaczego Appwrite?

Kluczowe zalety

  1. Pełna kontrola - Self-hosted oznacza, że Twoje dane nigdy nie opuszczają Twoich serwerów
  2. Open Source - Kod źródłowy jest w pełni dostępny, możesz go audytować i modyfikować
  3. Kompletność - Jeden produkt zamiast wielu serwisów (auth + db + storage + functions)
  4. Prostota - Intuicyjny dashboard i doskonałe SDK
  5. Wieloplatformowość - Natywne SDK dla Web, Flutter, iOS, Android, React Native
  6. Docker-native - Łatwy deployment i skalowanie
  7. Aktywna społeczność - Szybki rozwój i wsparcie

Appwrite vs Firebase vs Supabase

CechaAppwriteFirebaseSupabase
Open Source✅ Pełny❌ Zamknięty✅ Pełny
Self-hosting✅ Native❌ Nie✅ Możliwy
Typ bazyDokumentDokumentPostgreSQL
Real-time✅ Wbudowany✅ Wbudowany✅ Wbudowany
Functions✅ Multi-runtime✅ Tylko JS/TS✅ Edge + DB
Storage✅ Wbudowany✅ Wbudowany✅ Wbudowany
Cena CloudFree: 75K MAUFree: 50K MAUFree: 50K MAU
Vendor lock-inNiskiWysokiNiski
Flutter SDK✅ Natywny✅ Natywny✅ Natywny

Instalacja i Setup

Self-hosting z Docker

Najprostszy sposób uruchomienia Appwrite to użycie oficjalnego skryptu instalacyjnego:

Code
Bash
# Instalacja jedną komendą
docker run -it --rm \
  --volume /var/run/docker.sock:/var/run/docker.sock \
  --volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
  --entrypoint="install" \
  appwrite/appwrite:1.5.6

# Po instalacji otwórz http://localhost:80

Docker Compose (zalecane)

Dla większej kontroli użyj docker-compose:

docker-compose.yml
YAML
# docker-compose.yml
version: '3'

services:
  appwrite:
    image: appwrite/appwrite:1.5.6
    container_name: appwrite
    restart: unless-stopped
    ports:
      - 80:80
      - 443:443
    volumes:
      - appwrite-uploads:/storage/uploads
      - appwrite-cache:/storage/cache
      - appwrite-config:/storage/config
      - appwrite-certificates:/storage/certificates
      - appwrite-functions:/storage/functions
    environment:
      - _APP_ENV=production
      - _APP_OPENSSL_KEY_V1=your-secret-key
      - _APP_DOMAIN=localhost
      - _APP_DOMAIN_TARGET=localhost
      - _APP_REDIS_HOST=redis
      - _APP_REDIS_PORT=6379
      - _APP_DB_HOST=mariadb
      - _APP_DB_PORT=3306
      - _APP_DB_USER=appwrite
      - _APP_DB_PASS=password
      - _APP_STORAGE_DEVICE=local
      - _APP_STORAGE_S3_ACCESS_KEY=
      - _APP_STORAGE_S3_SECRET=
      - _APP_STORAGE_S3_REGION=us-east-1
      - _APP_STORAGE_S3_BUCKET=
    depends_on:
      - mariadb
      - redis

  mariadb:
    image: mariadb:10.11
    container_name: appwrite-mariadb
    restart: unless-stopped
    volumes:
      - appwrite-mariadb:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=rootpassword
      - MYSQL_DATABASE=appwrite
      - MYSQL_USER=appwrite
      - MYSQL_PASSWORD=password

  redis:
    image: redis:7.2-alpine
    container_name: appwrite-redis
    restart: unless-stopped
    volumes:
      - appwrite-redis:/data

volumes:
  appwrite-uploads:
  appwrite-cache:
  appwrite-config:
  appwrite-certificates:
  appwrite-functions:
  appwrite-mariadb:
  appwrite-redis:
Code
Bash
# Uruchomienie
docker-compose up -d

# Sprawdzenie statusu
docker-compose ps

# Logi
docker-compose logs -f appwrite

Appwrite Cloud

Dla szybkiego startu bez zarządzania infrastrukturą:

Code
Bash
# 1. Zarejestruj się na cloud.appwrite.io
# 2. Utwórz nowy projekt
# 3. Skopiuj Project ID i Endpoint

Instalacja SDK

Code
Bash
# JavaScript/TypeScript (Web)
npm install appwrite

# React Native
npm install react-native-appwrite

# Flutter
flutter pub add appwrite

# Node.js (Server)
npm install node-appwrite

# Python
pip install appwrite

# PHP
composer require appwrite/appwrite

Authentication - Kompletny System Autoryzacji

Konfiguracja klienta

TSlib/appwrite.ts
TypeScript
// lib/appwrite.ts
import { Client, Account, Databases, Storage, Functions } from 'appwrite'

const client = new Client()
  .setEndpoint('https://cloud.appwrite.io/v1') // Lub Twój self-hosted URL
  .setProject('your-project-id')

export const account = new Account(client)
export const databases = new Databases(client)
export const storage = new Storage(client)
export const functions = new Functions(client)
export { client }

Rejestracja i logowanie

Code
TypeScript
// Rejestracja nowego użytkownika
async function register(email: string, password: string, name: string) {
  try {
    // Utwórz konto
    const user = await account.create(
      'unique()', // Automatycznie generowany ID
      email,
      password,
      name
    )

    // Automatycznie zaloguj
    await account.createEmailPasswordSession(email, password)

    // Wyślij email weryfikacyjny
    await account.createVerification('https://example.com/verify')

    return user
  } catch (error) {
    console.error('Registration error:', error)
    throw error
  }
}

// Logowanie
async function login(email: string, password: string) {
  try {
    const session = await account.createEmailPasswordSession(email, password)
    return session
  } catch (error) {
    console.error('Login error:', error)
    throw error
  }
}

// Wylogowanie
async function logout() {
  try {
    await account.deleteSession('current')
  } catch (error) {
    console.error('Logout error:', error)
    throw error
  }
}

// Pobranie aktualnego użytkownika
async function getCurrentUser() {
  try {
    return await account.get()
  } catch (error) {
    // Użytkownik nie jest zalogowany
    return null
  }
}

OAuth - Social Login

Code
TypeScript
// Google OAuth
async function loginWithGoogle() {
  account.createOAuth2Session(
    'google',
    'https://example.com/success', // Success URL
    'https://example.com/failure', // Failure URL
    ['email', 'profile'] // Scopes
  )
}

// GitHub OAuth
async function loginWithGitHub() {
  account.createOAuth2Session(
    'github',
    'https://example.com/success',
    'https://example.com/failure'
  )
}

// Apple Sign In
async function loginWithApple() {
  account.createOAuth2Session(
    'apple',
    'https://example.com/success',
    'https://example.com/failure'
  )
}

// Discord OAuth
async function loginWithDiscord() {
  account.createOAuth2Session(
    'discord',
    'https://example.com/success',
    'https://example.com/failure',
    ['identify', 'email']
  )
}

Magic Link (Passwordless)

Code
TypeScript
// Wyślij magic link
async function sendMagicLink(email: string) {
  await account.createMagicURLToken(
    'unique()',
    email,
    'https://example.com/login?userId={userId}&secret={secret}'
  )
}

// Weryfikacja magic link
async function verifyMagicLink(userId: string, secret: string) {
  const session = await account.createSession(userId, secret)
  return session
}

Phone Authentication

Code
TypeScript
// Wyślij kod SMS
async function sendPhoneCode(phone: string) {
  await account.createPhoneToken(
    'unique()',
    phone // Format: +48123456789
  )
}

// Weryfikacja kodu SMS
async function verifyPhoneCode(userId: string, code: string) {
  const session = await account.createSession(userId, code)
  return session
}

Multi-Factor Authentication (MFA)

Code
TypeScript
// Włącz MFA
async function enableMFA() {
  // 1. Utwórz TOTP secret
  const totp = await account.createMfaAuthenticator('totp')

  // 2. Pokaż użytkownikowi QR code
  console.log('Secret:', totp.secret)
  console.log('QR URI:', totp.uri)

  return totp
}

// Weryfikuj i aktywuj MFA
async function verifyMFA(code: string) {
  await account.updateMfaAuthenticator('totp', code)
}

// Logowanie z MFA
async function loginWithMFA(email: string, password: string, mfaCode: string) {
  // 1. Normalne logowanie
  const session = await account.createEmailPasswordSession(email, password)

  // 2. Weryfikacja MFA jeśli wymagana
  if (session.mfaRequired) {
    await account.updateMfaChallenge(
      session.mfaChallengeId,
      mfaCode
    )
  }

  return session
}

React Hook dla Auth

TShooks/useAuth.ts
TypeScript
// hooks/useAuth.ts
import { useState, useEffect, createContext, useContext } from 'react'
import { account } from '@/lib/appwrite'
import type { Models } from 'appwrite'

interface AuthContextType {
  user: Models.User<Models.Preferences> | null
  loading: boolean
  login: (email: string, password: string) => Promise<void>
  logout: () => Promise<void>
  register: (email: string, password: string, name: string) => Promise<void>
}

const AuthContext = createContext<AuthContextType | null>(null)

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<Models.User<Models.Preferences> | null>(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    checkUser()
  }, [])

  async function checkUser() {
    try {
      const currentUser = await account.get()
      setUser(currentUser)
    } catch {
      setUser(null)
    } finally {
      setLoading(false)
    }
  }

  async function login(email: string, password: string) {
    await account.createEmailPasswordSession(email, password)
    await checkUser()
  }

  async function logout() {
    await account.deleteSession('current')
    setUser(null)
  }

  async function register(email: string, password: string, name: string) {
    await account.create('unique()', email, password, name)
    await login(email, password)
  }

  return (
    <AuthContext.Provider value={{ user, loading, login, logout, register }}>
      {children}
    </AuthContext.Provider>
  )
}

export function useAuth() {
  const context = useContext(AuthContext)
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider')
  }
  return context
}

Database - Dokumentowa Baza Danych

Struktura danych

Appwrite używa modelu dokumentowego z hierarchią:

  • Database → Kontener na kolekcje
  • Collection → Schemat dokumentów (jak tabela)
  • Document → Pojedynczy rekord
  • Attribute → Pole dokumentu

Tworzenie schematu

Code
TypeScript
// W konsoli Appwrite lub przez SDK
import { Databases, Permission, Role } from 'node-appwrite'

const databases = new Databases(client)

// 1. Utwórz bazę danych
const database = await databases.create(
  'main', // Database ID
  'Main Database' // Name
)

// 2. Utwórz kolekcję
const collection = await databases.createCollection(
  'main',
  'posts',
  'Blog Posts',
  [
    Permission.read(Role.any()), // Publiczny odczyt
    Permission.create(Role.users()), // Zalogowani mogą tworzyć
    Permission.update(Role.users()), // Zalogowani mogą edytować
    Permission.delete(Role.users())  // Zalogowani mogą usuwać
  ]
)

// 3. Dodaj atrybuty
await databases.createStringAttribute('main', 'posts', 'title', 255, true)
await databases.createStringAttribute('main', 'posts', 'content', 65535, true)
await databases.createStringAttribute('main', 'posts', 'slug', 255, true)
await databases.createStringAttribute('main', 'posts', 'authorId', 36, true)
await databases.createBooleanAttribute('main', 'posts', 'published', true, false)
await databases.createDatetimeAttribute('main', 'posts', 'publishedAt', false)
await databases.createStringAttribute('main', 'posts', 'tags', 50, false, undefined, true) // Array
await databases.createIntegerAttribute('main', 'posts', 'views', false, 0, 0, 999999999)

// 4. Utwórz indeksy
await databases.createIndex('main', 'posts', 'slug_index', 'unique', ['slug'])
await databases.createIndex('main', 'posts', 'author_index', 'key', ['authorId'])
await databases.createIndex('main', 'posts', 'published_index', 'key', ['published', 'publishedAt'])

CRUD Operations

Code
TypeScript
import { databases } from '@/lib/appwrite'
import { Query, ID } from 'appwrite'

const DATABASE_ID = 'main'
const COLLECTION_ID = 'posts'

// CREATE - Tworzenie dokumentu
async function createPost(data: {
  title: string
  content: string
  slug: string
  authorId: string
  tags?: string[]
}) {
  const document = await databases.createDocument(
    DATABASE_ID,
    COLLECTION_ID,
    ID.unique(), // Lub własny ID
    {
      ...data,
      published: false,
      views: 0,
      createdAt: new Date().toISOString()
    }
  )
  return document
}

// READ - Pobieranie dokumentu
async function getPost(postId: string) {
  const document = await databases.getDocument(
    DATABASE_ID,
    COLLECTION_ID,
    postId
  )
  return document
}

// READ - Lista dokumentów z filtrowaniem
async function getPosts(options?: {
  published?: boolean
  authorId?: string
  tags?: string[]
  limit?: number
  offset?: number
}) {
  const queries: string[] = []

  if (options?.published !== undefined) {
    queries.push(Query.equal('published', options.published))
  }

  if (options?.authorId) {
    queries.push(Query.equal('authorId', options.authorId))
  }

  if (options?.tags?.length) {
    queries.push(Query.contains('tags', options.tags))
  }

  // Sortowanie
  queries.push(Query.orderDesc('$createdAt'))

  // Paginacja
  queries.push(Query.limit(options?.limit || 10))
  queries.push(Query.offset(options?.offset || 0))

  const documents = await databases.listDocuments(
    DATABASE_ID,
    COLLECTION_ID,
    queries
  )

  return documents
}

// UPDATE - Aktualizacja dokumentu
async function updatePost(postId: string, data: Partial<{
  title: string
  content: string
  published: boolean
  tags: string[]
}>) {
  const document = await databases.updateDocument(
    DATABASE_ID,
    COLLECTION_ID,
    postId,
    data
  )
  return document
}

// DELETE - Usuwanie dokumentu
async function deletePost(postId: string) {
  await databases.deleteDocument(
    DATABASE_ID,
    COLLECTION_ID,
    postId
  )
}

Zaawansowane Query

Code
TypeScript
import { Query } from 'appwrite'

// Wyszukiwanie pełnotekstowe
const results = await databases.listDocuments(
  DATABASE_ID,
  COLLECTION_ID,
  [
    Query.search('title', 'React tutorial'),
    Query.equal('published', true)
  ]
)

// Zakres dat
const recentPosts = await databases.listDocuments(
  DATABASE_ID,
  COLLECTION_ID,
  [
    Query.greaterThan('publishedAt', '2024-01-01'),
    Query.lessThan('publishedAt', '2024-12-31')
  ]
)

// Wartości w tablicy
const postsWithTags = await databases.listDocuments(
  DATABASE_ID,
  COLLECTION_ID,
  [
    Query.contains('tags', ['javascript', 'react'])
  ]
)

// Null check
const drafts = await databases.listDocuments(
  DATABASE_ID,
  COLLECTION_ID,
  [
    Query.isNull('publishedAt')
  ]
)

// Wybór pól (zmniejszenie transferu danych)
const titles = await databases.listDocuments(
  DATABASE_ID,
  COLLECTION_ID,
  [
    Query.select(['title', 'slug', '$id'])
  ]
)

// Kursor dla paginacji
const nextPage = await databases.listDocuments(
  DATABASE_ID,
  COLLECTION_ID,
  [
    Query.cursorAfter('lastDocumentId'),
    Query.limit(10)
  ]
)

Relacje między dokumentami

Code
TypeScript
// Appwrite nie ma natywnych relacji, ale można je symulować

// 1. Referencja przez ID
interface Post {
  $id: string
  title: string
  authorId: string // Referencja do User
}

interface Comment {
  $id: string
  content: string
  postId: string // Referencja do Post
  authorId: string
}

// 2. Pobieranie z relacjami (manual join)
async function getPostWithComments(postId: string) {
  const [post, comments] = await Promise.all([
    databases.getDocument(DATABASE_ID, 'posts', postId),
    databases.listDocuments(DATABASE_ID, 'comments', [
      Query.equal('postId', postId),
      Query.orderDesc('$createdAt')
    ])
  ])

  return {
    ...post,
    comments: comments.documents
  }
}

// 3. Pobieranie autora
async function getPostWithAuthor(postId: string) {
  const post = await databases.getDocument(DATABASE_ID, 'posts', postId)
  const author = await databases.getDocument(DATABASE_ID, 'users', post.authorId)

  return {
    ...post,
    author
  }
}

Storage - Przechowywanie Plików

Konfiguracja bucket

Code
TypeScript
import { Storage, Permission, Role } from 'node-appwrite'

const storage = new Storage(client)

// Utwórz bucket
const bucket = await storage.createBucket(
  'avatars',
  'User Avatars',
  [
    Permission.read(Role.any()), // Publiczny odczyt
    Permission.create(Role.users()), // Użytkownicy mogą uploadować
    Permission.update(Role.users()),
    Permission.delete(Role.users())
  ],
  false, // fileSecurity - czy sprawdzać uprawnienia na poziomie pliku
  true,  // enabled
  5 * 1024 * 1024, // maxFileSize - 5MB
  ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], // allowedFileExtensions
  'gzip', // compression
  true, // encryption
  true  // antivirus
)

Upload plików

Code
TypeScript
import { storage } from '@/lib/appwrite'
import { ID } from 'appwrite'

// Upload z przeglądarki
async function uploadFile(file: File, bucketId: string = 'uploads') {
  const result = await storage.createFile(
    bucketId,
    ID.unique(),
    file
  )
  return result
}

// Upload z inputem
function FileUpload() {
  const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0]
    if (!file) return

    try {
      const result = await uploadFile(file, 'avatars')
      console.log('Uploaded:', result)
    } catch (error) {
      console.error('Upload error:', error)
    }
  }

  return (
    <input
      type="file"
      accept="image/*"
      onChange={handleUpload}
    />
  )
}

// Upload z progress
async function uploadWithProgress(
  file: File,
  bucketId: string,
  onProgress: (progress: number) => void
) {
  const result = await storage.createFile(
    bucketId,
    ID.unique(),
    file,
    undefined, // permissions
    (progress) => {
      onProgress(Math.round((progress.chunksUploaded / progress.chunksTotal) * 100))
    }
  )
  return result
}

Pobieranie i wyświetlanie plików

Code
TypeScript
// URL do pliku
function getFileUrl(bucketId: string, fileId: string) {
  return storage.getFileView(bucketId, fileId)
}

// URL do podglądu z transformacją
function getFilePreview(
  bucketId: string,
  fileId: string,
  options?: {
    width?: number
    height?: number
    quality?: number
    gravity?: string
    output?: string
  }
) {
  return storage.getFilePreview(
    bucketId,
    fileId,
    options?.width,
    options?.height,
    options?.gravity || 'center',
    options?.quality || 90,
    undefined, // borderWidth
    undefined, // borderColor
    undefined, // borderRadius
    undefined, // opacity
    undefined, // rotation
    undefined, // background
    options?.output || 'webp'
  )
}

// Komponent Image
function AppwriteImage({
  bucketId,
  fileId,
  width,
  height,
  alt
}: {
  bucketId: string
  fileId: string
  width: number
  height: number
  alt: string
}) {
  const url = getFilePreview(bucketId, fileId, { width, height })

  return (
    <img
      src={url.href}
      width={width}
      height={height}
      alt={alt}
      loading="lazy"
    />
  )
}

Download i usuwanie

Code
TypeScript
// Pobierz plik do download
async function downloadFile(bucketId: string, fileId: string) {
  const result = storage.getFileDownload(bucketId, fileId)

  // Otwórz w nowym oknie
  window.open(result.href, '_blank')
}

// Usuń plik
async function deleteFile(bucketId: string, fileId: string) {
  await storage.deleteFile(bucketId, fileId)
}

// Lista plików
async function listFiles(bucketId: string) {
  const files = await storage.listFiles(bucketId)
  return files.files
}

// Informacje o pliku
async function getFileInfo(bucketId: string, fileId: string) {
  const file = await storage.getFile(bucketId, fileId)
  return {
    id: file.$id,
    name: file.name,
    size: file.sizeOriginal,
    mimeType: file.mimeType,
    createdAt: file.$createdAt
  }
}

Cloud Functions

Tworzenie funkcji

JSfunctions/send-welcome-email/src/main.js
JavaScript
// functions/send-welcome-email/src/main.js
import { Client, Users } from 'node-appwrite'

export default async ({ req, res, log, error }) => {
  // Inicjalizacja klienta z API key
  const client = new Client()
    .setEndpoint(process.env.APPWRITE_FUNCTION_API_ENDPOINT)
    .setProject(process.env.APPWRITE_FUNCTION_PROJECT_ID)
    .setKey(req.headers['x-appwrite-key'])

  try {
    const { userId, email, name } = JSON.parse(req.body)

    log(`Sending welcome email to ${email}`)

    // Wyślij email (np. przez Resend, SendGrid)
    await sendWelcomeEmail(email, name)

    return res.json({
      success: true,
      message: `Welcome email sent to ${email}`
    })
  } catch (err) {
    error(err.message)
    return res.json({
      success: false,
      error: err.message
    }, 500)
  }
}

async function sendWelcomeEmail(email, name) {
  // Implementacja wysyłki
}

Konfiguracja funkcji

functions/send-welcome-email/appwrite.json
JSON
// functions/send-welcome-email/appwrite.json
{
  "projectId": "your-project-id",
  "projectName": "Your Project",
  "functions": [
    {
      "$id": "send-welcome-email",
      "name": "Send Welcome Email",
      "runtime": "node-18.0",
      "execute": ["users"],
      "events": ["users.*.create"],
      "schedule": "",
      "timeout": 15,
      "enabled": true,
      "logging": true,
      "entrypoint": "src/main.js",
      "commands": "npm install",
      "scopes": ["users.read"]
    }
  ]
}

Wywołanie funkcji

Code
TypeScript
import { functions } from '@/lib/appwrite'

// Synchroniczne wywołanie
async function callFunction() {
  const execution = await functions.createExecution(
    'send-welcome-email',
    JSON.stringify({ email: 'user@example.com', name: 'John' }),
    false, // async
    '/', // path
    'POST', // method
    { 'Content-Type': 'application/json' } // headers
  )

  return JSON.parse(execution.responseBody)
}

// Asynchroniczne wywołanie
async function callFunctionAsync() {
  const execution = await functions.createExecution(
    'process-data',
    JSON.stringify({ data: 'large-dataset' }),
    true // async
  )

  // Execution ID do sprawdzenia statusu później
  return execution.$id
}

// Sprawdzenie statusu
async function checkExecution(functionId: string, executionId: string) {
  const execution = await functions.getExecution(functionId, executionId)
  return {
    status: execution.status,
    response: execution.responseBody,
    errors: execution.errors
  }
}

Przykład: Webhook handler

JSfunctions/stripe-webhook/src/main.js
JavaScript
// functions/stripe-webhook/src/main.js
import Stripe from 'stripe'
import { Client, Databases } from 'node-appwrite'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)

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(process.env.APPWRITE_API_KEY)

  const databases = new Databases(client)

  try {
    // Weryfikacja webhook signature
    const signature = req.headers['stripe-signature']
    const event = stripe.webhooks.constructEvent(
      req.bodyRaw,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET
    )

    log(`Processing Stripe event: ${event.type}`)

    switch (event.type) {
      case 'checkout.session.completed': {
        const session = event.data.object

        // Zaktualizuj status zamówienia
        await databases.updateDocument(
          'main',
          'orders',
          session.metadata.orderId,
          {
            status: 'paid',
            stripePaymentId: session.payment_intent,
            paidAt: new Date().toISOString()
          }
        )
        break
      }

      case 'customer.subscription.created': {
        const subscription = event.data.object

        // Aktywuj subskrypcję użytkownika
        await databases.updateDocument(
          'main',
          'users',
          subscription.metadata.userId,
          {
            subscriptionStatus: 'active',
            subscriptionId: subscription.id,
            subscriptionEndsAt: new Date(subscription.current_period_end * 1000).toISOString()
          }
        )
        break
      }
    }

    return res.json({ received: true })
  } catch (err) {
    error(err.message)
    return res.json({ error: err.message }, 400)
  }
}

Realtime Subscriptions

Subskrypcje w czasie rzeczywistym

Code
TypeScript
import { client } from '@/lib/appwrite'

// Subskrybuj zmiany w kolekcji
function subscribeToCollection(
  databaseId: string,
  collectionId: string,
  callback: (payload: any) => void
) {
  const channel = `databases.${databaseId}.collections.${collectionId}.documents`

  return client.subscribe(channel, (response) => {
    console.log('Event:', response.events)
    console.log('Payload:', response.payload)
    callback(response.payload)
  })
}

// Subskrybuj konkretny dokument
function subscribeToDocument(
  databaseId: string,
  collectionId: string,
  documentId: string,
  callback: (payload: any) => void
) {
  const channel = `databases.${databaseId}.collections.${collectionId}.documents.${documentId}`

  return client.subscribe(channel, (response) => {
    callback(response.payload)
  })
}

// Subskrybuj pliki
function subscribeToFiles(bucketId: string, callback: (payload: any) => void) {
  const channel = `buckets.${bucketId}.files`

  return client.subscribe(channel, (response) => {
    callback(response.payload)
  })
}

// Subskrybuj status użytkownika
function subscribeToAccount(callback: (payload: any) => void) {
  return client.subscribe('account', (response) => {
    callback(response.payload)
  })
}

React Hook dla Realtime

TShooks/useRealtime.ts
TypeScript
// hooks/useRealtime.ts
import { useEffect, useState } from 'react'
import { client } from '@/lib/appwrite'

export function useRealtimeCollection<T>(
  databaseId: string,
  collectionId: string,
  initialData: T[]
) {
  const [data, setData] = useState<T[]>(initialData)

  useEffect(() => {
    const channel = `databases.${databaseId}.collections.${collectionId}.documents`

    const unsubscribe = client.subscribe(channel, (response) => {
      const eventType = response.events[0]
      const document = response.payload as T & { $id: string }

      if (eventType.includes('.create')) {
        setData(prev => [document, ...prev])
      } else if (eventType.includes('.update')) {
        setData(prev => prev.map(item =>
          (item as any).$id === document.$id ? document : item
        ))
      } else if (eventType.includes('.delete')) {
        setData(prev => prev.filter(item =>
          (item as any).$id !== document.$id
        ))
      }
    })

    return () => {
      unsubscribe()
    }
  }, [databaseId, collectionId])

  return data
}

// Użycie
function ChatMessages({ chatId }: { chatId: string }) {
  const [initialMessages, setInitialMessages] = useState([])

  useEffect(() => {
    // Pobierz początkowe wiadomości
    databases.listDocuments('main', 'messages', [
      Query.equal('chatId', chatId),
      Query.orderDesc('$createdAt')
    ]).then(res => setInitialMessages(res.documents))
  }, [chatId])

  const messages = useRealtimeCollection('main', 'messages', initialMessages)

  return (
    <div>
      {messages.map(msg => (
        <div key={msg.$id}>{msg.content}</div>
      ))}
    </div>
  )
}

Integracje i SDK

Next.js App Router

TSapp/api/posts/route.ts
TypeScript
// app/api/posts/route.ts
import { NextResponse } from 'next/server'
import { Client, Databases, Query } from 'node-appwrite'

const client = new Client()
  .setEndpoint(process.env.APPWRITE_ENDPOINT!)
  .setProject(process.env.APPWRITE_PROJECT_ID!)
  .setKey(process.env.APPWRITE_API_KEY!)

const databases = new Databases(client)

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const page = parseInt(searchParams.get('page') || '1')
  const limit = 10

  const posts = await databases.listDocuments(
    'main',
    'posts',
    [
      Query.equal('published', true),
      Query.orderDesc('$createdAt'),
      Query.limit(limit),
      Query.offset((page - 1) * limit)
    ]
  )

  return NextResponse.json(posts)
}

export async function POST(request: Request) {
  const body = await request.json()

  const post = await databases.createDocument(
    'main',
    'posts',
    'unique()',
    body
  )

  return NextResponse.json(post)
}

Flutter Integration

lib/appwrite.dart
DART
// lib/appwrite.dart
import 'package:appwrite/appwrite.dart';

class AppwriteService {
  static final Client client = Client()
    .setEndpoint('https://cloud.appwrite.io/v1')
    .setProject('your-project-id');

  static final Account account = Account(client);
  static final Databases databases = Databases(client);
  static final Storage storage = Storage(client);
}

// lib/services/auth_service.dart
class AuthService {
  final Account _account = AppwriteService.account;

  Future<User> register(String email, String password, String name) async {
    final user = await _account.create(
      userId: ID.unique(),
      email: email,
      password: password,
      name: name,
    );
    return user;
  }

  Future<Session> login(String email, String password) async {
    final session = await _account.createEmailPasswordSession(
      email: email,
      password: password,
    );
    return session;
  }

  Future<User?> getCurrentUser() async {
    try {
      return await _account.get();
    } catch (e) {
      return null;
    }
  }

  Future<void> logout() async {
    await _account.deleteSession(sessionId: 'current');
  }
}

React Native Integration

TSlib/appwrite.ts
TypeScript
// lib/appwrite.ts (React Native)
import { Client, Account, Databases, Storage } from 'react-native-appwrite'

const client = new Client()
  .setEndpoint('https://cloud.appwrite.io/v1')
  .setProject('your-project-id')
  .setPlatform('com.example.myapp') // Bundle ID

export const account = new Account(client)
export const databases = new Databases(client)
export const storage = new Storage(client)

// Upload z React Native
import * as ImagePicker from 'expo-image-picker'

async function pickAndUploadImage() {
  const result = await ImagePicker.launchImageLibraryAsync({
    mediaTypes: ImagePicker.MediaTypeOptions.Images,
    quality: 0.8,
  })

  if (!result.canceled) {
    const file = {
      uri: result.assets[0].uri,
      name: 'photo.jpg',
      type: 'image/jpeg',
    }

    const uploaded = await storage.createFile(
      'avatars',
      ID.unique(),
      file
    )

    return uploaded
  }
}

Cennik i Plany

Appwrite Cloud

PlanCenaMAUBandwidthStorageFunctions
Free$0/mo75,00010GB2GB750K exec
Pro$15/mo200,000+300GB150GB3.5M exec
Scale$599/moUnlimitedUnlimitedUnlimitedUnlimited
EnterpriseCustomCustomCustomCustomCustom

Self-hosted

  • Darmowy - Bez limitów użytkowników i funkcji
  • Koszt - Tylko infrastruktura (serwer, storage)
  • Wymagania - 1 vCPU, 2GB RAM (minimum), 4 vCPU, 8GB RAM (zalecane)

FAQ - Najczęściej Zadawane Pytania

Czy Appwrite jest lepszy od Firebase?

To zależy od potrzeb. Appwrite wygrywa w:

  • Open source i self-hosting
  • Kontrola nad danymi
  • Brak vendor lock-in
  • Prywatność i compliance (GDPR)

Firebase wygrywa w:

  • Integracja z Google Cloud
  • Analytics i A/B testing
  • Push notifications
  • Dojrzałość ekosystemu

Jak migrować z Firebase do Appwrite?

  1. Eksportuj dane z Firestore do JSON
  2. Utwórz odpowiednie kolekcje w Appwrite
  3. Zaimportuj dane przez SDK lub API
  4. Zaktualizuj kod aplikacji (podobne API)
  5. Migruj authentication (użytkownicy muszą zresetować hasła)

Czy Appwrite obsługuje full-text search?

Tak, Appwrite ma wbudowane wyszukiwanie pełnotekstowe przez Query.search(). Dla zaawansowanych potrzeb można zintegrować z Meilisearch lub Typesense.

Jak skalować Appwrite?

  • Horyzontalnie - Wiele instancji za load balancerem
  • Baza danych - MariaDB cluster lub zewnętrzny managed DB
  • Storage - S3-compatible storage (MinIO, AWS S3)
  • Redis - Redis cluster dla cache i sessions

Czy mogę używać własnego SMTP?

Tak, Appwrite wspiera konfigurację własnego serwera SMTP dla wysyłki emaili (weryfikacja, reset hasła, powiadomienia).


Appwrite - a complete guide to open-source BaaS

What is Appwrite?

Appwrite is an open-source Backend-as-a-Service (BaaS) that provides a complete set of backend tools for web and mobile applications. It is a self-hosted alternative to Firebase that gives you full control over your data and infrastructure. Appwrite offers authentication, document databases, file storage, Cloud Functions, realtime subscriptions, and messaging - all in a single package.

Founded in 2019 by Eldad Fux, Appwrite quickly gained popularity in the open-source community. The project has over 40,000 stars on GitHub and an active developer community. In 2023, Appwrite launched Appwrite Cloud - a managed cloud version that eliminates the need for self-hosting.

Appwrite stands out with its "developer-first" approach - every feature is designed with simplicity and excellent documentation in mind. SDKs are available for all popular platforms: Web, Flutter, iOS, Android, and many backend frameworks.

Why Appwrite?

Key advantages

  1. Full control - Self-hosted means your data never leaves your servers
  2. Open Source - The source code is fully available, you can audit and modify it
  3. Completeness - One product instead of many services (auth + db + storage + functions)
  4. Simplicity - Intuitive dashboard and excellent SDKs
  5. Multi-platform - Native SDKs for Web, Flutter, iOS, Android, React Native
  6. Docker-native - Easy deployment and scaling
  7. Active community - Rapid development and support

Appwrite vs Firebase vs Supabase

FeatureAppwriteFirebaseSupabase
Open Source✅ Full❌ Closed✅ Full
Self-hosting✅ Native❌ No✅ Possible
Database typeDocumentDocumentPostgreSQL
Real-time✅ Built-in✅ Built-in✅ Built-in
Functions✅ Multi-runtime✅ JS/TS only✅ Edge + DB
Storage✅ Built-in✅ Built-in✅ Built-in
Cloud priceFree: 75K MAUFree: 50K MAUFree: 50K MAU
Vendor lock-inLowHighLow
Flutter SDK✅ Native✅ Native✅ Native

Installation and setup

Self-hosting with Docker

The simplest way to run Appwrite is to use the official installation script:

Code
Bash
docker run -it --rm \
  --volume /var/run/docker.sock:/var/run/docker.sock \
  --volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
  --entrypoint="install" \
  appwrite/appwrite:1.5.6

# After installation open http://localhost:80

Docker Compose (recommended)

For greater control use docker-compose:

docker-compose.yml
YAML
# docker-compose.yml
version: '3'

services:
  appwrite:
    image: appwrite/appwrite:1.5.6
    container_name: appwrite
    restart: unless-stopped
    ports:
      - 80:80
      - 443:443
    volumes:
      - appwrite-uploads:/storage/uploads
      - appwrite-cache:/storage/cache
      - appwrite-config:/storage/config
      - appwrite-certificates:/storage/certificates
      - appwrite-functions:/storage/functions
    environment:
      - _APP_ENV=production
      - _APP_OPENSSL_KEY_V1=your-secret-key
      - _APP_DOMAIN=localhost
      - _APP_DOMAIN_TARGET=localhost
      - _APP_REDIS_HOST=redis
      - _APP_REDIS_PORT=6379
      - _APP_DB_HOST=mariadb
      - _APP_DB_PORT=3306
      - _APP_DB_USER=appwrite
      - _APP_DB_PASS=password
      - _APP_STORAGE_DEVICE=local
      - _APP_STORAGE_S3_ACCESS_KEY=
      - _APP_STORAGE_S3_SECRET=
      - _APP_STORAGE_S3_REGION=us-east-1
      - _APP_STORAGE_S3_BUCKET=
    depends_on:
      - mariadb
      - redis

  mariadb:
    image: mariadb:10.11
    container_name: appwrite-mariadb
    restart: unless-stopped
    volumes:
      - appwrite-mariadb:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=rootpassword
      - MYSQL_DATABASE=appwrite
      - MYSQL_USER=appwrite
      - MYSQL_PASSWORD=password

  redis:
    image: redis:7.2-alpine
    container_name: appwrite-redis
    restart: unless-stopped
    volumes:
      - appwrite-redis:/data

volumes:
  appwrite-uploads:
  appwrite-cache:
  appwrite-config:
  appwrite-certificates:
  appwrite-functions:
  appwrite-mariadb:
  appwrite-redis:
Code
Bash
docker-compose up -d

docker-compose ps

docker-compose logs -f appwrite

Appwrite Cloud

For a quick start without managing infrastructure:

Code
Bash
# 1. Sign up at cloud.appwrite.io
# 2. Create a new project
# 3. Copy the Project ID and Endpoint

SDK installation

Code
Bash
# JavaScript/TypeScript (Web)
npm install appwrite

# React Native
npm install react-native-appwrite

# Flutter
flutter pub add appwrite

# Node.js (Server)
npm install node-appwrite

# Python
pip install appwrite

# PHP
composer require appwrite/appwrite

Authentication - complete authorization system

Client configuration

TSlib/appwrite.ts
TypeScript
// lib/appwrite.ts
import { Client, Account, Databases, Storage, Functions } from 'appwrite'

const client = new Client()
  .setEndpoint('https://cloud.appwrite.io/v1')
  .setProject('your-project-id')

export const account = new Account(client)
export const databases = new Databases(client)
export const storage = new Storage(client)
export const functions = new Functions(client)
export { client }

Registration and login

Code
TypeScript
async function register(email: string, password: string, name: string) {
  try {
    const user = await account.create(
      'unique()',
      email,
      password,
      name
    )

    await account.createEmailPasswordSession(email, password)

    await account.createVerification('https://example.com/verify')

    return user
  } catch (error) {
    console.error('Registration error:', error)
    throw error
  }
}

async function login(email: string, password: string) {
  try {
    const session = await account.createEmailPasswordSession(email, password)
    return session
  } catch (error) {
    console.error('Login error:', error)
    throw error
  }
}

async function logout() {
  try {
    await account.deleteSession('current')
  } catch (error) {
    console.error('Logout error:', error)
    throw error
  }
}

async function getCurrentUser() {
  try {
    return await account.get()
  } catch (error) {
    return null
  }
}

OAuth - social login

Code
TypeScript
async function loginWithGoogle() {
  account.createOAuth2Session(
    'google',
    'https://example.com/success',
    'https://example.com/failure',
    ['email', 'profile']
  )
}

async function loginWithGitHub() {
  account.createOAuth2Session(
    'github',
    'https://example.com/success',
    'https://example.com/failure'
  )
}

async function loginWithApple() {
  account.createOAuth2Session(
    'apple',
    'https://example.com/success',
    'https://example.com/failure'
  )
}

async function loginWithDiscord() {
  account.createOAuth2Session(
    'discord',
    'https://example.com/success',
    'https://example.com/failure',
    ['identify', 'email']
  )
}

Magic Link (passwordless)

Code
TypeScript
async function sendMagicLink(email: string) {
  await account.createMagicURLToken(
    'unique()',
    email,
    'https://example.com/login?userId={userId}&secret={secret}'
  )
}

async function verifyMagicLink(userId: string, secret: string) {
  const session = await account.createSession(userId, secret)
  return session
}

Phone authentication

Code
TypeScript
async function sendPhoneCode(phone: string) {
  await account.createPhoneToken(
    'unique()',
    phone // Format: +48123456789
  )
}

async function verifyPhoneCode(userId: string, code: string) {
  const session = await account.createSession(userId, code)
  return session
}

Multi-Factor Authentication (MFA)

Code
TypeScript
async function enableMFA() {
  const totp = await account.createMfaAuthenticator('totp')

  console.log('Secret:', totp.secret)
  console.log('QR URI:', totp.uri)

  return totp
}

async function verifyMFA(code: string) {
  await account.updateMfaAuthenticator('totp', code)
}

async function loginWithMFA(email: string, password: string, mfaCode: string) {
  const session = await account.createEmailPasswordSession(email, password)

  if (session.mfaRequired) {
    await account.updateMfaChallenge(
      session.mfaChallengeId,
      mfaCode
    )
  }

  return session
}

React hook for auth

TShooks/useAuth.ts
TypeScript
// hooks/useAuth.ts
import { useState, useEffect, createContext, useContext } from 'react'
import { account } from '@/lib/appwrite'
import type { Models } from 'appwrite'

interface AuthContextType {
  user: Models.User<Models.Preferences> | null
  loading: boolean
  login: (email: string, password: string) => Promise<void>
  logout: () => Promise<void>
  register: (email: string, password: string, name: string) => Promise<void>
}

const AuthContext = createContext<AuthContextType | null>(null)

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<Models.User<Models.Preferences> | null>(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    checkUser()
  }, [])

  async function checkUser() {
    try {
      const currentUser = await account.get()
      setUser(currentUser)
    } catch {
      setUser(null)
    } finally {
      setLoading(false)
    }
  }

  async function login(email: string, password: string) {
    await account.createEmailPasswordSession(email, password)
    await checkUser()
  }

  async function logout() {
    await account.deleteSession('current')
    setUser(null)
  }

  async function register(email: string, password: string, name: string) {
    await account.create('unique()', email, password, name)
    await login(email, password)
  }

  return (
    <AuthContext.Provider value={{ user, loading, login, logout, register }}>
      {children}
    </AuthContext.Provider>
  )
}

export function useAuth() {
  const context = useContext(AuthContext)
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider')
  }
  return context
}

Database - document database

Data structure

Appwrite uses a document model with the following hierarchy:

  • Database - Container for collections
  • Collection - Document schema (like a table)
  • Document - A single record
  • Attribute - A document field

Creating a schema

Code
TypeScript
import { Databases, Permission, Role } from 'node-appwrite'

const databases = new Databases(client)

const database = await databases.create(
  'main',
  'Main Database'
)

const collection = await databases.createCollection(
  'main',
  'posts',
  'Blog Posts',
  [
    Permission.read(Role.any()),
    Permission.create(Role.users()),
    Permission.update(Role.users()),
    Permission.delete(Role.users())
  ]
)

await databases.createStringAttribute('main', 'posts', 'title', 255, true)
await databases.createStringAttribute('main', 'posts', 'content', 65535, true)
await databases.createStringAttribute('main', 'posts', 'slug', 255, true)
await databases.createStringAttribute('main', 'posts', 'authorId', 36, true)
await databases.createBooleanAttribute('main', 'posts', 'published', true, false)
await databases.createDatetimeAttribute('main', 'posts', 'publishedAt', false)
await databases.createStringAttribute('main', 'posts', 'tags', 50, false, undefined, true)
await databases.createIntegerAttribute('main', 'posts', 'views', false, 0, 0, 999999999)

await databases.createIndex('main', 'posts', 'slug_index', 'unique', ['slug'])
await databases.createIndex('main', 'posts', 'author_index', 'key', ['authorId'])
await databases.createIndex('main', 'posts', 'published_index', 'key', ['published', 'publishedAt'])

CRUD operations

Code
TypeScript
import { databases } from '@/lib/appwrite'
import { Query, ID } from 'appwrite'

const DATABASE_ID = 'main'
const COLLECTION_ID = 'posts'

async function createPost(data: {
  title: string
  content: string
  slug: string
  authorId: string
  tags?: string[]
}) {
  const document = await databases.createDocument(
    DATABASE_ID,
    COLLECTION_ID,
    ID.unique(),
    {
      ...data,
      published: false,
      views: 0,
      createdAt: new Date().toISOString()
    }
  )
  return document
}

async function getPost(postId: string) {
  const document = await databases.getDocument(
    DATABASE_ID,
    COLLECTION_ID,
    postId
  )
  return document
}

async function getPosts(options?: {
  published?: boolean
  authorId?: string
  tags?: string[]
  limit?: number
  offset?: number
}) {
  const queries: string[] = []

  if (options?.published !== undefined) {
    queries.push(Query.equal('published', options.published))
  }

  if (options?.authorId) {
    queries.push(Query.equal('authorId', options.authorId))
  }

  if (options?.tags?.length) {
    queries.push(Query.contains('tags', options.tags))
  }

  queries.push(Query.orderDesc('$createdAt'))

  queries.push(Query.limit(options?.limit || 10))
  queries.push(Query.offset(options?.offset || 0))

  const documents = await databases.listDocuments(
    DATABASE_ID,
    COLLECTION_ID,
    queries
  )

  return documents
}

async function updatePost(postId: string, data: Partial<{
  title: string
  content: string
  published: boolean
  tags: string[]
}>) {
  const document = await databases.updateDocument(
    DATABASE_ID,
    COLLECTION_ID,
    postId,
    data
  )
  return document
}

async function deletePost(postId: string) {
  await databases.deleteDocument(
    DATABASE_ID,
    COLLECTION_ID,
    postId
  )
}

Advanced queries

Code
TypeScript
import { Query } from 'appwrite'

const results = await databases.listDocuments(
  DATABASE_ID,
  COLLECTION_ID,
  [
    Query.search('title', 'React tutorial'),
    Query.equal('published', true)
  ]
)

const recentPosts = await databases.listDocuments(
  DATABASE_ID,
  COLLECTION_ID,
  [
    Query.greaterThan('publishedAt', '2024-01-01'),
    Query.lessThan('publishedAt', '2024-12-31')
  ]
)

const postsWithTags = await databases.listDocuments(
  DATABASE_ID,
  COLLECTION_ID,
  [
    Query.contains('tags', ['javascript', 'react'])
  ]
)

const drafts = await databases.listDocuments(
  DATABASE_ID,
  COLLECTION_ID,
  [
    Query.isNull('publishedAt')
  ]
)

const titles = await databases.listDocuments(
  DATABASE_ID,
  COLLECTION_ID,
  [
    Query.select(['title', 'slug', '$id'])
  ]
)

const nextPage = await databases.listDocuments(
  DATABASE_ID,
  COLLECTION_ID,
  [
    Query.cursorAfter('lastDocumentId'),
    Query.limit(10)
  ]
)

Relationships between documents

Code
TypeScript
interface Post {
  $id: string
  title: string
  authorId: string
}

interface Comment {
  $id: string
  content: string
  postId: string
  authorId: string
}

async function getPostWithComments(postId: string) {
  const [post, comments] = await Promise.all([
    databases.getDocument(DATABASE_ID, 'posts', postId),
    databases.listDocuments(DATABASE_ID, 'comments', [
      Query.equal('postId', postId),
      Query.orderDesc('$createdAt')
    ])
  ])

  return {
    ...post,
    comments: comments.documents
  }
}

async function getPostWithAuthor(postId: string) {
  const post = await databases.getDocument(DATABASE_ID, 'posts', postId)
  const author = await databases.getDocument(DATABASE_ID, 'users', post.authorId)

  return {
    ...post,
    author
  }
}

Storage - file storage

Bucket configuration

Code
TypeScript
import { Storage, Permission, Role } from 'node-appwrite'

const storage = new Storage(client)

const bucket = await storage.createBucket(
  'avatars',
  'User Avatars',
  [
    Permission.read(Role.any()),
    Permission.create(Role.users()),
    Permission.update(Role.users()),
    Permission.delete(Role.users())
  ],
  false, // fileSecurity - whether to check permissions at the file level
  true,  // enabled
  5 * 1024 * 1024, // maxFileSize - 5MB
  ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], // allowedFileExtensions
  'gzip', // compression
  true, // encryption
  true  // antivirus
)

File uploads

Code
TypeScript
import { storage } from '@/lib/appwrite'
import { ID } from 'appwrite'

async function uploadFile(file: File, bucketId: string = 'uploads') {
  const result = await storage.createFile(
    bucketId,
    ID.unique(),
    file
  )
  return result
}

function FileUpload() {
  const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0]
    if (!file) return

    try {
      const result = await uploadFile(file, 'avatars')
      console.log('Uploaded:', result)
    } catch (error) {
      console.error('Upload error:', error)
    }
  }

  return (
    <input
      type="file"
      accept="image/*"
      onChange={handleUpload}
    />
  )
}

async function uploadWithProgress(
  file: File,
  bucketId: string,
  onProgress: (progress: number) => void
) {
  const result = await storage.createFile(
    bucketId,
    ID.unique(),
    file,
    undefined,
    (progress) => {
      onProgress(Math.round((progress.chunksUploaded / progress.chunksTotal) * 100))
    }
  )
  return result
}

Retrieving and displaying files

Code
TypeScript
function getFileUrl(bucketId: string, fileId: string) {
  return storage.getFileView(bucketId, fileId)
}

function getFilePreview(
  bucketId: string,
  fileId: string,
  options?: {
    width?: number
    height?: number
    quality?: number
    gravity?: string
    output?: string
  }
) {
  return storage.getFilePreview(
    bucketId,
    fileId,
    options?.width,
    options?.height,
    options?.gravity || 'center',
    options?.quality || 90,
    undefined, // borderWidth
    undefined, // borderColor
    undefined, // borderRadius
    undefined, // opacity
    undefined, // rotation
    undefined, // background
    options?.output || 'webp'
  )
}

function AppwriteImage({
  bucketId,
  fileId,
  width,
  height,
  alt
}: {
  bucketId: string
  fileId: string
  width: number
  height: number
  alt: string
}) {
  const url = getFilePreview(bucketId, fileId, { width, height })

  return (
    <img
      src={url.href}
      width={width}
      height={height}
      alt={alt}
      loading="lazy"
    />
  )
}

Download and deletion

Code
TypeScript
async function downloadFile(bucketId: string, fileId: string) {
  const result = storage.getFileDownload(bucketId, fileId)

  window.open(result.href, '_blank')
}

async function deleteFile(bucketId: string, fileId: string) {
  await storage.deleteFile(bucketId, fileId)
}

async function listFiles(bucketId: string) {
  const files = await storage.listFiles(bucketId)
  return files.files
}

async function getFileInfo(bucketId: string, fileId: string) {
  const file = await storage.getFile(bucketId, fileId)
  return {
    id: file.$id,
    name: file.name,
    size: file.sizeOriginal,
    mimeType: file.mimeType,
    createdAt: file.$createdAt
  }
}

Cloud Functions

Creating a function

JSfunctions/send-welcome-email/src/main.js
JavaScript
// functions/send-welcome-email/src/main.js
import { Client, Users } from 'node-appwrite'

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'])

  try {
    const { userId, email, name } = JSON.parse(req.body)

    log(`Sending welcome email to ${email}`)

    await sendWelcomeEmail(email, name)

    return res.json({
      success: true,
      message: `Welcome email sent to ${email}`
    })
  } catch (err) {
    error(err.message)
    return res.json({
      success: false,
      error: err.message
    }, 500)
  }
}

async function sendWelcomeEmail(email, name) {
}

Function configuration

functions/send-welcome-email/appwrite.json
JSON
// functions/send-welcome-email/appwrite.json
{
  "projectId": "your-project-id",
  "projectName": "Your Project",
  "functions": [
    {
      "$id": "send-welcome-email",
      "name": "Send Welcome Email",
      "runtime": "node-18.0",
      "execute": ["users"],
      "events": ["users.*.create"],
      "schedule": "",
      "timeout": 15,
      "enabled": true,
      "logging": true,
      "entrypoint": "src/main.js",
      "commands": "npm install",
      "scopes": ["users.read"]
    }
  ]
}

Calling functions

Code
TypeScript
import { functions } from '@/lib/appwrite'

async function callFunction() {
  const execution = await functions.createExecution(
    'send-welcome-email',
    JSON.stringify({ email: 'user@example.com', name: 'John' }),
    false, // async
    '/', // path
    'POST', // method
    { 'Content-Type': 'application/json' } // headers
  )

  return JSON.parse(execution.responseBody)
}

async function callFunctionAsync() {
  const execution = await functions.createExecution(
    'process-data',
    JSON.stringify({ data: 'large-dataset' }),
    true // async
  )

  return execution.$id
}

async function checkExecution(functionId: string, executionId: string) {
  const execution = await functions.getExecution(functionId, executionId)
  return {
    status: execution.status,
    response: execution.responseBody,
    errors: execution.errors
  }
}

Example: webhook handler

JSfunctions/stripe-webhook/src/main.js
JavaScript
// functions/stripe-webhook/src/main.js
import Stripe from 'stripe'
import { Client, Databases } from 'node-appwrite'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)

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(process.env.APPWRITE_API_KEY)

  const databases = new Databases(client)

  try {
    const signature = req.headers['stripe-signature']
    const event = stripe.webhooks.constructEvent(
      req.bodyRaw,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET
    )

    log(`Processing Stripe event: ${event.type}`)

    switch (event.type) {
      case 'checkout.session.completed': {
        const session = event.data.object

        await databases.updateDocument(
          'main',
          'orders',
          session.metadata.orderId,
          {
            status: 'paid',
            stripePaymentId: session.payment_intent,
            paidAt: new Date().toISOString()
          }
        )
        break
      }

      case 'customer.subscription.created': {
        const subscription = event.data.object

        await databases.updateDocument(
          'main',
          'users',
          subscription.metadata.userId,
          {
            subscriptionStatus: 'active',
            subscriptionId: subscription.id,
            subscriptionEndsAt: new Date(subscription.current_period_end * 1000).toISOString()
          }
        )
        break
      }
    }

    return res.json({ received: true })
  } catch (err) {
    error(err.message)
    return res.json({ error: err.message }, 400)
  }
}

Realtime subscriptions

Real-time subscriptions

Code
TypeScript
import { client } from '@/lib/appwrite'

function subscribeToCollection(
  databaseId: string,
  collectionId: string,
  callback: (payload: any) => void
) {
  const channel = `databases.${databaseId}.collections.${collectionId}.documents`

  return client.subscribe(channel, (response) => {
    console.log('Event:', response.events)
    console.log('Payload:', response.payload)
    callback(response.payload)
  })
}

function subscribeToDocument(
  databaseId: string,
  collectionId: string,
  documentId: string,
  callback: (payload: any) => void
) {
  const channel = `databases.${databaseId}.collections.${collectionId}.documents.${documentId}`

  return client.subscribe(channel, (response) => {
    callback(response.payload)
  })
}

function subscribeToFiles(bucketId: string, callback: (payload: any) => void) {
  const channel = `buckets.${bucketId}.files`

  return client.subscribe(channel, (response) => {
    callback(response.payload)
  })
}

function subscribeToAccount(callback: (payload: any) => void) {
  return client.subscribe('account', (response) => {
    callback(response.payload)
  })
}

React hook for realtime

TShooks/useRealtime.ts
TypeScript
// hooks/useRealtime.ts
import { useEffect, useState } from 'react'
import { client } from '@/lib/appwrite'

export function useRealtimeCollection<T>(
  databaseId: string,
  collectionId: string,
  initialData: T[]
) {
  const [data, setData] = useState<T[]>(initialData)

  useEffect(() => {
    const channel = `databases.${databaseId}.collections.${collectionId}.documents`

    const unsubscribe = client.subscribe(channel, (response) => {
      const eventType = response.events[0]
      const document = response.payload as T & { $id: string }

      if (eventType.includes('.create')) {
        setData(prev => [document, ...prev])
      } else if (eventType.includes('.update')) {
        setData(prev => prev.map(item =>
          (item as any).$id === document.$id ? document : item
        ))
      } else if (eventType.includes('.delete')) {
        setData(prev => prev.filter(item =>
          (item as any).$id !== document.$id
        ))
      }
    })

    return () => {
      unsubscribe()
    }
  }, [databaseId, collectionId])

  return data
}

function ChatMessages({ chatId }: { chatId: string }) {
  const [initialMessages, setInitialMessages] = useState([])

  useEffect(() => {
    databases.listDocuments('main', 'messages', [
      Query.equal('chatId', chatId),
      Query.orderDesc('$createdAt')
    ]).then(res => setInitialMessages(res.documents))
  }, [chatId])

  const messages = useRealtimeCollection('main', 'messages', initialMessages)

  return (
    <div>
      {messages.map(msg => (
        <div key={msg.$id}>{msg.content}</div>
      ))}
    </div>
  )
}

Integrations and SDKs

Next.js App Router

TSapp/api/posts/route.ts
TypeScript
// app/api/posts/route.ts
import { NextResponse } from 'next/server'
import { Client, Databases, Query } from 'node-appwrite'

const client = new Client()
  .setEndpoint(process.env.APPWRITE_ENDPOINT!)
  .setProject(process.env.APPWRITE_PROJECT_ID!)
  .setKey(process.env.APPWRITE_API_KEY!)

const databases = new Databases(client)

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const page = parseInt(searchParams.get('page') || '1')
  const limit = 10

  const posts = await databases.listDocuments(
    'main',
    'posts',
    [
      Query.equal('published', true),
      Query.orderDesc('$createdAt'),
      Query.limit(limit),
      Query.offset((page - 1) * limit)
    ]
  )

  return NextResponse.json(posts)
}

export async function POST(request: Request) {
  const body = await request.json()

  const post = await databases.createDocument(
    'main',
    'posts',
    'unique()',
    body
  )

  return NextResponse.json(post)
}

Flutter integration

lib/appwrite.dart
DART
// lib/appwrite.dart
import 'package:appwrite/appwrite.dart';

class AppwriteService {
  static final Client client = Client()
    .setEndpoint('https://cloud.appwrite.io/v1')
    .setProject('your-project-id');

  static final Account account = Account(client);
  static final Databases databases = Databases(client);
  static final Storage storage = Storage(client);
}

// lib/services/auth_service.dart
class AuthService {
  final Account _account = AppwriteService.account;

  Future<User> register(String email, String password, String name) async {
    final user = await _account.create(
      userId: ID.unique(),
      email: email,
      password: password,
      name: name,
    );
    return user;
  }

  Future<Session> login(String email, String password) async {
    final session = await _account.createEmailPasswordSession(
      email: email,
      password: password,
    );
    return session;
  }

  Future<User?> getCurrentUser() async {
    try {
      return await _account.get();
    } catch (e) {
      return null;
    }
  }

  Future<void> logout() async {
    await _account.deleteSession(sessionId: 'current');
  }
}

React Native integration

TSlib/appwrite.ts
TypeScript
// lib/appwrite.ts (React Native)
import { Client, Account, Databases, Storage } from 'react-native-appwrite'

const client = new Client()
  .setEndpoint('https://cloud.appwrite.io/v1')
  .setProject('your-project-id')
  .setPlatform('com.example.myapp') // Bundle ID

export const account = new Account(client)
export const databases = new Databases(client)
export const storage = new Storage(client)

import * as ImagePicker from 'expo-image-picker'

async function pickAndUploadImage() {
  const result = await ImagePicker.launchImageLibraryAsync({
    mediaTypes: ImagePicker.MediaTypeOptions.Images,
    quality: 0.8,
  })

  if (!result.canceled) {
    const file = {
      uri: result.assets[0].uri,
      name: 'photo.jpg',
      type: 'image/jpeg',
    }

    const uploaded = await storage.createFile(
      'avatars',
      ID.unique(),
      file
    )

    return uploaded
  }
}

Pricing and plans

Appwrite Cloud

PlanPriceMAUBandwidthStorageFunctions
Free$0/mo75,00010GB2GB750K exec
Pro$15/mo200,000+300GB150GB3.5M exec
Scale$599/moUnlimitedUnlimitedUnlimitedUnlimited
EnterpriseCustomCustomCustomCustomCustom

Self-hosted

  • Free - No limits on users or functions
  • Cost - Infrastructure only (server, storage)
  • Requirements - 1 vCPU, 2GB RAM (minimum), 4 vCPU, 8GB RAM (recommended)

FAQ - frequently asked questions

Is Appwrite better than Firebase?

It depends on your needs. Appwrite wins in:

  • Open source and self-hosting
  • Control over your data
  • No vendor lock-in
  • Privacy and compliance (GDPR)

Firebase wins in:

  • Integration with Google Cloud
  • Analytics and A/B testing
  • Push notifications
  • Ecosystem maturity

How to migrate from Firebase to Appwrite?

  1. Export data from Firestore to JSON
  2. Create the corresponding collections in Appwrite
  3. Import the data via the SDK or API
  4. Update your application code (similar API)
  5. Migrate authentication (users will need to reset their passwords)

Does Appwrite support full-text search?

Yes, Appwrite has built-in full-text search via Query.search(). For more advanced needs you can integrate with Meilisearch or Typesense.

How to scale Appwrite?

  • Horizontally - Multiple instances behind a load balancer
  • Database - MariaDB cluster or an external managed DB
  • Storage - S3-compatible storage (MinIO, AWS S3)
  • Redis - Redis cluster for cache and sessions

Can I use my own SMTP?

Yes, Appwrite supports configuring your own SMTP server for sending emails (verification, password reset, notifications).