@mandujs/mcp 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,300 @@
1
+ ---
2
+ title: Use Supabase as Postgres provider
3
+ impact: MEDIUM
4
+ impactDescription: Managed Postgres + optional Realtime / Storage / Auth — Mandu consumes it as a connection string, not a deploy target
5
+ tags: database, postgres, supabase, baas
6
+ ---
7
+
8
+ ## Use Supabase as Postgres provider
9
+
10
+ > **Supabase 는 Mandu 의 배포 타겟이 아닙니다.** Mandu 앱은 Render / Fly / Railway /
11
+ > Vercel / Docker 등으로 배포하고, Supabase 는 **Postgres 제공자 + 선택적
12
+ > BaaS 기능** (Realtime, Storage, Auth, Edge Functions) 으로만 사용합니다.
13
+ > Mandu 의 `@mandujs/core/db` 가 `Bun.SQL` wrapper 이기 때문에 Supabase 의
14
+ > pooler URL 을 그대로 꽂으면 별도 SDK 없이 바로 동작합니다.
15
+
16
+ ## 1. 가장 단순한 경로 — DB-only (권장)
17
+
18
+ Supabase 를 Postgres 로만 쓰는 경우. Mandu 의 네이티브 DB 레이어가 그대로 커버합니다.
19
+
20
+ ### 1.1 Connection string 획득
21
+
22
+ Supabase Dashboard → Settings → Database → **Connection pooling**
23
+ - `Transaction` 모드 URL 복사 (포트 `6543`)
24
+ - 형식: `postgres://postgres.<project-ref>:<password>@aws-0-<region>.pooler.supabase.com:6543/postgres`
25
+
26
+ ### 1.2 `.env` 에 저장
27
+
28
+ ```bash
29
+ DATABASE_URL=postgres://postgres.xxxx:your-password@aws-0-us-east-1.pooler.supabase.com:6543/postgres
30
+ ```
31
+
32
+ ### 1.3 Mandu DB 핸들 생성
33
+
34
+ ```typescript
35
+ // lib/db.ts
36
+ import { createDb } from "@mandujs/core/db";
37
+
38
+ export const db = createDb({
39
+ url: process.env.DATABASE_URL!,
40
+ });
41
+ ```
42
+
43
+ ### 1.4 Filling 에서 쿼리
44
+
45
+ ```typescript
46
+ // app/users/[id]/route.ts
47
+ import { defineFilling, notFound } from "@mandujs/core/filling";
48
+ import { db } from "@/lib/db";
49
+
50
+ export const filling = defineFilling({
51
+ async loader({ params }) {
52
+ const user = await db.one<{ id: string; email: string; name: string }>`
53
+ SELECT id, email, name FROM users WHERE id = ${params.id}
54
+ `;
55
+ if (!user) throw notFound();
56
+ return { user };
57
+ },
58
+ });
59
+ ```
60
+
61
+ 트랜잭션:
62
+
63
+ ```typescript
64
+ await db.transaction(async (tx) => {
65
+ const user = await tx.one<{ id: string }>`
66
+ INSERT INTO users (email, name) VALUES (${email}, ${name}) RETURNING id
67
+ `;
68
+ await tx`INSERT INTO profiles (user_id, bio) VALUES (${user!.id}, ${bio})`;
69
+ });
70
+ ```
71
+
72
+ **필요한 Supabase-specific 의존성**: 없음. `@mandujs/core/db` 만 사용. Supabase 가 바뀌어도 connection string 교체뿐.
73
+
74
+ ## 2. 마이그레이션
75
+
76
+ Mandu 의 기본 마이그레이션 러너를 사용하거나 (`@mandujs/core/db/migrations`), Supabase CLI 를 병행할 수 있습니다.
77
+
78
+ ### Mandu 마이그레이션 (권장)
79
+
80
+ ```sql
81
+ -- migrations/001_users.sql
82
+ CREATE TABLE users (
83
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
84
+ email TEXT UNIQUE NOT NULL,
85
+ name TEXT,
86
+ created_at TIMESTAMPTZ DEFAULT NOW()
87
+ );
88
+ ```
89
+
90
+ ```bash
91
+ bun run mandu db migrate
92
+ ```
93
+
94
+ ### Supabase CLI 병행
95
+
96
+ RLS (Row Level Security) / policies 등 Supabase-native 기능이 필요한 경우:
97
+
98
+ ```bash
99
+ npm install -g supabase
100
+ supabase login
101
+ supabase init
102
+ supabase migration new create_users_table
103
+ # edit supabase/migrations/<timestamp>_create_users_table.sql
104
+ supabase db push
105
+ ```
106
+
107
+ `ALTER TABLE users ENABLE ROW LEVEL SECURITY;` 등은 plain SQL 이므로 Mandu 마이그레이션에도 그대로 작성 가능. Supabase CLI 는 RLS policy 편집 UX 와 타입 생성 (`supabase gen types typescript`) 때문에 편리합니다.
108
+
109
+ ## 3. Supabase SDK 를 함께 쓰고 싶다면 (BaaS 기능)
110
+
111
+ DB 는 `@mandujs/core/db` 로, Supabase-specific 기능 (Realtime / Storage / Edge Functions) 만 SDK 로 사용하는 하이브리드가 깔끔합니다.
112
+
113
+ ### 3.1 클라이언트 설정
114
+
115
+ ```bash
116
+ bun add @supabase/supabase-js
117
+ ```
118
+
119
+ ```typescript
120
+ // lib/supabase.ts
121
+ import { createClient } from "@supabase/supabase-js";
122
+
123
+ const supabaseUrl = process.env.SUPABASE_URL!;
124
+ const supabaseAnonKey = process.env.SUPABASE_ANON_KEY!;
125
+
126
+ export const supabase = createClient(supabaseUrl, supabaseAnonKey);
127
+
128
+ // 서버 사이드 (Service Role) — filling 안에서만 사용
129
+ export function createServerSupabase() {
130
+ return createClient(supabaseUrl, process.env.SUPABASE_SERVICE_ROLE_KEY!, {
131
+ auth: { autoRefreshToken: false, persistSession: false },
132
+ });
133
+ }
134
+ ```
135
+
136
+ ### 3.2 Realtime (Island)
137
+
138
+ Mandu 의 client island 에서 Realtime 구독:
139
+
140
+ ```tsx
141
+ // app/messages/MessagesIsland.client.tsx
142
+ import { island } from "@mandujs/core/client";
143
+ import { useEffect, useState } from "react";
144
+ import { supabase } from "@/lib/supabase";
145
+
146
+ function Messages() {
147
+ const [messages, setMessages] = useState<Array<{ id: string; content: string }>>([]);
148
+
149
+ useEffect(() => {
150
+ supabase.from("messages").select("*").then(({ data }) => {
151
+ setMessages(data ?? []);
152
+ });
153
+
154
+ const channel = supabase
155
+ .channel("messages")
156
+ .on(
157
+ "postgres_changes",
158
+ { event: "INSERT", schema: "public", table: "messages" },
159
+ (payload) => {
160
+ setMessages((prev) => [...prev, payload.new as { id: string; content: string }]);
161
+ },
162
+ )
163
+ .subscribe();
164
+
165
+ return () => {
166
+ supabase.removeChannel(channel);
167
+ };
168
+ }, []);
169
+
170
+ return (
171
+ <ul>
172
+ {messages.map((m) => <li key={m.id}>{m.content}</li>)}
173
+ </ul>
174
+ );
175
+ }
176
+
177
+ export default island("visible", <Messages />);
178
+ ```
179
+
180
+ ### 3.3 Storage
181
+
182
+ ```typescript
183
+ // filling 내에서
184
+ const serverSupabase = createServerSupabase();
185
+
186
+ const { data, error } = await serverSupabase.storage
187
+ .from("avatars")
188
+ .upload(`${userId}/avatar.png`, file);
189
+
190
+ const { data: { publicUrl } } = serverSupabase.storage
191
+ .from("avatars")
192
+ .getPublicUrl(`${userId}/avatar.png`);
193
+ ```
194
+
195
+ ### 3.4 Edge Functions
196
+
197
+ Supabase 의 Deno 런타임에서 도는 서버리스 함수. Mandu 앱 바깥에서 별도 배포되고, Mandu 는 HTTP 로 호출만:
198
+
199
+ ```typescript
200
+ const res = await fetch(`${process.env.SUPABASE_URL}/functions/v1/hello`, {
201
+ method: "POST",
202
+ headers: {
203
+ "Authorization": `Bearer ${process.env.SUPABASE_ANON_KEY}`,
204
+ "Content-Type": "application/json",
205
+ },
206
+ body: JSON.stringify({ name: "mandu" }),
207
+ });
208
+ ```
209
+
210
+ ## 4. Supabase Auth 와 Mandu auth — 선택 문제
211
+
212
+ 두 가지가 겹치는 영역이라 **둘 중 하나만** 씁니다:
213
+
214
+ | Mandu 의 `@mandujs/core/auth` (native) | Supabase Auth |
215
+ |---|---|
216
+ | Bun.password (argon2id) | Bcrypt + custom hashing |
217
+ | `@mandujs/core/middleware/session` SQLite session store | JWT + cookie |
218
+ | CSRF (`@mandujs/core/middleware/csrf`) | — |
219
+ | Mandu 프로젝트 자체 사용자 테이블 | Supabase `auth.users` 테이블 |
220
+
221
+ **추천**: Mandu 앱이 Supabase 의 다른 기능 (Realtime / Storage) 을 안 쓰면 **Mandu native auth** 가 의존성 적고 예측 가능. Supabase 의 social providers (GitHub / Google OAuth UI) 가 필요하면 Supabase Auth 를 쓰고 Mandu auth 는 bypass.
222
+
223
+ 병행 사용 (Supabase Auth 만 이용하면서 Mandu filling 안에서 token 검증):
224
+
225
+ ```typescript
226
+ // middleware/supabase-auth.ts
227
+ import { createServerSupabase } from "@/lib/supabase";
228
+
229
+ export async function supabaseAuthMiddleware(ctx) {
230
+ const header = ctx.headers.get("authorization");
231
+ if (!header?.startsWith("Bearer ")) return ctx.unauthorized("Missing token");
232
+
233
+ const token = header.slice(7);
234
+ const supabase = createServerSupabase();
235
+ const { data: { user }, error } = await supabase.auth.getUser(token);
236
+ if (error || !user) return ctx.unauthorized("Invalid token");
237
+
238
+ ctx.set("user", user);
239
+ }
240
+ ```
241
+
242
+ ## 5. 환경 변수
243
+
244
+ ```bash
245
+ # .env
246
+ # DB connection (최소 요구사항)
247
+ DATABASE_URL=postgres://postgres.xxx:password@aws-0-us-east-1.pooler.supabase.com:6543/postgres
248
+
249
+ # Supabase SDK 사용 시 추가
250
+ SUPABASE_URL=https://xxx.supabase.co
251
+ SUPABASE_ANON_KEY=eyJ...
252
+ SUPABASE_SERVICE_ROLE_KEY=eyJ... # 서버 전용, 클라이언트에 노출 금지
253
+ ```
254
+
255
+ ## 6. 배포 플랫폼과 조합
256
+
257
+ Supabase 는 DB layer 이므로 **Mandu 앱 자체는 다른 곳** 에 배포합니다. `DATABASE_URL` 만 환경 변수로 주입:
258
+
259
+ ### Render (`mandu deploy --target=render` 가 자동 생성)
260
+
261
+ ```yaml
262
+ # render.yaml
263
+ services:
264
+ - type: web
265
+ name: mandu-app
266
+ runtime: node
267
+ buildCommand: |
268
+ curl -fsSL https://bun.sh/install | bash
269
+ export PATH="$HOME/.bun/bin:$PATH"
270
+ bun install --frozen-lockfile
271
+ bun run build
272
+ startCommand: bun run start
273
+ envVars:
274
+ - key: DATABASE_URL
275
+ sync: false # Render 대시보드에서 Supabase pooler URL 설정
276
+ - key: SUPABASE_URL
277
+ sync: false # SDK 쓸 때만
278
+ - key: SUPABASE_ANON_KEY
279
+ sync: false
280
+ ```
281
+
282
+ ### Fly / Railway
283
+
284
+ ```bash
285
+ fly secrets set DATABASE_URL="postgres://..." SUPABASE_URL="..." SUPABASE_ANON_KEY="..."
286
+ railway variables set DATABASE_URL="postgres://..." SUPABASE_URL="..." SUPABASE_ANON_KEY="..."
287
+ ```
288
+
289
+ ## 7. 주의사항
290
+
291
+ - **Transaction mode pooler (6543) 를 사용**하세요. Session mode (5432) 는 커넥션 고정이 필요한 경우에만.
292
+ - **Service Role key 는 서버 전용**. Client island 에서 절대 import 하지 마세요 — Mandu 의 island 번들에 포함되면 공개됩니다.
293
+ - **RLS 를 켜지 않은 테이블** 은 anon key 로 전체 읽기가 가능해집니다. 항상 `ENABLE ROW LEVEL SECURITY` 적용하고 policy 를 명시.
294
+ - **Realtime 구독은 client bundle 비용**이 큽니다 (`@supabase/supabase-js` ~60 KB). 페이지 단위로 island 분리하세요.
295
+
296
+ ## Reference
297
+
298
+ - [Supabase Docs](https://supabase.com/docs)
299
+ - `@mandujs/core/db` — Bun.SQL wrapper, provider-agnostic. `packages/core/src/db/index.ts`
300
+ - Deploy adapters — `packages/cli/src/commands/deploy/adapters/{render,fly,railway,vercel,netlify,docker,docker-compose,cf-pages}.ts`
package/src/server.ts CHANGED
@@ -29,7 +29,8 @@ import { registerBuiltinTools, getToolsSummary } from "./tools/index.js";
29
29
 
30
30
  // DNA-007: 에러 처리
31
31
  import { createToolResponse, logToolError } from "./executor/error-handler.js";
32
- import { ToolExecutor, createToolExecutor } from "./executor/tool-executor.js";
32
+ import type { ToolExecutor} from "./executor/tool-executor.js";
33
+ import { createToolExecutor } from "./executor/tool-executor.js";
33
34
 
34
35
  // DNA-008: 로깅 통합
35
36
  import { setupMcpLogging, teardownMcpLogging } from "./logging/mcp-transport.js";