electric-proxy-auth について
Electric の認証プロキシを設定する開発者向けのスキルです。テナント単位の Shape、パラメーター転送の制御、トークン更新、プロキシ応答の処理を扱います。
主な機能
- 01テナント単位の Shape 設定
- 02プロトコルパラメーターの許可リスト
- 03401 時のトークン更新
- 04CORS レスポンスヘッダーの設定
- 05シークレットと SQL パラメーターの安全な扱い
活用シーン
- 01サーバー側 Electric プロキシの構築
- 02テナント別の Shape データ制限
- 03ShapeStream 用トークンの更新
- 04危険なパラメーター転送の防止
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ electric-sql/ electric --skill electric-proxy-authインストール後、Claude Code で/electric-proxy-authを入力して読み込まれたか確認します。
詳細
このスキルは electric-shapes を基盤としています。ShapeStream の設定については、まずそちらをお読みください。
Electric — プロキシと認証
セットアップ
import { ELECTRIC_PROTOCOL_QUERY_PARAMS } from '@electric-sql/client'
// Server route (Next.js App Router example)
export async function GET(request: Request) {
const url = new URL(request.url)
const originUrl = new URL('/v1/shape', process.env.ELECTRIC_URL)
// Only forward Electric protocol params — never table/where from client
url.searchParams.forEach((value, key) => {
if (ELECTRIC_PROTOCOL_QUERY_PARAMS.includes(key)) {
originUrl.searchParams.set(key, value)
}
})
// Server decides shape definition
originUrl.searchParams.set('table', 'todos')
originUrl.searchParams.set('secret', process.env.ELECTRIC_SOURCE_SECRET!)
const response = await fetch(originUrl)
const headers = new Headers(response.headers)
headers.delete('content-encoding')
headers.delete('content-length')
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
})
}
クライアントでの使用方法:
import { ShapeStream } from '@electric-sql/client'
const stream = new ShapeStream({
url: '/api/todos', // Points to your proxy, not Electric directly
})
主なパターン
WHERE パラメーターによるテナント分離
// In proxy route — inject user context server-side
const user = await getAuthUser(request)
originUrl.searchParams.set('table', 'todos')
originUrl.searchParams.set('where', 'org_id = $1')
originUrl.searchParams.set('params[1]', user.orgId)
401 時の認証トークンの更新
const stream = new ShapeStream({
url: '/api/todos',
headers: {
Authorization: async () => `Bearer ${await getToken()}`,
},
onError: async (error) => {
if (error instanceof FetchError && error.status === 401) {
const newToken = await refreshToken()
return { headers: { Authorization: `Bearer ${newToken}` } }
}
return {}
},
})
クロスオリジンプロキシの CORS 設定
// In proxy response headers
headers.set(
'Access-Control-Expose-Headers',
'electric-offset, electric-handle, electric-schema, electric-cursor'
)
サブセットのセキュリティ(AND のセマンティクス)
Electric は、メインシェイプの WHERE(プロキシで設定)とサブセットの WHERE(POST 本文から取得)を AND で結合します。サブセットで結果を絞り込むことはできますが、範囲を広げることはできません。
-- Main shape: WHERE org_id = $1 (set by proxy)
-- Subset: WHERE status = 'active' (from client POST)
-- Effective: WHERE org_id = $1 AND status = 'active'
サブセットに WHERE 1=1 を指定しても、メインシェイプの WHERE を回避することはできません。
よくある間違い
CRITICAL クライアントのすべてのパラメーターを Electric に転送する
誤り:
url.searchParams.forEach((value, key) => {
originUrl.searchParams.set(key, value)
})
正しい例:
import { ELECTRIC_PROTOCOL_QUERY_PARAMS } from '@electric-sql/client'
url.searchParams.forEach((value, key) => {
if (ELECTRIC_PROTOCOL_QUERY_PARAMS.includes(key)) {
originUrl.searchParams.set(key, value)
}
})
originUrl.searchParams.set('table', 'todos')
すべてのパラメーターを転送すると、クライアントが table、where、columns を制御でき、任意の Postgres テーブルにアクセスできてしまいます。転送するのは ELECTRIC_PROTOCOL_QUERY_PARAMS のみです。
ソース:examples/proxy-auth/app/shape-proxy/route.ts
CRITICAL content-encoding ヘッダーと content-length ヘッダーを削除しない
誤り:
return new Response(response.body, {
status: response.status,
headers: response.headers,
})
正しい例:
const headers = new Headers(response.headers)
headers.delete('content-encoding')
headers.delete('content-length')
return new Response(response.body, { status: response.status, headers })
fetch() はレスポンス本文を展開しますが、元の content-encoding ヘッダーと content-length ヘッダーはそのまま保持するため、ブラウザーでデコードに失敗します。
ソース:examples/proxy-auth/app/shape-proxy/route.ts:49-56
CRITICAL ELECTRIC_SECRET または SOURCE_SECRET をブラウザーに公開する
誤り:
// Client-side code
const url = `/v1/shape?table=todos&secret=${import.meta.env.VITE_ELECTRIC_SOURCE_SECRET}`
正しい例:
// Server proxy only
originUrl.searchParams.set('secret', process.env.ELECTRIC_SOURCE_SECRET!)
Vite などのバンドラーは、VITE_* 環境変数をクライアントコードに公開します。シークレットはプロキシ内でサーバー側からのみ注入してください。
ソース:AGENTS.md:17-20
CRITICAL 文字列補間による WHERE 句の SQL インジェクション
誤り:
originUrl.searchParams.set('where', `org_id = '${user.orgId}'`)
正しい例:
originUrl.searchParams.set('where', 'org_id = $1')
originUrl.searchParams.set('params[1]', user.orgId)
WHERE 句で文字列補間を使うと、SQL インジェクションが可能になります。位置パラメーター($1、$2)を使用してください。
ソース:website/docs/guides/auth.md
HIGH Bun の fetch 同時実行数の上限(256)が高負荷時にプロキシのボトルネックになる
誤り:
bun run proxy.ts
正しい例:
BUN_CONFIG_MAX_HTTP_REQUESTS=4096 bun run proxy.ts
Bun はデフォルトで、fetch() の同時呼び出し数を プロセスあたり 256 件に制限します。上限を超えたリクエストは通知なくキューに入ります。プロキシはインバウンド接続の受け付けを続けますが、Electric へのアップストリーム呼び出しが停滞し、エラーではなくレイテンシの急増として現れます。想定される同時シェイプリクエスト数に合わせて BUN_CONFIG_MAX_HTTP_REQUESTS を増やしてください(最大 65,336)。Node と Deno にはこの上限はありません。
ソース:https://bun.com/docs/runtime/networking/fetch
HIGH CORS 経由で Electric のレスポンスヘッダーを公開しない
誤り:
// No CORS header configuration — browser strips custom headers
return new Response(response.body, { headers })
正しい例:
headers.set(
'Access-Control-Expose-Headers',
'electric-offset, electric-handle, electric-schema, electric-cursor'
)
return new Response(response.body, { headers })
CORS によって Electric のレスポンスヘッダーが取り除かれると、クライアントは MissingHeadersError をスローします。electric-offset、electric-handle、electric-schema、electric-cursor を公開してください。
ソース:packages/typescript-client/src/error.ts:109-118
CRITICAL 本番環境のクライアントから Electric を直接呼び出す
誤り:
new ShapeStream({
url: 'https://my-electric.example.com/v1/shape',
params: { table: 'todos' },
})
正しい例:
new ShapeStream({
url: '/api/todos', // Your proxy route
})
Electric の HTTP API はデフォルトで認証なしの公開状態です。シェイプ定義をサーバーが管理し、シークレットを注入できるように、必ずサーバー経由でプロキシしてください。
ソース:AGENTS.md:19-20
関連項目:electric-shapes/SKILL.md — シェイプ URL は Electric を直接指すのではなく、プロキシのルートを指す必要があります。 関連項目:electric-deployment/SKILL.md — 本番環境では ELECTRIC_SECRET とプロキシが必要です。開発環境では ELECTRIC_INSECURE=true を使用します。 関連項目:electric-postgres-security/SKILL.md — プロキシがシークレットを注入し、Postgres のセキュリティ機能がそれを適用します。
バージョン
@electric-sql/client v1.5.10 を対象としています。