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
- Pełna kontrola - Self-hosted oznacza, że Twoje dane nigdy nie opuszczają Twoich serwerów
- Open Source - Kod źródłowy jest w pełni dostępny, możesz go audytować i modyfikować
- Kompletność - Jeden produkt zamiast wielu serwisów (auth + db + storage + functions)
- Prostota - Intuicyjny dashboard i doskonałe SDK
- Wieloplatformowość - Natywne SDK dla Web, Flutter, iOS, Android, React Native
- Docker-native - Łatwy deployment i skalowanie
- Aktywna społeczność - Szybki rozwój i wsparcie
Appwrite vs Firebase vs Supabase
| Cecha | Appwrite | Firebase | Supabase |
|---|---|---|---|
| Open Source | ✅ Pełny | ❌ Zamknięty | ✅ Pełny |
| Self-hosting | ✅ Native | ❌ Nie | ✅ Możliwy |
| Typ bazy | Dokument | Dokument | PostgreSQL |
| Real-time | ✅ Wbudowany | ✅ Wbudowany | ✅ Wbudowany |
| Functions | ✅ Multi-runtime | ✅ Tylko JS/TS | ✅ Edge + DB |
| Storage | ✅ Wbudowany | ✅ Wbudowany | ✅ Wbudowany |
| Cena Cloud | Free: 75K MAU | Free: 50K MAU | Free: 50K MAU |
| Vendor lock-in | Niski | Wysoki | Niski |
| Flutter SDK | ✅ Natywny | ✅ Natywny | ✅ Natywny |
Instalacja i Setup
Self-hosting z Docker
Najprostszy sposób uruchomienia Appwrite to użycie oficjalnego skryptu instalacyjnego:
# 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:80Docker Compose (zalecane)
Dla większej kontroli użyj docker-compose:
# 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:# Uruchomienie
docker-compose up -d
# Sprawdzenie statusu
docker-compose ps
# Logi
docker-compose logs -f appwriteAppwrite Cloud
Dla szybkiego startu bez zarządzania infrastrukturą:
# 1. Zarejestruj się na cloud.appwrite.io
# 2. Utwórz nowy projekt
# 3. Skopiuj Project ID i EndpointInstalacja SDK
# 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/appwriteAuthentication - Kompletny System Autoryzacji
Konfiguracja klienta
// 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
// 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
// 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)
// 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
// 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)
// 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
// 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
// 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
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
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
// 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
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
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
// 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
// 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
// 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
{
"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
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
// 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
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
// 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
// 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
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
// 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
| Plan | Cena | MAU | Bandwidth | Storage | Functions |
|---|---|---|---|---|---|
| Free | $0/mo | 75,000 | 10GB | 2GB | 750K exec |
| Pro | $15/mo | 200,000+ | 300GB | 150GB | 3.5M exec |
| Scale | $599/mo | Unlimited | Unlimited | Unlimited | Unlimited |
| Enterprise | Custom | Custom | Custom | Custom | Custom |
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?
- Eksportuj dane z Firestore do JSON
- Utwórz odpowiednie kolekcje w Appwrite
- Zaimportuj dane przez SDK lub API
- Zaktualizuj kod aplikacji (podobne API)
- 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
- Full control - Self-hosted means your data never leaves your servers
- Open Source - The source code is fully available, you can audit and modify it
- Completeness - One product instead of many services (auth + db + storage + functions)
- Simplicity - Intuitive dashboard and excellent SDKs
- Multi-platform - Native SDKs for Web, Flutter, iOS, Android, React Native
- Docker-native - Easy deployment and scaling
- Active community - Rapid development and support
Appwrite vs Firebase vs Supabase
| Feature | Appwrite | Firebase | Supabase |
|---|---|---|---|
| Open Source | ✅ Full | ❌ Closed | ✅ Full |
| Self-hosting | ✅ Native | ❌ No | ✅ Possible |
| Database type | Document | Document | PostgreSQL |
| Real-time | ✅ Built-in | ✅ Built-in | ✅ Built-in |
| Functions | ✅ Multi-runtime | ✅ JS/TS only | ✅ Edge + DB |
| Storage | ✅ Built-in | ✅ Built-in | ✅ Built-in |
| Cloud price | Free: 75K MAU | Free: 50K MAU | Free: 50K MAU |
| Vendor lock-in | Low | High | Low |
| 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:
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:80Docker Compose (recommended)
For greater control use docker-compose:
# 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:docker-compose up -d
docker-compose ps
docker-compose logs -f appwriteAppwrite Cloud
For a quick start without managing infrastructure:
# 1. Sign up at cloud.appwrite.io
# 2. Create a new project
# 3. Copy the Project ID and EndpointSDK installation
# 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/appwriteAuthentication - complete authorization system
Client configuration
// 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
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
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)
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
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)
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
// 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
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
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
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
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
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
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
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
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
// 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
{
"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
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
// 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
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
// 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
// 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
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
// 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
| Plan | Price | MAU | Bandwidth | Storage | Functions |
|---|---|---|---|---|---|
| Free | $0/mo | 75,000 | 10GB | 2GB | 750K exec |
| Pro | $15/mo | 200,000+ | 300GB | 150GB | 3.5M exec |
| Scale | $599/mo | Unlimited | Unlimited | Unlimited | Unlimited |
| Enterprise | Custom | Custom | Custom | Custom | Custom |
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?
- Export data from Firestore to JSON
- Create the corresponding collections in Appwrite
- Import the data via the SDK or API
- Update your application code (similar API)
- 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).