# 데이터베이스 변경에 관리자 승인 받기

> 조회는 바로 실행됩니다. 변경은 관리자가 휴대폰에서 정확한 SQL과 예상 행 수를 확인하고 승인한 뒤 한 번만 실행됩니다. 데이터베이스는 자사 서버를 떠나지 않습니다.

Source: https://hibira.io/ko/use-cases/database-write-approvals

직원이나 스크립트, 어시스턴트가 데이터베이스를 직접 조회하게 하면 몇 시간을 아낄 수 있습니다. 하지만 아무도 보지 않은 채 바꾸게 하면 "오래된 장바구니 정리해 줘" 한마디에 지난주 주문이 지워질 수도 있습니다. Hibira는 그 사이에 관리자를 둡니다. 조회는 바로 실행되고, 변경은 하나하나 관리자가 휴대폰에서 정확한 SQL과 영향받을 행 수를 보고 승인할 때까지 기다립니다. 데이터베이스와 접속 정보는 자사 서버를 떠나지 않습니다.

## 사용자가 얻는 것

- 읽기만 하는 질문은 누구도 기다리지 않고 바로 답이 나옵니다.
- 변경마다 승인 페이지가 있어 관리자가 푸시 알림이나 QR 코드로 엽니다. SQL, 예상 영향 행 수, 실행 계획, 지문과 함께 승인과 거절 버튼이 있습니다.
- 관리자가 결정하는 순간 요청한 사람의 화면에 결과가 나타납니다. 새로 고침이 필요 없고, 거절되면 관리자의 사유도 함께 옵니다.
- 실행되는 것은 승인된 문장과 정확히 같은 문장이며, 단 한 번만 실행됩니다.

![데모 콘솔: DELETE 하나가 관리자 승인을 기다리고 있고, QR 코드와 예상 영향 행 수, SQL의 SHA-256이 보입니다.](https://hibira.io/portal/articles/sql-approvals-console-ko-light.webp)

## 사용하는 기능

- **[승인](https://hibira.io/ko/guides/approvals)**: `POST /apps/APP_ID/approvals`(또는 [MCP 서버](https://hibira.io/ko/mcp-server)의 `request_approval`)가 Hibira에서 호스팅하는 승인 페이지를 만들고, 결정을 채널로 알립니다.
- **비공개 채널**: 콘솔이나 실행마다 하나, 예를 들어 `private-sqlgate-<random>`. 여기로 `approval` 이벤트가 오므로 폴링하지 않습니다.
- **푸시 알림**(선택): 관리자의 휴대폰으로 링크를 보냅니다.
- **[@hibira-io/approvals/sql](https://hibira.io/ko/guides/approvals)**: Node.js용 `classifySql()`, `sqlFingerprint()`, `assertApprovedSql()`. Python에도 같은 기능이 있습니다.

```text
sequence: 조회는 바로 실행, 변경은 관리자가 승인한 뒤에만 실행
요청자 | 자사 서버 | *Hibira | 관리자
1 > 2: SELECT … | 읽기 전용으로 실행, 결과 바로 반환
1 > 2: DELETE FROM carts … | 시험 실행: 10행
2 > 3: POST /approvals | SQL, 예상 행 수, SHA-256
3 > 4: 푸시 알림 | 승인 페이지 링크
4 > 3: 승인
3 > 1: approval 이벤트 | 비공개 채널, 폴링 없음
1 > 2: 실행 요청
2 > 3: GET /approvals/apr_… | 답을 한 번 확인
2 >> 1: 한 번 실행: 10행 변경
```

[demos.hibira.io](https://demos.hibira.io/examples/nextjs-sql-approvals/)에서 해 볼 수 있습니다. 변경을 고르고 휴대폰으로 QR 코드를 스캔해 승인하세요.

## 1단계: 조회는 통과, 나머지는 모두 멈추기

`classifySql()`은 확실할 때만 조회로 판단합니다. 문장이 하나이고 `SELECT`, `WITH`, `EXPLAIN`, `SHOW`로 시작하며 데이터를 바꾸는 부분이 없을 때입니다. 여러 문장, `WITH ... DELETE`, `EXPLAIN ANALYZE`, `SELECT ... INTO`, `FOR UPDATE`, 확실히 해석할 수 없는 것은 모두 변경입니다. 그래도 조회는 읽기 전용 트랜잭션에서 실행하세요. 잘못 판단한 문장이 있어도 쓰지 않고 실패할 뿐입니다.

```js
import pg from 'pg';
import { classifySql } from '@hibira-io/approvals/sql';

const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });

export async function runSql(sql, requestedBy) {
  if (classifySql(sql) === 'read') return { rows: await readOnly(sql) };
  return { approval: await askSupervisor(sql, requestedBy) };
}

async function readOnly(sql) {
  const db = await pool.connect();
  try {
    await db.query('BEGIN READ ONLY');
    const { rows } = await db.query(sql);
    await db.query('COMMIT');
    return rows.slice(0, 200);
  } catch (err) {
    await db.query('ROLLBACK');
    throw err;
  } finally {
    db.release();
  }
}
```

## 2단계: 변경이 무엇을 할지 관리자에게 보여 주기

요청하기 전에 행 수를 추정합니다. `EXPLAIN`은 문장을 실행하지 않습니다. 롤백되는 트랜잭션에서 시험 실행하면 정확한 수가 나오지만 문장이 실제로 실행되므로 시간 제한을 두세요. 그리고 문장 원문의 지문을 만듭니다. 승인은 이 문장에만 유효합니다.

```js
import { sqlFingerprint } from '@hibira-io/approvals/sql';

async function preview(sql) {
  const db = await pool.connect();
  try {
    await db.query('BEGIN');
    await db.query("SET LOCAL statement_timeout = '5s'");
    const { rowCount } = await db.query(sql);
    return { rows: rowCount, fingerprint: await sqlFingerprint(sql) };
  } finally {
    await db.query('ROLLBACK');
    db.release();
  }
}
```

## 3단계: 승인 요청하기

문장과 미리 보기를 `details`에 넣고, 결정을 알릴 채널을 지정해 요청을 만듭니다. 문장과 지문은 승인 ID와 함께 자사 쪽에 저장합니다. Hibira로 가는 것은 요청 문구뿐이며, 접속 정보도 행 데이터도 보내지 않습니다.

```js
import Pusher from 'pusher';

const hibira = new Pusher({ appId: process.env.HIBIRA_APP_ID, key: process.env.HIBIRA_KEY, secret: process.env.HIBIRA_SECRET, host: 'ws.hibira.io', useTLS: true });

async function askSupervisor(sql, requestedBy) {
  const { rows, fingerprint } = await preview(sql);
  const res = await hibira.post({
    path: '/approvals',
    body: {
      title: 'Run this database change?',
      details: `${sql}\n\nEstimated rows: ${rows}\n\nSHA-256: ${fingerprint}`,
      options: ['Approve', 'Reject'],
      channel: `private-sqlgate-${requestedBy.consoleId}`,
      push_users: ['supervisor-42'],
      expires_in_minutes: 15,
      agent: 'SQL console',
    },
  });
  const approval = await res.json();
  await pool.query('INSERT INTO pending_changes (approval_id, sql, fingerprint) VALUES ($1, $2, $3)', [approval.approval_id, sql, fingerprint]);
  return { id: approval.approval_id, url: approval.url, rows };
}
```

관리자는 링크가 담긴 푸시 알림을 받거나 요청자 화면의 QR 코드를 스캔해, 자신의 언어로 표시된 페이지에서 답합니다.

![관리자 휴대폰의 승인 페이지: SQL, 예상 영향 행 수, SHA-256, 승인과 거절.](https://hibira.io/portal/articles/sql-approvals-page-ko.webp)

## 4단계: 답이 오면 한 번만 실행하기

결정은 채널에 `approval` 이벤트로 도착합니다. 초인종처럼 다루세요. API에서 답을 한 번 읽고, 저장해 둔 문장이 지문과 일치하는지 확인한 뒤 실행합니다. 먼저 행을 선점하면 이벤트나 워커가 동시에 와도 한 번만 실행됩니다.

```js
import { assertApprovedSql } from '@hibira-io/approvals/sql';

// In the requester's page (pusher-js), or a worker that keeps a connection open:
channel.bind('approval', ({ id, reason }) => {
  if (reason === 'decided') fetch(`/api/changes/${id}/run`, { method: 'POST' });
});

// POST /api/changes/:id/run on your server:
export async function runApproved(id) {
  const res = await hibira.get({ path: `/approvals/${id}` });
  const answer = await res.json();
  if (answer.status !== 'approved') return { status: answer.status, comment: answer.comment };
  const { rows } = await pool.query('UPDATE pending_changes SET ran_at = now() WHERE approval_id = $1 AND ran_at IS NULL RETURNING sql, fingerprint', [id]);
  if (!rows.length) return { status: 'already run' };
  await assertApprovedSql(rows[0].sql, rows[0].fingerprint);
  const result = await pool.query(rows[0].sql);
  return { status: 'ran', rows: result.rowCount };
}
```

![승인 후: 콘솔에 변경이 한 번 실행되었고 몇 행이 바뀌었는지 표시됩니다.](https://hibira.io/portal/articles/sql-approvals-done-ko-light.webp)

## 이미 MCP 서버로 데이터베이스에 연결하고 있나요?

팀이 Claude, Cursor, ChatGPT 같은 MCP 클라이언트에서 DBHub, postgres-mcp, Supabase MCP 같은 서버를 통해 데이터베이스를 다룬다면, 그 앞에 [mcp-approval-gate](https://github.com/hibira-io/mcp-approval-gate)만 두면 됩니다. 코드는 바꿀 필요가 없습니다. 조회는 바로 통과하고, INSERT·UPDATE·DELETE나 스키마 변경은 같은 승인 페이지에서 멈췄다가 승인된 그대로 한 번만 실행됩니다.

```sh
npx -y @hibira-io/mcp-approval-gate --label shop-production --push-interests db-approvals \
  -- npx -y @bytebase/dbhub --transport stdio --dsn "$DATABASE_URL"
```

![실제 화면: 주문 1042를 환불됨으로 바꾸는 요청이 멈추고, 관리자가 휴대폰에서 UPDATE를 승인하면 변경이 한 번만 실행됩니다.](https://hibira.io/portal/articles/mcp-approval-gate-ko.mp4)

## 사용하는 스택에서 같은 관문 만들기

- **Vercel AI SDK, Mastra, LangGraph(JavaScript와 Python)**: [@hibira-io/approvals](https://hibira.io/ko/guides/approvals)에 각각의 레시피가 있습니다. `needsApproval`이나 `requireApproval`을 문장으로 판단하는 함수로 만들고, 자체 앱의 승인 받은편지함에서 호출마다 미리 보기를 보여 줍니다.
- **Laravel**: [filament-approvals](https://hibira.io/ko/guides/filament-approvals)는 Filament 패널 안에서 승인을 받습니다. README에 Laravel AI SDK용 `RunSql` 도구가 있습니다.
- **[n8n](https://hibira.io/ko/guides/n8n), [Make](https://hibira.io/ko/guides/make), [Activepieces](https://hibira.io/ko/guides/activepieces), Dify**: 코드 단계에서 문장을 판별하고, Hibira 승인 단계에서 관리자에게 묻고 기다리며, 데이터베이스 단계는 승인 분기에만 연결합니다. 각 README에 워크플로가 있습니다.
- **Claude, Cursor 등 MCP 클라이언트**: [MCP 서버](https://hibira.io/ko/mcp-server)의 `request_approval`로 채팅에서도 같은 방식으로 요청할 수 있습니다.
- **모든 데이터베이스 MCP 서버**: [mcp-approval-gate](https://github.com/hibira-io/mcp-approval-gate)로 DBHub, postgres-mcp, Supabase MCP 등을 코드 변경 없이 감쌀 수 있습니다.
