저는 포인트 적립 기능을 만들다가 “이 API를 누가 호출해도 되나?”라는 질문에 막혔습니다. 클라이언트에서 직접 포인트 테이블을 만지면 보안이 너무 약했고, 서버에서 중복 로직을 반복하기도 싫었습니다. 그래서 Supabase RPC와 서비스 롤 키를 결합해 트랜잭션을 데이터베이스로 밀어 넣는 전략을 선택했습니다.
add_points)·차감( spend_points)을 SQL 함수로 정의하고 RPC로 호출합니다.createAdminClient()는 자동 세션 갱신과 쿠키 저장을 끄고 서비스 롤 키로 인증된 Supabase 클라이언트를 생성합니다. 키가 누락되면 즉시 예외를 던져 배포 단계에서 문제를 조기에 발견했습니다.
포인트를 적립·차감하는 함수는 모두 RPC를 호출하도록 만들었습니다.
import { createAdminClient } from './supabaseAdmin';
export const addUserPoints = async (
userId: string,
points: number,
validityDays: number,
sourceDescription?: string,
) => {
const supabase = createAdminClient();
const { data, error } = await supabase.rpc('add_points', {
p_user_id: userId,
p_points_to_add: points,
p_validity_days: validityDays,
p_source_description: sourceDescription,
});
if (error) throw error;
return { success: true, batchId: data };
};
포인트 잔액과 사용 내역은 보안 뷰에서 가져옵니다. 서비스 롤만 접근할 수 있으므로 RLS를 건드리지 않고도 안전하게 데이터를 확인할 수 있습니다.
points > 0 등)을 먼저 수행했습니다.지금은 포인트 관련 API를 전부 서버에서만 호출하도록 막아두어 프런트엔드는 단순히 엔드포인트만 부르면 됩니다. 비즈니스 로직이 데이터베이스 함수에 모여 있어 회계 정책이 바뀌면 SQL 한 군데만 수정하면 되는 점도 만족스럽습니다. 다음에는 RPC 호출에 감사 로그를 붙여 누가 언제 포인트를 조정했는지 추적할 예정입니다.
여러분은 포인트나 크레딧 시스템을 어떻게 설계하고 계신가요? 다른 접근법이 있다면 꼭 공유해 주세요. 특히 RLS와 서비스 롤을 조합하는 노하우가 궁금합니다.