앱 코드
defineApp, 실행 문맥(ctx), 이벤트 함수와 HTTP 핸들러, 외부 호출과 실행 한도, 로그
앱 코드는 src/server.ts 하나에서 시작하는 TypeScript 모듈입니다. 웹 표준 API(fetch·Request·Response·crypto 등)로 실행되며
Node.js 전용 모듈(fs·net·child_process)은 쓸 수 없습니다. 패키지는 package.json에 적고 npm install하면 deploy가 번들에 넣습니다.
import { defineApp } from "@sayren/app";
export default defineApp({
events: {
"ORDER.PAID": async (event, ctx) => {
/* … */
},
},
http: {
"GET /summary": async (request, ctx) => Response.json({ ok: true }),
"POST /settings/test": async (request, ctx) => new Response(null, { status: 204 }),
},
});events는 매니페스트 events에 적은 이벤트마다 함수 하나입니다. 함수가 없는 이벤트가 있으면 빌드가 HANDLER_MISSING으로 실패합니다.
http는 상점 플랫폼 앱 화면이 부르는 핸들러입니다. 키는 "<메서드> <경로>"이고 경로는 /로 시작합니다. 경로 매개변수(/orders/:id)와
쿼리는 request.url에서 읽습니다.
실행 문맥 ctx
| 속성 | 뜻 |
|---|---|
ctx.installation | installationId · appId · versionNumber · storeId · storeName |
ctx.api | 설치된 상점의 Store SDK 클라이언트. 동의한 권한으로 관리 API를 부릅니다. 토큰은 sayren이 호출마다 넣습니다 |
ctx.settings | 셀러가 채운 설정값(Record<string, string | number | boolean | null>). 비밀 필드도 들어 있습니다 |
ctx.secrets | 개발자 비밀 값(Record<string, string>) |
ctx.user | HTTP 핸들러에서만. 앱 화면을 연 사람의 userId와 역할(OWNER · ADMIN). 이벤트 함수에는 없습니다 |
ctx.log | info·warn·error. console.*와 같이 로그에 남습니다 |
ctx.invocationId | 이 호출의 id. 재시도는 같은 값으로 옵니다 |
ctx.token | 설치 토큰 문자열. ctx.api 대신 직접 fetch로 관리 API를 부를 때 Authorization: Bearer에 싣습니다. 설치 해제·데이터 삭제 호출에서는 null입니다 |
ctx.api는 상점이 고정된 클라이언트라 X-Store-Id를 넣을 필요가 없습니다. 권한 밖의 호출은 403 INSUFFICIENT_SCOPE입니다. 구성원·결제 설정처럼
상점 플랫폼에서만 하는 작업은 권한과 무관하게 403입니다.
이벤트 함수
"ORDER.PAID": async (event, ctx) => {
// event: { id, type: "ORDER.PAID", createdAt, storeId, sequence, data: { orderId, orderNo, … } }
}event는 웹훅과 같은 모양입니다. data의 필드는 이벤트에 있습니다.
TypeScript에서는 함수 키가 이벤트 이름이면 event.data가 그 이벤트의 모양으로 좁혀집니다("ORDER.PAID"의 event.data.orderId는 string).
매니페스트에 쓸 수 없는 이름은 타입 오류입니다. 여러 이벤트를 한 함수에서 받으면 AppEvent 유니온을 event.type으로 좁힙니다.
import { type AppEvent, defineApp } from "@sayren/app";
export default defineApp({
events: {
"ORDER.PAID": async (event, ctx) => {
const order = await ctx.api.orders.get(event.data.orderId); // 캐스팅 없이
},
"APP.SETTINGS_UPDATED": async (event) => {
console.log("바뀐 설정", event.data.changedKeys);
},
},
});- 최소 한 번 전달됩니다. 함수가 예외를 던지거나 한도를 넘으면 뒤에 다시 실행됩니다(간격을 늘리며 최대 24시간). 같은 호출은 같은
ctx.invocationId로 오므로, 외부에 두 번 보내면 안 되는 작업은 이 값으로 중복을 막으십시오. - 순서는 보장하지 않습니다. 같은 주문의 이벤트도 먼저 생긴 것이 늦게 올 수 있습니다.
event.sequence(리소스별 번호)로 순서를 판단합니다. - 함수의 반환값은 쓰지 않습니다. 정상으로 끝나면 성공으로 기록됩니다.
- 필수 설정이 비어 있는 상점에는 이벤트가 오지 않습니다(실행 기록에 「설정 필요」로 남습니다).
HTTP 핸들러
"POST /notes": async (request, ctx) => {
const body = await request.json();
if (ctx.user?.role !== "OWNER") return new Response("소유자만", { status: 403 });
return Response.json({ saved: true });
}앱 화면의 코드가 @sayren/app/ui로 부릅니다(상점 플랫폼 앱 화면). 요청 본문은 1MB까지입니다. 응답은 그대로
앱 화면에 전달됩니다. 핸들러는 설치 토큰으로 동작하므로 사람의 역할로 권한이 좁혀지지 않습니다. 역할에 따라 막을 작업은 ctx.user.role로
직접 판단하십시오. 지금은 소유자·관리자만 앱 화면을 엽니다.
외부 호출
fetch는 매니페스트 network.hosts와 sayren API에만 나갑니다. 나머지는 403이고 로그에 OUTBOUND_BLOCKED로 남습니다.
https://·443 포트만 되고 리다이렉트를 따라가지 않습니다. WebSocket·TCP 연결은 쓸 수 없습니다.
실행 한도
| 항목 | 한도 |
|---|---|
| 호출당 CPU 시간 | 50ms. 외부 호출을 기다리는 시간은 세지 않습니다 |
| 호출당 외부·API 요청 수 | 50 |
| 호출당 실행 시간 | 30초 |
| 메모리 | 128MB |
| 앱 전체 동시 실행 | 20 |
| 상점(설치)당 이벤트 | 분당 300. 넘으면 뒤로 미룹니다 |
| 상점당 관리 API 호출 | 분당 120. 넘으면 429이고 Retry-After를 보고 다시 부릅니다 |
한도를 넘은 호출은 LIMIT_EXCEEDED로 기록되고 이벤트는 다시 실행됩니다. 앱이 부른 관리 API는 설치된 상점의 API 사용량으로 셉니다.
전역 상태
같은 앱 코드가 여러 상점의 호출을 처리합니다. 모듈 전역 변수는 호출 사이, 상점 사이에 공유될 수 있고 언제든 사라질 수 있습니다.
상점별 데이터는 ctx에서 읽고 전역에 두지 마십시오. 호출 사이에 남길 저장소는 아직 없습니다.
로그
console.log·ctx.log.*와 잡히지 않은 예외가 호출 단위로 기록됩니다. npx sayren app logs --tail 또는 developer.sayren.app › {앱} › 로그에서
7일 동안 봅니다. 설치 토큰과 비밀 설정·비밀 값이 로그에 섞이면 가려서 저장합니다. 그래도 비밀 값을 로그에 찍지 마십시오.
셀러에게는 로그 본문이 보이지 않고 호출 결과만 보입니다.
패키지
@sayren/app—defineApp과ctx타입.@sayren/app/ui는 앱 화면용,@sayren/app/testing은 로컬 테스트용입니다.@sayren/store-sdk—ctx.api의 타입. 직접 설치하지 않아도 됩니다.- 그 밖의 패키지는
package.json에 적습니다. 알려진 보안 취약점이 있는 패키지, 레지스트리 밖(git·파일) 의존성은 올리기가 거절합니다(pnpm-lock.yaml이 있으면 함께 검사합니다). 서버 번들은 3MB(gzip)까지입니다.