이 문서에 대하여
주의 Nexa BaaS는 아직 공개 배포 전입니다. 저장소는 비공개이고, 다운로드·npm 패키지·호스팅 서비스는 제공하지 않습니다. 이 설명서는 현재 구현된 기능을 바탕으로 사용 흐름을 미리 정리한 것이며, 공개 시점에 API 경로·설정 이름·절차가 바뀔 수 있습니다.
Nexa BaaS는 모바일·웹 앱 개발자를 위한 자체 호스팅형 Backend-as-a-Service입니다. 앱마다 반복해서 만드는 인증·권한·데이터 저장·파일 업로드·예약 푸시·오프라인 동기화를 서버 한 벌로 제공하고, 개발자는 Docker 기반으로 직접 운영합니다.
이 문서는 다음 독자를 대상으로 합니다.
- Nexa BaaS 위에 앱을 만들 앱 개발자 — 콘솔, 프로젝트, SDK, REST API 사용법
- 서버를 직접 띄워 볼 운영자 — 자체 호스팅 절차와 환경 변수
제품 소개는 앱 소개 페이지를 참고하세요.
개념과 구성
핵심 개념
| 용어 | 뜻 |
|---|---|
| 개발자 계정 | 개발자 콘솔에 로그인하는 계정. 내부적으로 예약된 플랫폼 테넌트의 사용자입니다 |
| 프로젝트 | 앱 하나에 대응하는 격리된 백엔드 공간(= 테넌트). 개발자 계정 하나로 여러 개를 소유할 수 있습니다 |
| 프로젝트 ID(tenantId) | 프로젝트를 식별하는 UUID. SDK와 API 호출에 사용합니다 |
| 앱 사용자 | 각 프로젝트에 가입한 최종 사용자. 프로젝트마다 별도의 사용자 풀을 가집니다(같은 이메일도 프로젝트별로 별개 계정) |
| 컬렉션 | 데이터 API의 레코드(JSON) 묶음. 접근 규칙·동기화 여부·유니크 제약을 선언합니다 |
| 버킷 | 파일 스토리지의 파일 묶음. 컬렉션과 같은 문법의 접근 규칙을 가집니다 |
프로젝트 사이의 데이터는 PostgreSQL Row Level Security(RLS) 로 DB 수준에서 격리되며, 모든 요청은 해당 프로젝트 컨텍스트가 설정된 트랜잭션 안에서 실행됩니다.
서비스 구성 (12종)
| 서비스 | 제공 기능 | 주 사용 지점 |
|---|---|---|
| 인증 | 이메일 인증 기반 가입(승인 모드 3종), 로그인·토큰 갱신, 비밀번호 재설정, MFA(TOTP), Google 로그인 | SDK·REST |
| 사용자·그룹·권한(RBAC) | 사용자 관리, 그룹·멤버십, resource:action 권한(거부 우선 평가) | 콘솔·관리 API |
| 데이터 API | JSON 레코드 CRUD, 필터·정렬·페이지, 선언적 접근 규칙, 유니크 제약 | SDK·콘솔 |
| 파일 스토리지 | presigned 직접 업로드, 대용량 멀티파트(재개), 다운로드 서명 URL | SDK |
| 리마인더(예약 푸시) | 기기 등록, 원하는 시각에 푸시 예약·취소 | SDK |
| 오프라인 동기화 | 리비전 기반 델타 pull·배치 push, 충돌 정책 3종 | SDK |
| 웹훅 | 이벤트 Outbox 발행, HMAC 서명 발송·재시도, Slack·Telegram·카카오워크 커넥터 | 콘솔·관리 API |
| 개발자 콘솔 | 프로젝트 생성, 사용자·데이터·웹훅·OAuth·로그·사용량 화면 | 웹 |
| 플랜 한도 | 플랜별 가입자 수·스토리지 용량 자동 차단 | 서버 자동 |
| 사용량 미터링 | 데이터·파일·동기화 호출 일 단위 집계 | 콘솔 |
| 로그 관리 | 카테고리별 감사 로그 기록 설정·조회 | 콘솔·관리 API |
| 프로젝트별 OAuth(BYO) | 개발자 소유 Google 자격증명 등록(암호화 저장) | 콘솔 |
구성 요소
| 구성 요소 | 설명 | 기본 포트 |
|---|---|---|
| API 서버 | NestJS 애플리케이션. 모든 REST API의 경로 접두어는 /api/v1(헬스체크 제외) | 3000 |
| 개발자 콘솔 | Vite + React 웹 앱 | 5174 |
| 예제 앱 Nexa Words | SDK로 만든 오프라인 우선 단어장(참고용) | 5173 |
| PostgreSQL | 주 데이터베이스(필수) | 5432 |
| Redis(Valkey) | 예약 푸시·웹훅 발송·유지보수 작업 큐(선택) | 6379 |
| S3 호환 오브젝트 스토리지 | 파일 스토리지(선택, MinIO 등) | 9000 |
빠른 시작
전체 흐름은 다음과 같습니다.
- 서버를 띄웁니다 — 자체 호스팅 참고.
- 개발자 계정을 만들고 콘솔에 로그인합니다.
- 콘솔에서 프로젝트를 만들고 프로젝트 ID를 확인합니다.
- 콘솔의 데이터 화면에서 컬렉션과 접근 규칙을 만듭니다.
- 앱에서 SDK에 서버 주소와 프로젝트 ID를 넣고, 앱 사용자를 가입·로그인시킨 뒤 데이터를 읽고 씁니다.
- 콘솔에서 가입자 승인·차단, 로그, 사용량을 확인합니다.
개발자 콘솔
개발자 계정 만들기
콘솔 화면에는 아직 가입 양식이 없습니다. 개발자 계정은 플랫폼 테넌트를 대상으로 일반 가입 절차를 그대로 거쳐 만듭니다. 플랫폼 테넌트 ID는 서버 환경 변수 PLATFORM_TENANT_ID로 정하며, 지정하지 않으면 예약 UUID 00000000-0000-4000-8000-000000000000을 씁니다.
# ① 가입 신청 — 인증 메일 발송 (응답은 항상 202, 가입 여부를 노출하지 않음)
curl -X POST http://localhost:3000/api/v1/auth/signup/request \
-H 'content-type: application/json' \
-H 'x-tenant-id: <플랫폼 테넌트 ID>' \
-d '{"email":"[email protected]"}'
# ② 메일 속 링크의 token으로 검증 → signupSessionToken 수신
curl -X POST http://localhost:3000/api/v1/auth/signup/verify \
-H 'content-type: application/json' -H 'x-tenant-id: <플랫폼 테넌트 ID>' \
-d '{"token":"<메일의 토큰>"}'
# ③ 비밀번호를 정해 가입 완료
curl -X POST http://localhost:3000/api/v1/auth/signup/complete \
-H 'content-type: application/json' -H 'x-tenant-id: <플랫폼 테넌트 ID>' \
-d '{"signupSessionToken":"<②의 값>","password":"<비밀번호>","passwordConfirm":"<비밀번호>"}'
참고 메일 발송 설정(
MAIL_PROVIDER)이 기본값fake이면 실제 메일 대신 API 서버 콘솔 로그에 검증 링크가 출력됩니다. 로컬에서 시험할 때는 로그에서 토큰을 확인하세요.
로그인과 화면 구성
콘솔(기본 http://localhost:5174)에서 개발자 계정으로 로그인합니다. 콘솔이 접속할 API 주소와 플랫폼 테넌트 ID는 콘솔 빌드 환경 변수 VITE_NEXA_BASE_URL(기본 http://localhost:3000)과 VITE_NEXA_PLATFORM_TENANT_ID로 바꿀 수 있습니다.
- 왼쪽 사이드바 — 대시보드 · 프로젝트, 그리고 프로젝트를 고르면 데이터 · 파일 · 리마인더 · 웹훅 · 소셜 로그인 · 사용량 · 로그 · 사용자
- 상단 — 현재 프로젝트 표시, 개발자 이메일, 로그아웃
- 대시보드 — 기능 카탈로그 카드(무엇을 할 수 있는지와 콘솔 지원 여부). 프로젝트를 고르면 각 기능을 그 프로젝트 기준으로 열 수 있습니다.
프로젝트 만들기와 프로젝트 ID
- 프로젝트 화면에서 이름(1~100자)을 입력하고 + 생성을 누릅니다.
- 목록에서 프로젝트를 선택하면 이후 화면이 모두 그 프로젝트 기준으로 동작합니다.
- 프로젝트의 ID(UUID) 가 SDK의
tenantId이자 API의X-Tenant-Id값입니다.
참고 현재는 프로젝트별 API 키를 발급하지 않습니다. 앱(클라이언트)은 프로젝트 ID와 앱 사용자의 로그인 토큰(JWT)으로 API를 호출하고, 관리 작업은 콘솔(개발자 토큰)에서 합니다. 공개 전 설계상 API 키 방식은 후속 과제로 남아 있습니다. 프로젝트 삭제·이관 기능도 아직 없습니다.
사용자 화면
프로젝트에 가입한 앱 사용자를 상태별로 조회하고 관리합니다.
- 승인 / 거절 — 사전 승인 모드에서 대기 중인 가입자 처리
- 차단 / 해제 — 계정 사용 중지와 복구
- 비밀번호 초기화 — 사용자에게 재설정 메일을 강제로 발송(임시 비밀번호는 만들지 않음)
- 탈퇴 처리 — 소프트 삭제. 기본 30일 유예 후 이메일이 익명화되어 같은 이메일로 재가입할 수 있게 됩니다
데이터 화면
컬렉션을 만들고 레코드를 직접 넣어 볼 수 있는 플레이그라운드입니다.
- 컬렉션 이름은
[a-z0-9_-]문자로 구성합니다. - 생성 시 접근 규칙(
rules), 오프라인 동기화(sync)와 충돌 정책, 유니크 제약(uniqueBy)을 지정합니다. - 레코드는 JSON으로 편집해 생성·삭제합니다. 컬렉션을 삭제하면 레코드 전체가 함께 삭제됩니다.
웹훅 화면
수신 URL, 받을 이벤트, 형식(generic·slack·telegram·kakaowork)을 골라 등록합니다. 텔레그램은 chatId가 추가로 필요합니다. 아래쪽 발송 이력에서 상태·시도 횟수를 확인하고, 실패한 건을 재발송 대기열에 다시 넣을 수 있습니다. 자세한 내용은 웹훅을 참고하세요.
소셜 로그인(BYO) 화면
프로젝트 전용 Google OAuth 자격증명을 등록하면 Google 동의 화면에 개발자의 앱 이름이 표시됩니다.
- Google Cloud Console에서 OAuth 클라이언트를 만듭니다.
- 콘솔에 앱 이름, Client ID, Client Secret을 등록합니다.
- 등록 응답에 표시되는 콜백 URI를 Google Cloud Console의 승인된 리디렉션 URI에 추가합니다.
자격증명을 삭제하면 서버 공용 자격증명(설정되어 있는 경우)으로 되돌아갑니다. 서버에 자격증명 암호화 키(CREDENTIALS_ENCRYPTION_KEY)가 설정되어 있어야 등록할 수 있습니다.
사용량 · 로그 화면
- 사용량 — 데이터·파일·동기화 호출의 일 단위 집계를 7·30·90일 범위로 봅니다.
- 로그 — 카테고리(
auth·admin·user·data·file·reminder·hook)별 감사 로그 기록을 켜고 끄며, 기간·사용자로 조회합니다.auth·admin은 보안 감사용이라 끌 수 없습니다.
파일 · 리마인더 화면
두 기능은 앱 사용자 토큰으로만 호출하는 기능이라 콘솔에는 안내만 있습니다. SDK나 REST API로 시험하세요.
클라이언트 SDK
설치와 초기화
@nexa/sdk는 웹·React Native·Node에서 동작하는 TypeScript 클라이언트로, 런타임 의존성이 없습니다(fetch와 crypto만 사용).
주의 SDK는 아직 npm에 배포되지 않았습니다. 현재는 저장소 안의 워크스페이스 패키지로만 사용할 수 있으며, npm 배포는 서비스 공개 시점으로 미뤄져 있습니다.
import { createClient } from '@nexa/sdk';
const nexa = createClient({
baseUrl: 'https://api.example.com', // '/api/v1' 접두어는 SDK가 붙입니다
tenantId: '<프로젝트 ID>',
// store: 지정하지 않으면 메모리 저장(앱 종료 시 사라짐)
// syncIntervalMs: 30_000, // sync 컬렉션 자동 동기화 주기(선택)
});
로그인과 세션
await nexa.auth.login('[email protected]', 'password');
nexa.auth.onAuthLost(() => {
// 서버가 세션을 거부했을 때 — 로그인 화면으로 유도
});
await nexa.auth.logout();
- 토큰은 지정한 스토어에 저장되어 앱을 다시 시작해도 복원됩니다.
- API가 401을 돌려주면 SDK가 토큰을 한 번 갱신한 뒤 원래 요청을 다시 보냅니다.
- 네트워크가 끊겨 갱신에 실패한 경우는 세션 상실로 보지 않습니다.
onAuthLost는 서버가 세션을 거부했을 때만 호출됩니다. - 앱 사용자 가입은 SDK 메서드가 아니라 REST API로 진행합니다(인증 참고).
주의 MFA가 켜진 계정은 SDK
login()이AUTH_MFA_REQUIRED오류를 던집니다. 현재 SDK는 MFA 로그인 단계를 지원하지 않습니다.
온라인 컬렉션
const memos = nexa.collection<{ title: string; done?: boolean }>('memos');
const created = await memos.create({ title: '첫 메모' });
await memos.update(created.id, { done: true }); // 최상위 키 단위 병합
const page = await memos.list({
filter: { done: true, 'meta.count': { gte: 3 }, tag: { in: ['a', 'b'] } },
sort: '-updated_at',
page: 1,
pageSize: 20,
});
await memos.remove(created.id);
모든 호출은 즉시 서버로 갑니다. 실패하면 서버 오류 코드가 담긴 NexaError(code, message, status)가 던져집니다.
오프라인 우선(sync) 컬렉션
서버에서 컬렉션을 sync: true로 만들어 두어야 합니다.
const words = nexa.collection<{ term: string; memo?: string }>('words', {
sync: true,
onConflict: (r) => console.log('서버가 병합/거부한 항목', r.id, r.error),
});
const w = await words.put({ term: 'apple' }); // 로컬에 즉시 저장, id는 UUID 자동 생성
const items = await words.all(); // 로컬 스냅샷
const off = words.subscribe((items) => render(items));
const { pushed, pulled, resetOccurred } = await words.sync(); // 내 변경 올리기 → 남의 변경 받기
- 읽기·쓰기는 모두 로컬에서 즉시 처리되어 오프라인에서도 그대로 동작합니다.
- 같은 레코드를 여러 번 고쳐도 올릴 변경은 1건으로 합쳐집니다.
- 충돌은 서버의 충돌 정책 결과를 그대로 따릅니다. 병합·거부된 항목은 서버 값으로 바뀌고
onConflict로 알려 줍니다. - 동기화는 수동
sync(),syncIntervalMs주기, 브라우저online이벤트 세 가지로 시작됩니다. 앱 종료 시에는nexa.dispose()를 호출하세요.
영속 스토어
기본 메모리 스토어는 프로세스가 끝나면 사라집니다. 앱을 다시 켜도 로그인·오프라인 데이터를 유지하려면 스토어를 주입합니다.
// 웹 — IndexedDB
import { createClient, IndexedDBLocalStore } from '@nexa/sdk';
const nexa = createClient({ baseUrl, tenantId, store: new IndexedDBLocalStore() });
// React Native / Node — 원하는 SQLite 라이브러리를 2개 메서드로 감싸 주입
import { createClient, SqlLocalStore, type ISqlDriver } from '@nexa/sdk';
import * as SQLite from 'expo-sqlite';
const db = await SQLite.openDatabaseAsync('nexa.db');
const driver: ISqlDriver = {
run: async (sql, params) => void (await db.runAsync(sql, ...(params as []))),
all: async (sql, params) => db.getAllAsync(sql, ...(params as [])),
};
const nexa = createClient({ baseUrl, tenantId, store: new SqlLocalStore(driver) });
파일 · 리마인더 헬퍼
// 파일 — 서버를 거치지 않는 직접 업로드. 32MiB를 넘으면 멀티파트로 자동 전환
const docs = nexa.files('docs');
const file = await docs.upload(blob, { name: 'note.txt' });
window.open(file.downloadUrl); // 5분짜리 서명 URL
await docs.list();
await docs.remove(file.id);
// 기기 등록 + 리마인더 예약
await nexa.devices.register(fcmToken, 'fcm');
const r = await nexa.reminders.schedule({
sendAt: new Date(Date.now() + 60_000),
payload: { title: '복습할 시간' },
});
await nexa.reminders.cancel(r.id);
서비스별 사용법
공통 규칙
- 경로 접두어:
/api/v1(헬스체크/health,/health/ready제외) - 프로젝트 지정: 로그인 토큰이 있으면 토큰의 프로젝트, 없으면
X-Tenant-Id헤더 - 인증:
Authorization: Bearer <access token> - 응답 형식: 성공은
{ "success": true, "data": … }(목록이면meta포함), 실패는{ "success": false, "error": { "code", "message" } } - 모든 응답에 추적용
X-Trace-Id헤더가 붙습니다.
인증
| 기능 | 경로 |
|---|---|
| 가입 신청 → 검증 → 완료 | POST /auth/signup/request → POST /auth/signup/verify → POST /auth/signup/complete |
| 로그인 / MFA 2단계 | POST /auth/login, POST /auth/login/mfa |
| 토큰 갱신 / 로그아웃 | POST /auth/refresh, POST /auth/logout |
| 비밀번호 재설정 | POST /auth/password/reset/request → /verify → /complete |
| 내 정보 | GET·PATCH /me, POST /me/password, POST /me/password/set |
| MFA(TOTP) | POST /me/mfa/totp/enroll → POST /me/mfa/totp/activate, DELETE /me/mfa/totp |
| Google 로그인 | GET /auth/oauth/… (서버 또는 프로젝트별 Google 자격증명 필요) |
- 이메일 먼저 검증하는 방식이라, 메일 속 링크를 확인하기 전에는 계정이 만들어지지 않습니다. 검증 링크는 기본 10분간 유효합니다.
- 승인 모드는 프로젝트별로 즉시 사용(
AUTO_FULL) · 기본 사용 후 일부 권한만 승인(AUTO_BASIC) · 관리자 사전 승인(PRE_APPROVAL) 세 가지입니다. - 비밀번호는 강도 검사와 유출 비밀번호 목록 대조를 거칩니다.
- 액세스 토큰 기본 수명은 15분, 리프레시 토큰은 14일이며(서버 설정으로 변경 가능), 리프레시 토큰은 사용할 때마다 교체되고 재사용이 감지되면 모든 세션이 무효화됩니다.
- Google 계정은 이메일이 검증된 경우에만 같은 이메일의 기존 계정에 자동 연결됩니다.
사용자 · 그룹 · 권한
관리 API는 콘솔 개발자 토큰(Authorization + 대상 프로젝트 X-Tenant-Id)으로 호출합니다.
- 사용자:
/admin/users— 목록·상세·수정, 승인·차단·해제·삭제, 비밀번호 초기화 메일 - 그룹:
/admin/groups— CRUD,POST /admin/groups/:id/members,DELETE /admin/groups/:id/members/:userId - 권한:
/admin/permissions(정의), 그룹 부여, 사용자별 허용·거부 오버라이드 - 권한 키 형식은
resource:action이며, 평가 순서는 사용자 거부 > 사용자 허용 > 그룹 부여 > 기본 거부입니다.
데이터 API
컬렉션 만들기(관리 API 또는 콘솔 데이터 화면):
curl -X POST http://localhost:3000/api/v1/admin/collections \
-H 'authorization: Bearer <개발자 토큰>' -H 'x-tenant-id: <프로젝트 ID>' \
-H 'content-type: application/json' \
-d '{"name":"memos","rules":{"read":["owner"],"create":["authenticated"],"update":["owner"],"delete":["owner"]}}'
접근 규칙의 주체(principal)는 네 가지입니다.
| 주체 | 의미 |
|---|---|
public | 로그인하지 않은 요청도 허용(읽기 공개용) |
authenticated | 로그인한 앱 사용자 |
owner | 레코드를 만든 사용자 본인. 목록 조회 시 본인 레코드만 자동으로 걸러집니다 |
permission:<key> | RBAC 권한을 가진 사용자 |
- 액션은
read·create·update·delete이며, 규칙에 적지 않은 액션은 거부됩니다. - 레코드 API:
POST·GET /data/:collection,GET·PATCH·DELETE /data/:collection/:id - 목록 쿼리:
filter[field][op]=value(연산자eq·ne·gt·gte·lt·lte·in·like, 점 표기 경로 가능),sort=-updated_at,page·pageSize(기본 20, 최대 100) - 쓰기는 항상 로그인이 필요합니다. 로그인 없이 호출하려면
X-Tenant-Id헤더가 필요하고public읽기만 허용됩니다. uniqueBy: ["term"]처럼 최상위 필드 1~3개 조합의 중복을 막을 수 있습니다(위반 시DATA_006).- 한도: 레코드 64KB, 프로젝트당 컬렉션 100개, 필터 10개. 레코드 삭제는 즉시 영구 삭제입니다.
파일 스토리지
- 버킷 관리:
/admin/file-buckets— 규칙은read(다운로드)·create(업로드)·delete - 업로드 흐름:
POST /files/:bucket(업로드 준비, 15분짜리 업로드 URL 발급) → 클라이언트가 스토리지로 직접 PUT →POST /files/:bucket/:id/complete(크기 대조 후 확정) - 조회:
GET /files/:bucket/:id(5분짜리 다운로드 URL 포함),GET /files/:bucket(목록),DELETE /files/:bucket/:id - 대용량: 준비 요청에
multipart: true(최대 5GiB, 8MiB 단위 파트). 파트 URL을 다시 받으면 이어서 올릴 수 있고,abort로 중단합니다. - 단일 업로드는 32MiB까지입니다. 24시간 넘게 완료되지 않은 업로드는 자동 정리됩니다.
리마인더(예약 푸시)
- 기기:
POST /me/devices(같은 토큰은 갱신),GET /me/devices,DELETE /me/devices/:id - 예약:
POST /reminders { sendAt, payload: { title, body, data } }—sendAt은 지금부터 30초 뒤 ~ 1년 이내 - 조회·취소:
GET /reminders?status=,DELETE /reminders/:id(예약 상태일 때만) - 제목 100자, 본문 500자, data 4KB, 사용자당 대기 예약 500건이 한도입니다.
- Redis가 있어야 예약이 실제로 발송됩니다. 실제 푸시 발송은 FCM 설정(
PUSH_PROVIDER=fcm)이 필요하며, 기본값은 서버 로그에만 기록하는fake입니다.
오프라인 동기화
- 컬렉션을
sync: true로 만들고 충돌 정책(conflictPolicy)을 고릅니다.
| 정책 | 동작 |
|---|---|
lww(기본) | 나중에 올라온 쓰기가 이깁니다 |
fieldMerge | 최상위 필드 단위로 병합합니다 |
serverWins | 충돌한 쓰기를 거부하고 서버 값을 돌려줍니다 |
- 받기:
GET /sync/:collection?since=<seq>&limit=(기본 200, 최대 1000) - 올리기:
POST /sync/:collection/push(한 번에 최대 200건, 항목별 부분 성공) — 레코드 id는 클라이언트가 만든 UUID v4 - 삭제는 표식(툼스톤)으로 전파되며 90일 뒤 정리됩니다. 너무 오래된 위치에서 받기를 요청하면
resetRequired: true가 와서 전체를 다시 받습니다(SDK가 자동 처리). - 동기화는 로그인이 필수이며, SDK를 쓰면 위 프로토콜을 직접 다룰 필요가 없습니다.
웹훅
- 등록:
/admin/webhooks(프로젝트당 10개). 등록 시 서명용 시크릿이 한 번만 표시되니 바로 보관하세요. - 이벤트:
record.created·record.updated·record.deleted·user.registered·file.uploaded·file.deleted·reminder.sent generic형식은X-Nexa-Signature: sha256=<본문의 HMAC-SHA256>서명과X-Nexa-Event, 중복 처리용X-Nexa-Delivery헤더를 붙여 보냅니다. 수신 서버에서 서명을 검증하세요.- 실패하면 1분 → 5분 → 30분 → 2시간 → 12시간 간격으로 재시도하고, 5회를 넘으면 실패로 확정됩니다. 이력은
GET /admin/webhooks/events, 재발송은POST /admin/webhooks/events/:id/retry. - 발송은 Redis 기반 작업자가 약 1분 주기로 처리합니다. Redis가 없으면 대기 상태에 머뭅니다.
플랜 한도 · 사용량 · 로그
- 플랜별 한도(현재 서버가 차단하는 항목은 가입자 수와 스토리지 용량뿐입니다):
| 플랜 | 가입자 수 | 스토리지 |
|---|---|---|
| free | 100명 | 1GiB |
| starter | 1,000명 | 10GiB |
| growth | 10,000명 | 100GiB |
| enterprise | 무제한 | 무제한 |
- 사용량:
GET /console/projects/:tenantId/usage?from=&to=(기본 최근 30일) - 로그:
GET /admin/logs, 설정은GET·PUT /admin/logs/settings - 주요 API에는 분당 120회(프로젝트 × 사용자) 수준의 요청 제한이 있으며, 초과하면
CMN_004가 반환됩니다.
주요 오류 코드
| 코드 | 의미 |
|---|---|
AUTH_002 | 이메일 미인증 또는 토큰 무효·만료 |
AUTH_003 | 잘못된 인증 정보 |
USER_002 | 권한 없음(접근 규칙·RBAC 거부) |
DATA_001 / DATA_005 | 컬렉션 없음 / 레코드 없음 |
DATA_004 | 레코드 크기 초과(64KB) |
DATA_006 | 유니크 제약 위반 |
FILE_004 / FILE_005 | 파일 크기 초과 / 업로드 상태 오류 |
RMD_002 / RMD_003 | 발송 시각 범위 위반 / 취소할 수 없는 상태 |
SYNC_001 | 동기화가 켜지지 않은 컬렉션 |
CON_001 / CON_002 | 콘솔 계정이 아님 / 프로젝트 없음 또는 소유하지 않음 |
CMN_002 / CMN_004 | 한도 초과 / 요청 과다 |
자체 호스팅
주의 아래 절차는 저장소 문서에 정리된 개발·검증용 실행 방법입니다. 운영 배포 가이드(TLS, 백업, 확장 구성 등)는 아직 없으며, 저장소가 공개되기 전까지는 저장소 접근 권한이 있어야 따라 할 수 있습니다.
준비물
| 준비 수준 | 필요한 것 | 할 수 있는 것 |
|---|---|---|
| 최소 | Node.js 22, pnpm(corepack) | 빌드·테스트, 서버 기동과 헬스체크 |
| 기본 | + PostgreSQL 접속 정보 | 콘솔·SDK로 대부분의 기능 사용 |
| 전체 | + Docker(PostgreSQL·Redis·MinIO) | 예약 푸시·웹훅 발송 등 큐 기능 |
| 실서비스 | + 메일·FCM·Google 자격증명 | 실제 메일·푸시·소셜 로그인 |
설치와 기동
corepack enable pnpm # 최초 1회
pnpm install
docker compose up -d postgres redis minio # 로컬 인프라
cp apps/api/.env.example apps/api/.env # 환경 변수 준비 후 값 채우기
pnpm --filter @nexa/shared build # 최초 1회
pnpm --filter @nexa/api migrate # DB 마이그레이션 적용
pnpm dev # API·콘솔·예제 앱 동시 실행
- 개별 실행:
pnpm --filter @nexa/api dev(API :3000),pnpm --filter nexa-console dev(콘솔 :5174),pnpm --filter example-words dev(예제 앱 :5173) - 헬스체크:
GET /health(동작 여부),GET /health/ready(DB 연결 확인) - API까지 컨테이너로 띄우려면
docker compose --profile full up -d를 사용합니다. - 비밀번호 유출 목록을 쓰려면
pnpm --filter @nexa/api seed:breached로 미리 적재합니다.
주요 환경 변수
| 변수 | 설명 |
|---|---|
DATABASE_URL | PostgreSQL 접속 문자열(DB 기능에 필수) |
JWT_SECRET | 토큰 서명 키(16자 이상, 운영에서 필수) |
ACCESS_TOKEN_TTL_SECONDS / REFRESH_TOKEN_TTL_SECONDS | 토큰 수명(기본 900초 / 14일) |
APP_BASE_URL | 인증 메일 링크에 쓰는 기준 주소 |
MAIL_PROVIDER · MAIL_FROM | 메일 발송 방식(fake 기본 · resend)과 발신 주소 |
REDIS_URL | 큐 기능(예약 푸시·웹훅·유지보수 작업) |
STORAGE_ENDPOINT · STORAGE_ACCESS_KEY · STORAGE_SECRET_KEY · STORAGE_BUCKET · STORAGE_REGION | S3 호환 오브젝트 스토리지 |
PUSH_PROVIDER · FCM_SERVICE_ACCOUNT_JSON | 푸시 발송 방식(fake 기본 · fcm)과 FCM 서비스 계정(base64) |
GOOGLE_CLIENT_ID · GOOGLE_CLIENT_SECRET | 서버 공용 Google 로그인(미설정 시 비활성) |
ADMIN_API_KEY | 운영자용 관리 API 키(16자 이상, X-Admin-Key 헤더) |
PLATFORM_TENANT_ID | 개발자 계정용 플랫폼 테넌트 ID |
주의
.env.example에는 스토리지 설정이MINIO_*이름으로 적혀 있지만, 서버가 실제로 읽는 이름은 위 표의STORAGE_*입니다. 파일 기능을 쓰려면STORAGE_*로 작성하세요.
주의 키·비밀번호 같은 비밀값은 저장소에 커밋하지 말고 환경 변수나 Docker secrets로만 관리하세요. 이 밖에 MFA 비밀 암호화용
MFA_MASTER_KEY(권장)와 BYO OAuth 자격증명 암호화용CREDENTIALS_ENCRYPTION_KEY(32바이트 base64)를 설정합니다.
버전 변경 이력
버전 흐름 요약
정식 버전 번호는 아직 없습니다(패키지 버전 0.0.0). 아래는 날짜별 개발 이정표입니다.
| 날짜 | 이정표 |
|---|---|
| 2026-07-28 | 실행·테스트 종합 가이드, 구현·미구현 경계 정리 |
| 2026-07-16 | 실제 DB·Redis 컨테이너 통합 테스트 체계 |
| 2026-07-15 | 개발자 콘솔 탐색 화면 완성(대시보드·데이터·웹훅·OAuth·사용량) |
| 2026-07-14 | 웹훅·커넥터, 플랜 한도, 사용량 미터링, BYO OAuth, 로그 관리 |
| 2026-07-13 | 개발자 콘솔·프로젝트, 예제 앱 Nexa Words, 유니크 제약 |
| 2026-07-12 | 데이터 API, 파일 스토리지, 리마인더, 오프라인 동기화, SDK |
| 2026-07-11 | 로그인·토큰, MFA, Google 로그인, 사용자·그룹·RBAC |
| 2026-07-10 | 이메일 인증 기반 가입 |
| 2026-06-23 | 프로젝트 시작(모노레포·서버 골격) |
2026-07-28 — 문서 정비
추가
- 준비 수준별 실행·테스트 종합 가이드
- 기능별 구현·미구현 경계 목록
2026-07-16 — 통합 테스트
추가
- 실제 PostgreSQL·Redis 컨테이너로 마이그레이션·테넌트 격리·트랜잭션·큐 왕복을 검증하는 통합 테스트와 CI 작업
2026-07-15 — 콘솔 탐색 화면
추가
- 콘솔 사이드바·프로젝트 전환·기능 카탈로그 대시보드
- 데이터 플레이그라운드(컬렉션·레코드 CRUD)
- 웹훅 등록·발송 이력·재발송, BYO OAuth 자격증명 화면
- 사용량 대시보드(7·30·90일)
개선
- 반응형 레이아웃과 접근성 정리
2026-07-14 — 운영 기능
추가
- 웹훅 Outbox 발행·HMAC 서명 발송·재시도, 파일·리마인더·동기화 이벤트, 발송 이력과 재발송
- Slack·Telegram·카카오워크 커넥터
- 플랜 한도(가입자 수·스토리지) 자동 차단
- 사용량 일 단위 집계와 콘솔 조회
- 프로젝트별 Google OAuth 자격증명(암호화 저장)
- 카테고리별 감사 로그 설정·조회
- SDK 파일·리마인더 헬퍼, 대용량 파일 멀티파트 자동 전환
수정
- 사용량 조회의 날짜 하루 어긋남 문제
2026-07-13 — 개발자 콘솔
추가
- 개발자 계정(플랫폼 테넌트)과 프로젝트 생성·목록·상세
- 콘솔 개발자 토큰으로 관리 API 사용, 관리자 비밀번호 초기화 메일
- 콘솔 웹 UI(로그인·프로젝트·사용자·로그)
- 예제 앱 Nexa Words(오프라인 우선 단어장)
- 컬렉션 유니크 제약(
uniqueBy)
수정
- 브라우저 환경에서의 SDK 결함
2026-07-12 — 데이터·파일·동기화·SDK
추가
- 데이터 API(선언적 접근 규칙·쿼리·RBAC 연동)
- 파일 스토리지(presigned 업로드·멀티파트 재개·목록·삭제)
- 리마인더(기기 등록·예약·발송 작업자·복구)
- 오프라인 동기화(델타 pull·배치 push·충돌 정책 3종·툼스톤 정리)
- 클라이언트 SDK(
@nexa/sdk)와 IndexedDB·SQLite 영속 스토어 - 탈퇴 계정 30일 유예 후 익명화
2026-07-11 — 인증 트랙 완성
추가
- 로그인·토큰 발급과 갱신(교체·재사용 탐지)·로그아웃
- 비밀번호 재설정·변경, 관리자 승인
- Google 소셜 가입·연동·로그인
/me설정과 MFA(TOTP)- 관리자 사용자 관리, 그룹·멤버십, RBAC
변경
- 모든 시각을 UTC로 통일, 전용 DB 스키마 도입
2026-07-10 — 가입
추가
- 이메일 먼저 검증하는 가입 절차, 비밀번호 강도·유출 검사, 승인 모드 3종
2026-06-23 — 프로젝트 시작
추가
- pnpm + Turborepo 모노레포, NestJS 서버 골격, Docker Compose 개발 인프라
- 공통 응답 형식·오류 코드·로깅, DB 연결과 마이그레이션(06-23~06-26)
알려진 제한
- 공개 배포 전 — 다운로드·호스팅 서비스·npm 패키지가 없고 저장소도 비공개입니다.
- 프로젝트별 API 키 없음 — 클라이언트는 프로젝트 ID와 사용자 토큰으로만 호출합니다. 프로젝트 삭제·이관, 팀원 초대도 아직 없습니다.
- 콘솔 가입 화면 없음 — 개발자 계정은 가입 API로 만들어야 합니다.
- SDK — MFA 로그인과 가입 메서드를 지원하지 않습니다.
- 콘솔의 파일·리마인더 화면 — 앱 사용자 전용 기능이라 콘솔에서는 시험할 수 없습니다.
- 소셜 로그인은 Google만 지원합니다.
- 결제·구독 청구 미구현 — 플랜 한도는 가입자 수와 스토리지 용량만 차단합니다.
- 실시간 기능 없음 — 동기화는 주기·수동 방식이며 WebSocket 실시간 구독, GraphQL은 없습니다.
- 웹훅은 발송만 지원합니다(외부 서비스 웹훅 수신 없음, 시크릿 회전은 후속).
- 요청 제한이 서버 프로세스별로 계산되어, 서버를 여러 대 띄우면 실제 한도가 늘어납니다.
/health/ready는 DB만 확인하고 Redis·스토리지는 점검하지 않습니다.MAIL_PROVIDER를resend로 지정하지 않으면 운영 모드에서도 메일이 실제로 발송되지 않습니다.- PostgreSQL 외 데이터베이스는 아직 동작하지 않습니다.
- 실제 자격증명이 필요한 종단 검증(FCM 푸시, 웹훅 실발송, 프로젝트별 Google 동의 화면)과 약관·개인정보처리방침 법률 검토가 남아 있습니다.