Communitygithub.com

Appwrite Patterns

오픈소스 BaaS인 Appwrite를 사용한 인증, TablesDB, 스토리지, 서버리스 함수, 실시간 구독 및 권한 모델 패턴을 제공합니다.

Appwrite Patterns란 무엇인가요?

이 스킬은 Appwrite를 백엔드 서비스로 활용하는 개발자를 위한 종합적인 패턴 가이드입니다. Appwrite는 인증, TablesDB, 파일 스토리지, 서버리스 함수, 실시간 기능을 통합 SDK로 제공하는 오픈소스 BaaS(Backend-as-a-Service)입니다. 이 스킬은 ‘보안 우선’ 설계 철학을 강조하며, 모든 행과 파일에 명시적인 권한 설정이 필수적이고, `Permission`과 `Role` 클래스를 통해 세밀한 접근 제어가 가능합니다. 클라이언트(`appwrite`)와 서버(`node-appwrite`) SDK가 분리되어 있으며, 서버는 API 키 인증을, 클라이언트는 세션 기반 인증을 사용합니다. TablesDB는 신규 프로젝트에 권장되는 최신 데이터베이스 API이며, `Query` 클래스를 통해 타입 세이프한 필터링, 정렬, 페이지네이션을 지원합니다. 에러 처리는 `AppwriteException`을 try/catch로 잡아야 하며, `ID.unique()`를 사용해 자동 생성된 ID를 활용하는 것이 핵심 패턴입니다. 이 가이드는 Claude Code, Cursor, Codex 등 TypeScript 기반 AI 에이전트가 Appwrite를 백엔드로 통합할 때 필요한 모든 패턴을 커버합니다.

지원 대상✓Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/agents-inc/skills/tree/HEAD/src/skills/api-baas-appwrite

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

Appwrite Patterns은(는) 무엇을 하나요?

빠른 가이드: Appwrite를 오픈소스 BaaS로 사용하여 인증, 구조화된 데이터(TablesDB), 파일 스토리지, 서버리스 함수, 실시간 구독을 구현하세요. 항상 공유 Client 인스턴스에서 서비스 클래스를 초기화하고, 모든 행에 명시적으로 권한을 설정하며(기본적으로 아무것도 접근 불가), 백엔드에서는 API 키 인증을 사용하는 서버 SDK(node-appwrite)만 사용하세요.


<critical_requirements>

중요: 이 스킬을 사용하기 전에

모든 코드는 CLAUDE.md의 프로젝트 규칙을 따라야 합니다(kebab-case, 명명된 내보내기, import 순서, import type, 명명된 상수).

(모든 행과 파일에 명시적으로 권한을 설정해야 합니다. Appwrite는 기본적으로 접근을 허용하지 않으므로, 권한을 생략하면 데이터에 접근할 수 없습니다)

(서버에서는 node-appwrite와 API 키 인증을, 클라이언트에서는 appwrite와 세션 인증을 사용해야 합니다. 클라이언트 번들에 API 키를 절대 노출하지 마세요)

(모든 SDK 호출에서 항상 AppwriteException을 확인해야 합니다. Appwrite는 예외를 던지며, { data, error } 튜플을 반환하지 않습니다)

(자동 생성 ID에는 항상 ID.unique()를 사용하세요. 원시 문자열을 전달하면 사용자 정의 ID가 생성되며, 자동 생성되지 않습니다)

(새로운 프로젝트에는 TablesDB API를 사용해야 합니다. 레거시 Databases/collections/documents API는 더 이상 사용되지 않으며 보안 패치만 받습니다)

</critical_requirements>


자동 감지 키워드: Appwrite, appwrite, node-appwrite, TablesDB, Account, Databases, Permission, Role, ID.unique, Query.equal, createEmailPasswordSession, realtime.subscribe, Channel.files, Channel.tablesdb, APPWRITE_FUNCTION

사용 시기:

  • TypeScript로 Appwrite 클라이언트 또는 서버 SDK 설정
  • 인증 구현 (이메일/비밀번호, OAuth, 매직 URL, 전화 OTP, 익명 세션)
  • TablesDB 테이블 CRUD (행, 쿼리, 권한)
  • 접근 제어가 포함된 Storage 버킷 파일 업로드 및 제공
  • 이벤트, 스케줄 또는 HTTP로 트리거되는 서버리스 함수 구축
  • 행, 파일 또는 계정 이벤트에 대한 실시간 변경 구독
  • 팀 멤버십 및 역할 기반 권한 관리

주요 패턴:

  • 타입화된 서비스 클래스를 사용한 클라이언트 및 서버 SDK 초기화
  • 인증 흐름: 회원가입, 로그인, OAuth, 매직 URL, 세션 관리
  • Query 클래스를 사용한 TablesDB 작업 (필터링, 페이지네이션, 정렬)
  • 권한 모델: Permission.read(Role.any()), Role.user(), Role.team()
  • Storage: 업로드, 다운로드, 이미지 변환이 포함된 미리보기, 버킷 권한
  • 서버리스 함수: export default async ({ req, res, log, error }) => {}
  • Channel 헬퍼와 realtime.subscribe()를 통한 실시간 구독

사용하지 말아야 할 경우:

  • 직접 데이터베이스 연결 또는 SQL 쿼리 (Appwrite는 API 전용, 원시 SQL 없음)
  • 여러 테이블에 걸친 복잡한 관계형 조인 (Appwrite는 관계를 지원하지만 임의 SQL 조인은 지원하지 않음)
  • 서버 측 실시간이 필요한 애플리케이션 (Appwrite Realtime은 클라이언트 SDK 전용)

상세 리소스:

  • 의사 결정 프레임워크 및 안티패턴은 reference.md 참조

클라이언트 및 쿼리:

  • examples/core.md — 클라이언트 설정, TablesDB CRUD, 에러 처리, 권한

인증:

Storage 및 Functions:

실시간:


철학

Appwrite는 인증, 데이터베이스(TablesDB), 파일 스토리지, 서버리스 함수, 실시간 기능을 통합 SDK로 제공하는 오픈소스 백엔드 서비스입니다. 셀프 호스팅하거나 Appwrite Cloud를 통해 사용할 수 있습니다.

핵심 원칙:

  1. 기본 보안 — 명시적인 권한 없이는 어떤 것에도 접근할 수 없습니다. 모든 행과 파일에 권한을 설정해야 하며, 그렇지 않으면 모든 사용자에게 보이지 않습니다. 이는 "기본적으로 열려 있음"과 반대되는 개념입니다. 필수 단계로 취급하세요.
  2. 서비스 클래스 아키텍처 — 모든 SDK 상호작용은 공유 Client에서 인스턴스화된 서비스 클래스(Account, TablesDB, Storage, Functions, Teams, Realtime)를 통해 이루어집니다. 이 패턴은 클라이언트와 서버 SDK에서 일관됩니다.
  3. 두 개의 SDK 분리 — appwrite (클라이언트)는 브라우저용 세션 기반 인증을 사용합니다. node-appwrite (서버)는 백엔드용 API 키 인증을 사용합니다. 동일한 API 모양을 공유하지만 인증 메커니즘이 다릅니다. 절대 혼용하지 마세요.
  4. 튜플이 아닌 예외 — Appwrite는 실패 시 { data, error } 튜플이 아닌 AppwriteException을 던집니다. 항상 try/catch로 호출을 감싸세요.
  5. TablesDB가 미래 — Appwrite 1.8은 TablesDB(테이블/행/열)를 최신 API로 도입했습니다. 레거시 Databases API(컬렉션/문서/속성)는 여전히 작동하지만 더 이상 사용되지 않습니다. 새로운 기능은 TablesDB에만 추가됩니다.
  6. ID 생성 — 서버 생성 고유 ID에는 ID.unique()를 사용하세요. 문자열을 직접 전달하면 사용자 정의 ID가 되며(테이블 내에서 전역적으로 고유해야 함).

Appwrite 사용 시기:

  • 데이터를 완전히 제어할 수 있는 오픈소스, 셀프 호스팅 BaaS
  • 하나의 플랫폼에서 인증, 데이터베이스, 스토리지, 함수가 필요한 프로젝트
  • 공급업체 종속 없이 셀프 호스팅 가능한 BaaS를 원하는 팀
  • 세분화된 문서/행 수준 권한이 필요한 애플리케이션

사용하지 말아야 할 경우:

  • 조인, 뷰, 저장 프로시저가 필요한 복잡한 SQL 중심 애플리케이션
  • 서버 측 실시간 소비자 (Appwrite Realtime은 클라이언트 SDK 전용, 서버 SDK 지원 없음)
  • 로컬 우선 동기화가 필요한 오프라인 우선 앱 (Appwrite에는 내장 오프라인 동기화 없음)

핵심 패턴

패턴 1: 클라이언트 SDK 설정 (브라우저)

엔드포인트와 프로젝트 ID로 Appwrite 클라이언트를 초기화합니다. 모든 서비스 클래스는 단일 Client 인스턴스를 공유합니다.

// lib/appwrite.ts
import { Client, Account, TablesDB, Storage, Realtime } from "appwrite";

const APPWRITE_ENDPOINT = process.env.APPWRITE_ENDPOINT!;
const APPWRITE_PROJECT_ID = process.env.APPWRITE_PROJECT_ID!;

const client = new Client()
  .setEndpoint(APPWRITE_ENDPOINT)
  .setProject(APPWRITE_PROJECT_ID);

export const account = new Account(client);
export const tablesDB = new TablesDB(client);
export const storage = new Storage(client);
export const realtime = new Realtime(client);

좋은 이유: 단일 클라이언트 인스턴스를 서비스 전체에서 공유, 설정을 위한 명명된 상수, 각 서비스를 명명된 내보내기로 제공, 환경 변수로 코드에서 설정 분리

// 잘못된 예: 하드코딩된 설정, 클라이언트 재생성
import { Client, Account } from "appwrite";

const account = new Account(
  new Client()
    .setEndpoint("https://cloud.appwrite.io/v1") // 하드코딩
    .setProject("abc123"), // 하드코딩된 프로젝트 ID
);

잘못된 이유: 하드코딩된 엔드포인트와 프로젝트 ID, 서비스당 새 Client 인스턴스는 리소스 낭비, 공유 설정 없음


패턴 2: 서버 SDK 설정 (Node.js)

서버 SDK는 API 키 인증을 사용합니다. 클라이언트 코드에 API 키를 절대 노출하지 마세요.

// lib/appwrite-server.ts
import { Client, TablesDB, Users, Storage } from "node-appwrite";

const APPWRITE_ENDPOINT = process.env.APPWRITE_ENDPOINT!;
const APPWRITE_PROJECT_ID = process.env.APPWRITE_PROJECT_ID!;
const APPWRITE_API_KEY = process.env.APPWRITE_API_KEY!;

const client = new Client()
  .setEndpoint(APPWRITE_ENDPOINT)
  .setProject(APPWRITE_PROJECT_ID)
  .setKey(APPWRITE_API_KEY);

export const tablesDB = new TablesDB(client);
export const users = new Users(client);
export const storage = new Storage(client);

좋은 이유: .setKey()로 API 키 인증(서버 전용), Users 서비스로 관리자 사용자 관리(클라이언트 SDK에서는 사용 불가), 명명된 상수

사용 시기: API 라우트, 서버리스 함수, 관리 스크립트, 웹훅. 이 모듈을 클라이언트 측 코드에서 절대 import하지 마세요.


패턴 3: 에러 처리 (AppwriteException)

Appwrite는 AppwriteException을 던집니다. { data, error } 튜플을 반환하지 않습니다. 항상 try/catch를 사용하세요.

import { AppwriteException, type Models } from "appwrite";

async function getUser(): Promise<Models.User<Models.Preferences>> {
  try {
    return await account.get();
  } catch (error) {
    if (error instanceof AppwriteException) {
      // error.message — 사람이 읽을 수 있는 메시지
      // error.code — HTTP 상태 코드 (401, 404 등)
      // error.type — 기계가 읽을 수 있는 에러 유형 문자열
      throw new Error(`Appwrite 오류 ${error.code}: ${error.message}`);
    }
    throw error;
  }
}

좋은 이유: 타입이 좁혀진 AppwriteException은 code, message, type을 제공, 알 수 없는 오류는 다시 던짐, Models 네임스페이스를 사용한 명시적 반환 타입

// 잘못된 예: { data, error } 튜플 반환 가정
const { data, error } = await account.get(); // 잘못됨 — Appwrite는 튜플을 반환하지 않고 던짐
if (error) {
  /* ... */
}

잘못된 이유: Appwrite는 에러 튜플을 반환하지 않음. account.get()은 데이터를 반환하거나 예외를 던지므로 이 코드는 조용히 실패함


패턴 4: TablesDB 연산 (CRUD)

모든 데이터베이스 연산에 TablesDB를 사용하세요. 생성 시 행 ID가 필요하며, 자동 생성에는 ID.unique()를 사용하세요.

행 생성

import { ID, Permission, Role } from "appwrite";

interface Todo {
  title: string;
  completed: boolean;
}

const DATABASE_ID = "main";
const TABLE_ID = "todos";

const row = await tablesDB.createRow<Todo>({
  databaseId: DATABASE_ID,
  tableId: TABLE_ID,
  rowId: ID.unique(),
  data: {
    title: "장보기",
    completed: false,
  },
  permissions: [
    Permission.read(Role.user(userId)),
    Permission.update(Role.user(userId)),
    Permission.delete(Role.user(userId)),
  ],
});

쿼리로 행 목록 조회

import { Query } from "appwrite";

const PAGE_SIZE = 25;

const result = await tablesDB.listRows<Todo>({
  databaseId: DATABASE_ID,
  tableId: TABLE_ID,
  queries: [
    Query.equal("completed", false),
    Query.orderDesc("$createdAt"),
    Query.limit(PAGE_SIZE),
  ],
});

// result.rows — Todo 행 배열
// result.total — 일치하는 총 개수

행 업데이트

const updated = await tablesDB.updateRow<Todo>({
  databaseId: DATABASE_ID,
  tableId: TABLE_ID,
  rowId: row.$id,
  data: { completed: true },
});

행 삭제

await tablesDB.deleteRow({
  databaseId: DATABASE_ID,
  tableId: TABLE_ID,
  rowId: row.$id,
});

좋은 이유: 데이터베이스/테이블 ID를 위한 명명된 상수, 자동 생성 ID를 위한 ID.unique(), 생성 시 명시적 권한 설정, 타입 세이프 필터링을 위한 Query 클래스, 행 형태를 위한 제네릭 타입 매개변수


패턴 5: 권한 모델

Appwrite는 기본적으로 접근을 허용하지 않습니다. 모든 행과 파일에 권한을 설정해야 합니다.

Permission + Role 구문

import { Permission, Role } from "appwrite";

// 공개 읽기, 소유자 전체 접근
const publicReadPermissions = [
  Permission.read(Role.any()),
  Permission.update(Role.user(userId)),
  Permission.delete(Role.user(userId)),
];

// 팀 접근
const teamPermissions = [
  Permission.read(Role.team(teamId)),
  Permission.update(Role.team(teamId, "admin")),
  Permission.delete(Role.team(teamId, "owner")),
];

// 인증된 사용자만
const verifiedOnlyPermissions = [Permission.read(Role.users("verified"))];

일반적인 Role 헬퍼

Role.any(); //任何人 (게스트 포함)
Role.guests(); // 인증되지 않은 사용자만
Role.users(); // 모든 인증된 사용자
Role.users("verified"); // 인증된 사용자만
Role.user(userId); // 특정 사용자 ID
Role.team(teamId); // 팀의 모든 구성원
Role.team(teamId, "admin"); // "admin" 역할의 팀 구성원
Role.member(membershipId); // 특정 팀 멤버십
Role.label("premium"); // "premium" 레이블이 있는 사용자

좋은 이유: 모든 리소스에 명시적 권한, 역할 헬퍼는 타입 세이프, 팀 역할로 세분화된 접근 제어 가능

// 잘못된 예: 권한 없이 행 생성
const row = await tablesDB.createRow({
  databaseId: DATABASE_ID,
  tableId: TABLE_ID,
  rowId: ID.unique(),
  data: { title: "비밀" },
  // 권한이 설정되지 않음 — 이 행은 모든 사람에게 보이지 않습니다!
});

잘못된 이유: 권한이 없으면 행은 데이터베이스에 존재하지만 어떤 사용자도 읽기, 업데이트, 삭제할 수 없습니다(생성자조차도). 이것은 가장 흔한 Appwrite 실수입니다.


패턴 6: 인증 흐름

모든 인증 작업에는 Account 서비스를 사용하세요.

이메일/비밀번호 회원가입 및 로그인

import { ID, type Models } from "appwrite";

// 회원가입: 계정 생성 + 세션 생성 (두 단계)
const user = await account.create({
  userId: ID.unique(),
  email,
  password,
  name,
});
const session = await account.createEmailPasswordSession({ email, password });

// 로그인
const session = await account.createEmailPasswordSession({ email, password });

// 로그아웃 (현재 기기)
await account.deleteSession({ sessionId: "current" });

좋은 이유: 두 단계 회원가입 (계정 생성 + 세션 생성), 모든 SDK 호출에 객체 매개변수, 현재 기기만 로그아웃하는 deleteSession({ sessionId: "current" })

전체 인증 흐름 (OAuth, 매직 URL, 익명 세션, 팀, 비밀번호 재설정 포함)은 examples/auth.md를 참조하세요.


패턴 7: Storage 연산

Storage 서비스를 사용하여 파일을 업로드, 다운로드, 미리보기하세요.

import { ID, Permission, Role } from "appwrite";

const BUCKET_ID = "user-uploads";
const PREVIEW_WIDTH = 400;
const PREVIEW_HEIGHT = 300;

// 권한과 함께 업로드
await storage.createFile({
  bucketId: BUCKET_ID,
  fileId: ID.unique(),
  file,
  permissions: [
    Permission.read(Role.user(userId)),
    Permission.update(Role.user(userId)),
  ],
});

// 변환이 포함된 이미지 미리보기
const url = storage.getFilePreview({
  bucketId: BUCKET_ID,
  fileId: file.$id,
  width: PREVIEW_WIDTH,
  height: PREVIEW_HEIGHT,
  quality: 80,
});

// 파일 다운로드
const blob = await storage.getFileDownload({
  bucketId: BUCKET_ID,
  fileId: file.$id,
});

자세한 내용은 examples/storage.md를 참조하세요.

Individual skills in this repo

This repo contains 11 individual skills — each has its own dedicated page.

관련 스킬